diff --git a/spring-shell-jline/src/main/java/org/springframework/shell/jline/CommandCompleter.java b/spring-shell-jline/src/main/java/org/springframework/shell/jline/CommandCompleter.java index c5a9d5dbc..a88c1d380 100644 --- a/spring-shell-jline/src/main/java/org/springframework/shell/jline/CommandCompleter.java +++ b/spring-shell-jline/src/main/java/org/springframework/shell/jline/CommandCompleter.java @@ -93,20 +93,23 @@ private boolean isOptionPresent(ParsedLine line, CommandOption option) { } @Nullable private Command findCommandByWords(List words) { + Command match = null; StringBuilder commandName = new StringBuilder(); for (String word : words) { if (word.startsWith("-")) { break; } commandName.append(word).append(" "); + Command candidate = this.commandRegistry.getCommandByName(commandName.toString().trim()); + if (candidate != null) { + match = candidate; + } } - - Command command = this.commandRegistry.getCommandByName(commandName.toString().trim()); // the command is found but was not completed on the line - if (command != null && getCommandNames(command).toList().contains(String.join(" ", words))) { - command = null; + if (match != null && getCommandNames(match).toList().contains(String.join(" ", words))) { + match = null; } - return command; + return match; } @Nullable private CommandOption findOptionByWords(List words, List options) { diff --git a/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandCompleterTests.java b/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandCompleterTests.java index 0d1e6fe30..5f2d4fac6 100644 --- a/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandCompleterTests.java +++ b/spring-shell-jline/src/test/java/org/springframework/shell/jline/CommandCompleterTests.java @@ -171,7 +171,13 @@ static Stream completeData() { Arguments.of(List.of("hello", "--first", "Paul", "--last=Noris", ""), List.of()), Arguments.of(List.of("hello", "--first", "Paul", "-l=Noris", ""), List.of()), Arguments.of(List.of("hello", "-f", "Paul", "--last=Noris", ""), List.of()), - Arguments.of(List.of("hello", "-f", "Paul", "-l=Noris", ""), List.of())); + Arguments.of(List.of("hello", "-f", "Paul", "-l=Noris", ""), List.of()), + + // positional arguments after the command should not break completion + // (gh-1346) + Arguments.of(List.of("hello", "IN"), List.of("--first", "--last", "-f", "-l")), + Arguments.of(List.of("hello", "INBOUND"), List.of("--first", "--last", "-f", "-l")), + Arguments.of(List.of("hello", "IN", ""), List.of("--first", "--last", "-f", "-l"))); } @ParameterizedTest @@ -326,7 +332,12 @@ static Stream completeWithSubCommandsData() { Arguments.of(List.of("hello world", "--first", ""), List.of("Mary", "Paul", "Peter")), Arguments.of(List.of("hello world", "--last", ""), List.of("Chan", "Noris")), - Arguments.of(List.of("hello world", "--first", "Paul", "--last", "Noris", ""), List.of())); + Arguments.of(List.of("hello world", "--first", "Paul", "--last", "Noris", ""), List.of()), + + // positional arguments after a multi-word command should not break + // completion (gh-1346) + Arguments.of(List.of("hello world", "IN"), List.of("--first", "--last", "-f", "-l")), + Arguments.of(List.of("hello world", "IN", ""), List.of("--first", "--last", "-f", "-l"))); } @ParameterizedTest @@ -486,4 +497,42 @@ static Stream completeForCommandAlias() { Arguments.of(List.of("bye", ""), List.of())); } + @ParameterizedTest + @MethodSource("completeForPositionalArgumentData") + void testCompleteForPositionalArgument(List words, List expectedValues) { + // given + CompletionProvider positionalProvider = completionContext -> { + if (completionContext.getCommandOption() != null) { + return Collections.emptyList(); + } + String typed = completionContext.getWords().get(completionContext.getWordIndex()); + return Stream.of("INBOUND", "OUTBOUND", "INTERNAL") + .filter(v -> v.startsWith(typed)) + .map(CompletionProposal::new) + .toList(); + }; + when(command.getCompletionProvider()).thenReturn(positionalProvider); + when(command.getOptions()).thenReturn(List.of()); + + List candidates = new ArrayList<>(); + ParsedLine line = mock(ParsedLine.class); + when(line.words()).thenReturn(words); + when(line.wordIndex()).thenReturn(words.size() - 1); + when(line.word()).thenReturn(words.get(words.size() - 1)); + when(line.line()).thenReturn(String.join(" ", words)); + + // when + completer.complete(mock(LineReader.class), line, candidates); + + // then + assertEquals(expectedValues, toCandidateNames(candidates)); + } + + static Stream completeForPositionalArgumentData() { + return Stream.of(Arguments.of(List.of("hello", ""), List.of("INBOUND", "INTERNAL", "OUTBOUND")), + Arguments.of(List.of("hello", "IN"), List.of("INBOUND", "INTERNAL")), + Arguments.of(List.of("hello", "INBOUND"), List.of("INBOUND")), + Arguments.of(List.of("hello", "OUT"), List.of("OUTBOUND"))); + } + } \ No newline at end of file