diff --git a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java index 67842e167..a9cd5bd99 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/Shell.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/Shell.java @@ -288,8 +288,8 @@ public List complete(CompletionContext context) { // Try to complete arguments List matchedArgOptions = new ArrayList<>(); - if (argsContext.getWords().size() > 0) { - matchedArgOptions.addAll(matchOptions(registration.getOptions(), argsContext.getWords().get(0))); + if (argsContext.getWords().size() > 0 && argsContext.getWordIndex() > 0 && argsContext.getWords().size() > argsContext.getWordIndex()) { + matchedArgOptions.addAll(matchOptions(registration.getOptions(), argsContext.getWords().get(argsContext.getWordIndex() - 1))); } List argProposals = matchedArgOptions.stream() diff --git a/spring-shell-core/src/main/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolver.java b/spring-shell-core/src/main/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolver.java index b69b05b23..fa33ba387 100644 --- a/spring-shell-core/src/main/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolver.java +++ b/spring-shell-core/src/main/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolver.java @@ -38,11 +38,15 @@ public List apply(CompletionContext context) { List candidates = new ArrayList<>(); context.getCommandRegistration().getOptions().stream() .flatMap(o -> Stream.of(o.getLongNames())) - .map(ln -> new CompletionProposal("--" + ln)) + .map(ln -> "--" + ln) + .filter(ln -> !context.getWords().contains(ln)) + .map(CompletionProposal::new) .forEach(candidates::add); context.getCommandRegistration().getOptions().stream() .flatMap(o -> Stream.of(o.getShortNames())) - .map(ln -> new CompletionProposal("-" + ln)) + .map(ln -> "-" + ln) + .filter(ln -> !context.getWords().contains(ln)) + .map(CompletionProposal::new) .forEach(candidates::add); return candidates; } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/ShellTests.java b/spring-shell-core/src/test/java/org/springframework/shell/ShellTests.java index 184e905c7..f91742921 100644 --- a/spring-shell-core/src/test/java/org/springframework/shell/ShellTests.java +++ b/spring-shell-core/src/test/java/org/springframework/shell/ShellTests.java @@ -311,6 +311,39 @@ public void completionArgWithFunction() throws Exception { assertThat(proposals).containsExactlyInAnyOrder("--arg1"); } + @Test + public void shouldCompleteWithCorrectArgument() throws Exception { + CommandRegistration registration1 = CommandRegistration.builder() + .command("hello world") + .withTarget() + .method(this, "helloWorld") + .and() + .withOption() + .longNames("arg1") + .completion(ctx -> Arrays.asList("arg1Comp1").stream().map(CompletionProposal::new).collect(Collectors.toList())) + .and() + .withOption() + .longNames("arg2") + .completion(ctx -> Arrays.asList("arg2Comp1").stream().map(CompletionProposal::new).collect(Collectors.toList())) + .and() + .build(); + Map registrations = new HashMap<>(); + registrations.put("hello world", registration1); + when(commandRegistry.getRegistrations()).thenReturn(registrations); + + List proposals1 = shell.complete(new CompletionContext(Arrays.asList("hello", "world", "--arg1", ""), 3, "".length(), null, null)) + .stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(proposals1).containsExactlyInAnyOrder("--arg2", "arg1Comp1"); + + List proposals2 = shell.complete(new CompletionContext(Arrays.asList("hello", "world", "--arg1", "xxx", "--arg2", ""), 5, "".length(), null, null)) + .stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(proposals2).containsExactlyInAnyOrder("arg2Comp1"); + + List proposals3 = shell.complete(new CompletionContext(Arrays.asList("hello", "world", "--arg2", "xxx", "--arg1", ""), 5, "".length(), null, null)) + .stream().map(CompletionProposal::value).collect(Collectors.toList()); + assertThat(proposals3).containsExactlyInAnyOrder("arg1Comp1"); + } + private static class Exit extends RuntimeException { } diff --git a/spring-shell-core/src/test/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolverTests.java b/spring-shell-core/src/test/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolverTests.java new file mode 100644 index 000000000..e30bae613 --- /dev/null +++ b/spring-shell-core/src/test/java/org/springframework/shell/completion/RegistrationOptionsCompletionResolverTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.completion; + +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.shell.CompletionContext; +import org.springframework.shell.CompletionProposal; +import org.springframework.shell.command.CommandRegistration; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RegistrationOptionsCompletionResolverTests { + + private final RegistrationOptionsCompletionResolver resolver = new RegistrationOptionsCompletionResolver(); + + @Test + void completesAllOptions() { + CommandRegistration registration = CommandRegistration.builder() + .command("hello world") + .withTarget() + .consumer(ctx -> {}) + .and() + .withOption() + .longNames("arg1") + .and() + .withOption() + .longNames("arg2") + .and() + .build(); + CompletionContext ctx = new CompletionContext(Arrays.asList("hello", "world", ""), 2, "".length(), registration, null); + List proposals = resolver.apply(ctx); + assertThat(proposals).isNotNull(); + assertThat(proposals).hasSize(2); + } + + @Test + void completesNonExistingOptions() { + CommandRegistration registration = CommandRegistration.builder() + .command("hello world") + .withTarget() + .consumer(ctx -> {}) + .and() + .withOption() + .longNames("arg1") + .and() + .withOption() + .longNames("arg2") + .and() + .build(); + CompletionContext ctx = new CompletionContext(Arrays.asList("hello", "world", "--arg1", ""), 2, "".length(), registration, null); + List proposals = resolver.apply(ctx); + assertThat(proposals).isNotNull(); + assertThat(proposals).hasSize(1); + } + +} diff --git a/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/InteractiveCompletionCommands.java b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/InteractiveCompletionCommands.java new file mode 100644 index 000000000..3efa06def --- /dev/null +++ b/spring-shell-samples/src/main/java/org/springframework/shell/samples/e2e/InteractiveCompletionCommands.java @@ -0,0 +1,106 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.shell.samples.e2e; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.context.annotation.Bean; +import org.springframework.shell.CompletionContext; +import org.springframework.shell.CompletionProposal; +import org.springframework.shell.command.CommandRegistration; +import org.springframework.shell.standard.ShellComponent; +import org.springframework.shell.standard.ShellMethod; +import org.springframework.shell.standard.ShellOption; +import org.springframework.shell.standard.ValueProvider; + +@ShellComponent +public class InteractiveCompletionCommands extends BaseE2ECommands { + + @ShellMethod(key = LEGACY_ANNO + "interactive-completion-1", group = GROUP) + public String testInteractiveCompletion1( + @ShellOption(valueProvider = Test1ValuesProvider.class) String arg1, + @ShellOption(valueProvider = Test2ValuesProvider.class) String arg2 + ) { + return "Hello " + arg1; + } + + @Bean + CommandRegistration testInteractiveCompletion1Registration() { + Test1ValuesProvider test1ValuesProvider = new Test1ValuesProvider(); + Test2ValuesProvider test2ValuesProvider = new Test2ValuesProvider(); + return CommandRegistration.builder() + .command(REG, "interactive-completion-1") + .group(GROUP) + .withOption() + .longNames("arg1") + .completion(ctx -> test1ValuesProvider.complete(ctx)) + .and() + .withOption() + .longNames("arg2") + .completion(ctx -> test2ValuesProvider.complete(ctx)) + .and() + .withTarget() + .function(ctx -> { + String arg1 = ctx.getOptionValue("arg1"); + return "Hello " + arg1; + }) + .and() + .build(); + } + + @Bean + Test1ValuesProvider test1ValuesProvider() { + return new Test1ValuesProvider(); + } + + @Bean + Test2ValuesProvider test2ValuesProvider() { + return new Test2ValuesProvider(); + } + + static class Test1ValuesProvider implements ValueProvider { + + private final static String[] VALUES = new String[] { + "values1Complete1", + "values1Complete2" + }; + + @Override + public List complete(CompletionContext completionContext) { + return Arrays.stream(VALUES) + .map(CompletionProposal::new) + .collect(Collectors.toList()); + } + } + + static class Test2ValuesProvider implements ValueProvider { + + private final static String[] VALUES = new String[] { + "values2Complete1", + "values2Complete2" + }; + + @Override + public List complete(CompletionContext completionContext) { + return Arrays.stream(VALUES) + .map(CompletionProposal::new) + .collect(Collectors.toList()); + } + } + +}