Skip to content

Commit 5eaa5dd

Browse files
committed
Implement interactive completion
- This is a re-implementation of a interactive completion with breaking changes as it moves away from a direct use of a MethodParameter in favour of a CommandRegistration and its option definitions. - Fixes #449
1 parent 341a69e commit 5eaa5dd

File tree

18 files changed

+364
-247
lines changed

18 files changed

+364
-247
lines changed

spring-shell-autoconfigure/src/main/java/org/springframework/shell/boot/CompleterAutoConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public static class CompleterAdapter implements Completer {
5252
public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) {
5353
CompletingParsedLine cpl = (line instanceof CompletingParsedLine) ? ((CompletingParsedLine) line) : t -> t;
5454

55-
CompletionContext context = new CompletionContext(sanitizeInput(line.words()), line.wordIndex(), line.wordCursor());
55+
CompletionContext context = new CompletionContext(sanitizeInput(line.words()), line.wordIndex(), line.wordCursor(), null, null);
5656

5757
List<CompletionProposal> proposals = shell.complete(context);
5858
proposals.stream()

spring-shell-core/src/main/java/org/springframework/shell/CompletionContext.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import java.util.List;
2121
import java.util.stream.Collectors;
2222

23+
import org.springframework.shell.command.CommandOption;
24+
import org.springframework.shell.command.CommandRegistration;
25+
2326
/**
2427
* Represents the buffer context in which completion was triggered.
2528
*
@@ -33,16 +36,22 @@ public class CompletionContext {
3336

3437
private final int position;
3538

39+
private final CommandOption commandOption;
40+
41+
private final CommandRegistration commandRegistration;
42+
3643
/**
3744
*
3845
* @param words words in the buffer, excluding words for the command name
3946
* @param wordIndex the index of the word the cursor is in
4047
* @param position the position inside the current word where the cursor is
4148
*/
42-
public CompletionContext(List<String> words, int wordIndex, int position) {
49+
public CompletionContext(List<String> words, int wordIndex, int position, CommandRegistration commandRegistration, CommandOption commandOption) {
4350
this.words = words;
4451
this.wordIndex = wordIndex;
4552
this.position = position;
53+
this.commandRegistration = commandRegistration;
54+
this.commandOption = commandOption;
4655
}
4756

4857
public List<String> getWords() {
@@ -57,6 +66,14 @@ public int getPosition() {
5766
return position;
5867
}
5968

69+
public CommandOption getCommandOption() {
70+
return commandOption;
71+
}
72+
73+
public CommandRegistration getCommandRegistration() {
74+
return commandRegistration;
75+
}
76+
6077
public String upToCursor() {
6178
String start = words.subList(0, wordIndex).stream().collect(Collectors.joining(" "));
6279
if (wordIndex < words.size()) {
@@ -84,6 +101,11 @@ public String currentWordUpToCursor() {
84101
* Return a copy of this context, as if the first {@literal nbWords} were not present
85102
*/
86103
public CompletionContext drop(int nbWords) {
87-
return new CompletionContext(new ArrayList<String>(words.subList(nbWords, words.size())), wordIndex-nbWords, position);
104+
return new CompletionContext(new ArrayList<String>(words.subList(nbWords, words.size())), wordIndex - nbWords,
105+
position, commandRegistration, commandOption);
106+
}
107+
108+
public CompletionContext commandOption(CommandOption commandOption) {
109+
return new CompletionContext(words, wordIndex, position, commandRegistration, commandOption);
88110
}
89111
}

spring-shell-core/src/main/java/org/springframework/shell/Shell.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.Optional;
2323
import java.util.function.Function;
2424
import java.util.stream.Collectors;
25+
import java.util.stream.Stream;
2526

2627
import javax.validation.Validator;
2728
import javax.validation.ValidatorFactory;
@@ -38,13 +39,15 @@
3839
import org.springframework.shell.command.CommandAlias;
3940
import org.springframework.shell.command.CommandCatalog;
4041
import org.springframework.shell.command.CommandExecution;
42+
import org.springframework.shell.command.CommandOption;
4143
import org.springframework.shell.command.CommandExecution.CommandExecutionException;
4244
import org.springframework.shell.command.CommandExecution.CommandExecutionHandlerMethodArgumentResolvers;
4345
import org.springframework.shell.command.CommandRegistration;
4446
import org.springframework.shell.completion.CompletionResolver;
4547
import org.springframework.shell.context.InteractionMode;
4648
import org.springframework.shell.context.ShellContext;
4749
import org.springframework.shell.exit.ExitCodeMappings;
50+
import org.springframework.util.StringUtils;
4851

4952
/**
5053
* Main class implementing a shell loop.
@@ -275,17 +278,83 @@ public List<CompletionProposal> complete(CompletionContext context) {
275278
String best = findLongestCommand(prefix);
276279
if (best != null) {
277280
CompletionContext argsContext = context.drop(best.split(" ").length);
278-
// Try to complete arguments
279281
CommandRegistration registration = commandRegistry.getRegistrations().get(best);
280282

281283
for (CompletionResolver resolver : completionResolvers) {
282284
List<CompletionProposal> resolved = resolver.resolve(registration, argsContext);
283285
candidates.addAll(resolved);
284286
}
287+
288+
// Try to complete arguments
289+
List<CommandOption> matchedArgOptions = new ArrayList<>();
290+
if (argsContext.getWords().size() > 0) {
291+
matchedArgOptions.addAll(matchOptions(registration.getOptions(), argsContext.getWords().get(0)));
292+
}
293+
294+
List<CompletionProposal> argProposals = matchedArgOptions.stream()
295+
.flatMap(o -> {
296+
Function<CompletionContext, List<CompletionProposal>> completion = o.getCompletion();
297+
if (completion != null) {
298+
List<CompletionProposal> apply = completion.apply(argsContext.commandOption(o));
299+
return apply.stream();
300+
}
301+
return Stream.empty();
302+
})
303+
.collect(Collectors.toList());
304+
305+
candidates.addAll(argProposals);
285306
}
286307
return candidates;
287308
}
288309

310+
private List<CommandOption> matchOptions(List<CommandOption> options, String arg) {
311+
List<CommandOption> matched = new ArrayList<>();
312+
String trimmed = StringUtils.trimLeadingCharacter(arg, '-');
313+
int count = arg.length() - trimmed.length();
314+
if (count == 1) {
315+
if (trimmed.length() == 1) {
316+
Character trimmedChar = trimmed.charAt(0);
317+
options.stream()
318+
.filter(o -> {
319+
for (Character sn : o.getShortNames()) {
320+
if (trimmedChar.equals(sn)) {
321+
return true;
322+
}
323+
}
324+
return false;
325+
})
326+
.findFirst()
327+
.ifPresent(o -> matched.add(o));
328+
}
329+
else if (trimmed.length() > 1) {
330+
trimmed.chars().mapToObj(i -> (char)i)
331+
.forEach(c -> {
332+
options.stream().forEach(o -> {
333+
for (Character sn : o.getShortNames()) {
334+
if (c.equals(sn)) {
335+
matched.add(o);
336+
}
337+
}
338+
});
339+
});
340+
}
341+
}
342+
else if (count == 2) {
343+
options.stream()
344+
.filter(o -> {
345+
for (String ln : o.getLongNames()) {
346+
if (trimmed.equals(ln)) {
347+
return true;
348+
}
349+
}
350+
return false;
351+
})
352+
.findFirst()
353+
.ifPresent(o -> matched.add(o));
354+
}
355+
return matched;
356+
}
357+
289358
private List<CompletionProposal> commandsStartingWith(String prefix) {
290359
// Workaround for https://github.com/spring-projects/spring-shell/issues/150
291360
// (sadly, this ties this class to JLine somehow)

spring-shell-core/src/main/java/org/springframework/shell/command/CommandOption.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
*/
1616
package org.springframework.shell.command;
1717

18+
import java.util.List;
19+
import java.util.function.Function;
20+
1821
import org.springframework.core.ResolvableType;
22+
import org.springframework.shell.CompletionContext;
23+
import org.springframework.shell.CompletionProposal;
1924

2025
/**
2126
* Interface representing an option in a command.
@@ -94,6 +99,13 @@ public interface CommandOption {
9499
*/
95100
String getLabel();
96101

102+
/**
103+
* Gets a completion function.
104+
*
105+
* @return the completion function
106+
*/
107+
Function<CompletionContext, List<CompletionProposal>> getCompletion();
108+
97109
/**
98110
* Gets an instance of a default {@link CommandOption}.
99111
*
@@ -103,7 +115,7 @@ public interface CommandOption {
103115
* @return default command option
104116
*/
105117
public static CommandOption of(String[] longNames, Character[] shortNames, String description) {
106-
return of(longNames, shortNames, description, null, false, null, null, null, null, null);
118+
return of(longNames, shortNames, description, null, false, null, null, null, null, null, null);
107119
}
108120

109121
/**
@@ -117,7 +129,7 @@ public static CommandOption of(String[] longNames, Character[] shortNames, Strin
117129
*/
118130
public static CommandOption of(String[] longNames, Character[] shortNames, String description,
119131
ResolvableType type) {
120-
return of(longNames, shortNames, description, type, false, null, null, null, null, null);
132+
return of(longNames, shortNames, description, type, false, null, null, null, null, null, null);
121133
}
122134

123135
/**
@@ -133,13 +145,14 @@ public static CommandOption of(String[] longNames, Character[] shortNames, Strin
133145
* @param arityMin the min arity
134146
* @param arityMax the max arity
135147
* @param label the label
148+
* @param completion the completion
136149
* @return default command option
137150
*/
138151
public static CommandOption of(String[] longNames, Character[] shortNames, String description,
139152
ResolvableType type, boolean required, String defaultValue, Integer position, Integer arityMin,
140-
Integer arityMax, String label) {
153+
Integer arityMax, String label, Function<CompletionContext, List<CompletionProposal>> completion) {
141154
return new DefaultCommandOption(longNames, shortNames, description, type, required, defaultValue, position,
142-
arityMin, arityMax, label);
155+
arityMin, arityMax, label, completion);
143156
}
144157

145158
/**
@@ -157,10 +170,12 @@ public static class DefaultCommandOption implements CommandOption {
157170
private int arityMin;
158171
private int arityMax;
159172
private String label;
173+
private Function<CompletionContext, List<CompletionProposal>> completion;
160174

161175
public DefaultCommandOption(String[] longNames, Character[] shortNames, String description,
162176
ResolvableType type, boolean required, String defaultValue, Integer position,
163-
Integer arityMin, Integer arityMax, String label) {
177+
Integer arityMin, Integer arityMax, String label,
178+
Function<CompletionContext, List<CompletionProposal>> completion) {
164179
this.longNames = longNames != null ? longNames : new String[0];
165180
this.shortNames = shortNames != null ? shortNames : new Character[0];
166181
this.description = description;
@@ -171,6 +186,7 @@ public DefaultCommandOption(String[] longNames, Character[] shortNames, String d
171186
this.arityMin = arityMin != null ? arityMin : -1;
172187
this.arityMax = arityMax != null ? arityMax : -1;
173188
this.label = label;
189+
this.completion = completion;
174190
}
175191

176192
@Override
@@ -222,5 +238,10 @@ public int getArityMax() {
222238
public String getLabel() {
223239
return label;
224240
}
241+
242+
@Override
243+
public Function<CompletionContext, List<CompletionProposal>> getCompletion() {
244+
return completion;
245+
}
225246
}
226247
}

spring-shell-core/src/main/java/org/springframework/shell/command/CommandRegistration.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import org.springframework.core.ResolvableType;
3030
import org.springframework.lang.Nullable;
3131
import org.springframework.shell.Availability;
32+
import org.springframework.shell.CompletionContext;
33+
import org.springframework.shell.CompletionProposal;
3234
import org.springframework.shell.context.InteractionMode;
3335
import org.springframework.util.Assert;
3436
import org.springframework.util.ObjectUtils;
@@ -208,6 +210,14 @@ public interface OptionSpec {
208210
*/
209211
OptionSpec label(String label);
210212

213+
/**
214+
* Define a {@code completion function} for an option.
215+
*
216+
* @param completion the completion function
217+
* @return option spec for chaining
218+
*/
219+
OptionSpec completion(Function<CompletionContext, List<CompletionProposal>> completion);
220+
211221
/**
212222
* Return a builder for chaining.
213223
*
@@ -528,6 +538,7 @@ static class DefaultOptionSpec implements OptionSpec {
528538
private Integer arityMin;
529539
private Integer arityMax;
530540
private String label;
541+
private Function<CompletionContext, List<CompletionProposal>> completion;
531542

532543
DefaultOptionSpec(BaseBuilder builder) {
533544
this.builder = builder;
@@ -626,6 +637,12 @@ public OptionSpec label(String label) {
626637
return this;
627638
}
628639

640+
@Override
641+
public OptionSpec completion(Function<CompletionContext, List<CompletionProposal>> completion) {
642+
this.completion = completion;
643+
return this;
644+
}
645+
629646
@Override
630647
public Builder and() {
631648
return builder;
@@ -670,6 +687,10 @@ public Integer getArityMax() {
670687
public String getLabel() {
671688
return label;
672689
}
690+
691+
public Function<CompletionContext, List<CompletionProposal>> getCompletion() {
692+
return completion;
693+
}
673694
}
674695

675696
static class DefaultTargetSpec implements TargetSpec {
@@ -840,7 +861,7 @@ public List<CommandOption> getOptions() {
840861
return optionSpecs.stream()
841862
.map(o -> CommandOption.of(o.getLongNames(), o.getShortNames(), o.getDescription(), o.getType(),
842863
o.isRequired(), o.getDefaultValue(), o.getPosition(), o.getArityMin(), o.getArityMax(),
843-
o.getLabel()))
864+
o.getLabel(), o.getCompletion()))
844865
.collect(Collectors.toList());
845866
}
846867

0 commit comments

Comments
 (0)