Skip to content

Commit

Permalink
Fix #969: autocomplete & suggestions for nested subcommands not working
Browse files Browse the repository at this point in the history
in Picocli Shell JLine3?
  • Loading branch information
mattirn authored and remkop committed Apr 10, 2020
1 parent 3a996ac commit c10bd91
Showing 1 changed file with 64 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@
import java.util.stream.Collectors;

import org.jline.builtins.CommandRegistry;
import org.jline.builtins.Completers.OptionCompleter;
import org.jline.builtins.Completers.SystemCompleter;
import org.jline.builtins.Options.HelpException;
import org.jline.builtins.Widgets.ArgDesc;
import org.jline.builtins.Widgets.CmdDesc;
import org.jline.reader.Candidate;
import org.jline.reader.Completer;
import org.jline.reader.impl.completer.AggregateCompleter;
import org.jline.reader.impl.completer.ArgumentCompleter;
import org.jline.reader.impl.completer.NullCompleter;
import org.jline.reader.impl.completer.StringsCompleter;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
import org.jline.utils.AttributedString;
import picocli.CommandLine;
import picocli.CommandLine.Help;
Expand All @@ -25,7 +23,6 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -67,75 +64,78 @@ public boolean hasCommand(String command) {
return commands.contains(command) || aliasCommand.containsKey(command);
}

private static class CommandHierarchyCompleter extends SystemCompleter {
List<CommandHierarchyCompleter> nested = new ArrayList<>();
@Override
public void compile() {
super.compile();
nested.forEach(CommandHierarchyCompleter::compile);
}
}

/**
*
* @return SystemCompleter for command completion
*/
public SystemCompleter compileCompleters() {
return createCompilableCompleter(cmd);
SystemCompleter out = new SystemCompleter();
List<String> all = new ArrayList<>();
all.addAll(commands);
all.addAll(aliasCommand.keySet());
out.add(all, new PicocliCompleter());
return out;
}

private CommandHierarchyCompleter createCompilableCompleter(CommandLine cmd) {
CommandHierarchyCompleter result = new CommandHierarchyCompleter();
for (CommandLine sub : cmd.getSubcommands().values()) {
String commandName = sub.getCommandName();
for (String alias : sub.getCommandSpec().aliases()) {
result.getAliases().put(alias, commandName);
}
ArgumentCompleter argumentCompleter = createArgumentCompleter(result, sub.getCommandSpec());
result.add(commandName, argumentCompleter);
}
return result;
}
private class PicocliCompleter implements Completer {

private ArgumentCompleter createArgumentCompleter(CommandHierarchyCompleter hierarchy, CommandSpec spec) {
Completer optionCompleter = createOptionCompleter(hierarchy, spec);
return new ArgumentCompleter(new StringsCompleter(spec.name()), optionCompleter);
}
public PicocliCompleter() {}

private Completer createOptionCompleter(CommandHierarchyCompleter hierarchy, CommandSpec spec) {
// TODO positional parameter completion
// JLine OptionCompleter need to be improved with option descriptions and option value completion,
// now it completes only strings.
List<String> options = new ArrayList<>();
Map<String, List<String>> optionValues = new HashMap<>();
for (OptionSpec option : spec.options()) {
if (option.arity().max() == 0) {
options.addAll(Arrays.asList(option.names()));
@Override
public void complete(LineReader reader, ParsedLine commandLine, List<Candidate> candidates) {
assert commandLine != null;
assert candidates != null;
String word = commandLine.word();
List<String> words = commandLine.words();
CommandLine sub = cmd;
for (int i = 0; i < commandLine.wordIndex(); i++) {
if (!words.get(i).startsWith("-")) {
sub = findSubcommandLine(sub,words.get(i));
if (sub == null) {
return;
}
}
}
if (word.startsWith("-")) {
String buffer = word.substring(0, commandLine.wordCursor());
int eq = buffer.indexOf('=');
for (OptionSpec option : sub.getCommandSpec().options()) {
if (option.arity().max() == 0 && eq < 0) {
addCandidates(candidates, Arrays.asList(option.names()));
} else {
if (eq > 0) {
String opt = buffer.substring(0, eq);
if (Arrays.asList(option.names()).contains(opt) && option.completionCandidates() != null) {
addCandidates(candidates, option.completionCandidates(), buffer.substring(0, eq + 1), "", true);
}
} else {
addCandidates(candidates, Arrays.asList(option.names()), "", "=", false);
}
}
}
} else {
List<String> values = new ArrayList<>();
if (option.completionCandidates() != null) {
option.completionCandidates().forEach(values::add);
addCandidates(candidates, sub.getSubcommands().keySet());
for (CommandLine s : sub.getSubcommands().values()) {
addCandidates(candidates, Arrays.asList(s.getCommandSpec().aliases()));
}
for (String name : option.names()) {
optionValues.put(name, values);
}
}

private void addCandidates(List<Candidate> candidates, Iterable<String> cands) {
addCandidates(candidates, cands, "", "", true);
}

private void addCandidates(List<Candidate> candidates, Iterable<String> cands, String preFix, String postFix, boolean complete) {
for (String s : cands) {
candidates.add(new Candidate(AttributedString.stripAnsi(preFix + s + postFix), s, null, null, null, null, complete));
}
}

private CommandLine findSubcommandLine(CommandLine cmdline, String command) {
for (CommandLine s : cmdline.getSubcommands().values()) {
if (s.getCommandName().equals(command) || Arrays.asList(s.getCommandSpec().aliases()).contains(command)) {
return s;
}
}
return null;
}
// TODO support nested sub-subcommands (https://github.com/remkop/picocli/issues/969)
//
List<Completer> subcommands = new ArrayList<>();
// for (CommandLine sub : spec.subcommands().values()) {
// // TODO unfortunately createCompilableCompleter will not include an OptionCompleter for sub...
// CommandHierarchyCompleter subCompleter = createCompilableCompleter(sub);
// hierarchy.nested.add(subCompleter); // ensure subCompleter is compiled when top-level completer is compiled
// subcommands.add(subCompleter);
// }
// Completer nestedCompleters = new AggregateCompleter(subcommands);
Completer nestedCompleters = NullCompleter.INSTANCE;

return options.isEmpty() && optionValues.isEmpty() && subcommands.isEmpty()
? NullCompleter.INSTANCE
: new OptionCompleter(nestedCompleters, optionValues, options, 1);
}

/**
Expand Down

0 comments on commit c10bd91

Please sign in to comment.