Reconsider how input buffer is tokenized

Fixes #85
This commit is contained in:
Eric Bottard
2017-06-19 09:35:11 +02:00
parent 7fd5390c29
commit 14b2401374
4 changed files with 45 additions and 37 deletions

View File

@@ -97,21 +97,19 @@ public class Shell implements CommandRegistry {
resultHandler.handleResult(e);
continue;
}
if (input.words().isEmpty()) {
if (noInput(input)) {
continue;
}
String line = input.words().stream().collect(Collectors.joining(" ")).trim();
List<String> words = input.words();
String command = findLongestCommand(line);
List<String> words = input.words();
Object result;
if (command != null) {
int wordsUsedForCommandKey = command.split(" ").length;
MethodTarget methodTarget = methodTargets.get(command);
List<String> wordsForArgs = words.subList(wordsUsedForCommandKey, words.size());
List<String> wordsForArgs = wordsForArguments(command, words);
Method method = methodTarget.getMethod();
try {
@@ -130,6 +128,29 @@ public class Shell implements CommandRegistry {
}
}
/**
* Return true if the parsed input ends up being empty (<em>e.g.</em> hitting ENTER on an empty line or blank space)
*/
private boolean noInput(Input input) {
return input.words().isEmpty()
|| (input.words().size() == 1 && input.words().get(0).trim().isEmpty());
}
/**
* Returns the list of words to be considered for argument resolving. Drops the first N words used for the
* command, as well as an optional empty word at the end of the list (which may be present if user added spaces
* before submitting the buffer)
*/
private List<String> wordsForArguments(String command, List<String> words) {
int wordsUsedForCommandKey = command.split(" ").length;
List<String> args = words.subList(wordsUsedForCommandKey, words.size());
int last = args.size() - 1;
if (last >= 0 && "".equals(args.get(last))) {
args.remove(last);
}
return args;
}
/**
* Gather completion proposals given some (incomplete) input the user has already typed in.
* When and how this method is invoked is implementation specific and decided by the actual user interface.
@@ -139,39 +160,30 @@ public class Shell implements CommandRegistry {
String prefix = context.upToCursor();
List<CompletionProposal> candidates = new ArrayList<>();
// Find the longest match for a command name with words in the buffer
String best = findLongestCommand(prefix);
if (best == null) { // no command found
candidates.addAll(commandsStartingWith(prefix));
return candidates;
} // if we're here, we're either trying to complete args for command <best> (will fall through)
// or trying to complete command whose name starts with <best> (which also happens to be a command)
else if (prefix.equals(best)) {
candidates.addAll(commandsStartingWith(best));
} // valid command (<best>) followed by a suffix (but not necessarily [<space> args*])
else if (!prefix.startsWith(best + " ")) {
// must be an invalid command, can't do anything
return candidates;
}
candidates.addAll(commandsStartingWith(prefix));
CompletionContext argsContext = context.drop(best.split(" ").length);
// Try to complete arguments
MethodTarget methodTarget = methodTargets.get(best);
Method method = methodTarget.getMethod();
return Arrays.stream(method.getParameters())
.map(Utils::createMethodParameter)
.flatMap(mp -> findResolver(mp).complete(mp, argsContext).stream())
.collect(Collectors.toList());
String best = findLongestCommand(prefix);
if (best != null) {
CompletionContext argsContext = context.drop(best.split(" ").length);
// Try to complete arguments
MethodTarget methodTarget = methodTargets.get(best);
Method method = methodTarget.getMethod();
Arrays.stream(method.getParameters())
.map(Utils::createMethodParameter)
.flatMap(mp -> findResolver(mp).complete(mp, argsContext).stream())
.forEach(candidates::add);
}
return candidates;
}
private List<CompletionProposal> commandsStartingWith(String prefix) {
return methodTargets.entrySet().stream()
.filter(e -> e.getKey().startsWith(prefix))
.map(e -> toCompletionProposal(e.getKey(), e.getValue()))
.map(e -> toCommandProposal(e.getKey(), e.getValue()))
.collect(Collectors.toList());
}
private CompletionProposal toCompletionProposal(String command, MethodTarget methodTarget) {
private CompletionProposal toCommandProposal(String command, MethodTarget methodTarget) {
return new CompletionProposal(command)
.dontQuote(true)
.category("Available commands")

View File

@@ -132,7 +132,6 @@ public class JLineShell {
words = words.stream()
.map(s -> s.replaceAll("^\\n+|\\n+$", "")) // CR at beginning/end of line introduced by backslash continuation
.map(s -> s.replaceAll("\\n+", " ")) // CR in middle of word introduced by return inside a quoted string
.filter(w -> w.length() > 0) // Empty word introduced when using quotes, no idea why...
.collect(Collectors.toList());
return words;
}

View File

@@ -38,7 +38,7 @@ import org.springframework.stereotype.Component;
@ShellComponent("")
public class Commands {
@ShellMethod(help = "a command whose name looks the same as another one")
@ShellMethod(help = "a command whose name looks the same as another one", value = "help me out")
public void helpMeOut() {
System.out.println("You can go");
}

View File

@@ -297,7 +297,7 @@ public class StandardParameterResolver implements ParameterResolver {
if (!set) {
if (unfinished == null) { // case 1 above
return commandsThatStartWithContextPrefix(methodParameter, context);
return argumentKeysThatStartWithContextPrefix(methodParameter, context);
} // case 2
else {
return valueCompletions(methodParameter, context);
@@ -306,9 +306,6 @@ public class StandardParameterResolver implements ParameterResolver {
else {
List<CompletionProposal> result = new ArrayList<>();
String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : "";
// TODO: should not look at last word only, but everything after what was used for key
Object value = convertRawValue(parameterRawValue, methodParameter);
if (value instanceof Collection && ((Collection) value).size() == arity
|| (ObjectUtils.isArray(value) && Array.getLength(value) == arity)) {
@@ -320,7 +317,7 @@ public class StandardParameterResolver implements ParameterResolver {
if (parameterRawValue.positional()) {
// Case 4.1: There exists "--command foo" and user has typed "--comm" which (wrongly) got resolved as a positional param
result.addAll(commandsThatStartWithContextPrefix(methodParameter, context));
result.addAll(argumentKeysThatStartWithContextPrefix(methodParameter, context));
}
return result;
}
@@ -333,7 +330,7 @@ public class StandardParameterResolver implements ParameterResolver {
.findFirst().orElseGet(() -> Collections.emptyList());
}
private List<CompletionProposal> commandsThatStartWithContextPrefix(MethodParameter methodParameter, CompletionContext context) {
private List<CompletionProposal> argumentKeysThatStartWithContextPrefix(MethodParameter methodParameter, CompletionContext context) {
String prefix = context.currentWordUpToCursor() != null ? context.currentWordUpToCursor() : "";
return describe(methodParameter).keys().stream()
.filter(k -> k.startsWith(prefix))