Closed loetermann closed 8 years ago
This is not possible out of the box.
You could leverage the org.jline.builtins.Completers
helpers but it only support options (starting with '-') with a following string, not arguments with a following string.
Another option is to write your own completer:
completer = (reader, line, candidates) -> {
if (line.wordIndex() == 0) {
candidates.add(new Candidate("Command1"));
} else if (line.words().get(0).equals("Command1")) {
if (line.words().get(line.wordIndex() - 1).equals("Option1")) {
candidates.add(new Candidate("Param1"));
candidates.add(new Candidate("Param2"));
} else {
if (line.wordIndex() == 1) {
candidates.add(new Candidate("Option1"));
}
if (!line.words().contains("Option2")) {
candidates.add(new Candidate("Option2"));
}
if (!line.words().contains("Option3")) {
candidates.add(new Candidate("Option3"));
}
}
}
};
Thanks for the answer! I took a look into the implementation of the ArgumentCompleter and understood that it's sub-completers are supposed to return Candidates which represent exactly one word. I wonder if this is true for Candidates in general, but I will put that into a separate issue.
The Completers class looks almost like what I need. My goal is to develop a dynamic Completer or a Completer Factory based on JCommander objects (see http://jcommander.org/). Basically I think I only have to change the '-' into a variable character and add support for subcommands, like git add ...
(this could become complicated again).
I still wonder if a general completer like I had in mind in the beginning would be possible. I think the problem is the ParsedLine. If I could derive a subclass (FakeLine) from a ParsedLine with the features to move the cursor and to discard everything before the cursor, I think the following code would work (more or less):
public class SequenceCompleter implements Completer {
private final List<Completer> completers;
public SequenceCompleter(List<Completer> completers) {
this.completers = completers;
}
@Override
public void complete(LineReader reader, ParsedLine line, List<Candidate> candidates) {
// Derive a FakeLine from any ParsedLine
FakeLine fakeLine = new FakeParsedLine(line);
// create a new FakeLine with a cursor moved by one
fakeLine = fakeLine.setCursor(0);
complete(reader, line, candidates, fakeLine, 0);
}
private void complete(LineReader reader, ParsedLine line, List<Candidate> candidates,
FakeLine fakeLine, int currentCompleterIndex, int currentIndex) {
if (currentCompleterIndex >= completers.size() || currentIndex > line.cursor()) {
return;
}
Completer currentCompleter = completers.get(currentCompleterIndex);
String parsedText = fakeLine.line().substring(0, fakeLine.cursor()));
List<Candidate> subCandidates = Lists.newArrayList();
currentCompleter.complete(reader, fakeLine, subCandidates);
for (Candidate subCandidate : subCandidates) {
if(line.cursor() == currentIndex) {
if(subCandidate.complete()
&& completers.size() > currentCompleterIndex + 1) {
// the following two line would simply create a copy of
// the candidate with some modifications
subCandidate.append(" ");
subCandidate.setComplete(false);
candidates.add(subCandidate);
} else {
candidates.add(subCandidate);
}
}else if (subCandidate.complete()
&& subCandidate.value().equals(parsedText) {
// create a new FakeLine from the currentIndex till the end
FakeLine newFakeLine = fakeLine.subLine(fakeLine.cursor());
complete(reader, line, candidates,
newFakeLine, currentCompleterIndex+1, currentIndex+1);
}
}
if(!subCandidates.isEmpty()) {
// create a new ParsedLine with a cursor moved by one
FakeLine newFakeLine = fakeLine.setCursor(fakeLine.cursor()+1);
complete(reader, line, candidates,
fakeLine, currentCompleterIndex, currentIndex+1);
}
}
}
The problem with moving the cursor is updating the wordCursor/Index accordingly.
I thought about generating a new ParsedLine by feeding a substring of the unparsed line into the Parser but this could be problematic if the substring is not well formed, e.g. if command "composed parameter" -option
is split within the quotes.
I'm aware that the runtime of this method can grow rapidly due to the recursion within the loop but it should be fine in the normal cases.
I would appreciate any further help on this topic. Otherwise this issue can be closed.
We do have a similar use case in Karaf, though the annotations are slightly different. We use the following ArgumentCompleter. Note that each option or argument on the command may have its own simple completer and we combine them. At first glance, your proposal is quite similar. For verifying individual arguments, we use a ParsedLine which contains a single argument, see the code below
protected boolean verifyCompleter(Session session, Completer completer, String argument) {
List<Candidate> candidates = new ArrayList<>();
completer.completeCandidates(session, new ArgumentCommandLine(argument, argument.length()), candidates);
return !candidates.isEmpty();
}
Note that the API is slightly different as Karaf provides its own API, but it's really close.
I tried to create a completer for the following command syntax:
Command1 [Param1 (Option1 | Option2) | Param2 | Param3]
Here is what I tried (JLine Version 3.0.1):Com + tab
is completed correctly toCommand1
.Command1 Par + tab
is completed toParam2 Param3
but I expectedParam1
to be in the list as well.Command1 Param1 + tab
is completed toParam2 Param3
, so it is somehow parsed correctly as the completion still works (in the unexpected way). I expectedOption1 Option2
in this case and thatParam2 Param3
is only proposed again when an option is specified.Command1 Param1 Opt + tab
does not result in any completion proposals.In case I misunderstood the architecture and this result is the expected one, I would appreciate some help how to implement my desired completer.