Skip to content

Commit

Permalink
#508: enabled autocompletion for commandlet options (#833)
Browse files Browse the repository at this point in the history
  • Loading branch information
WorkingAmeise authored Dec 5, 2024
1 parent d439c2d commit 2a4e15c
Show file tree
Hide file tree
Showing 14 changed files with 360 additions and 114 deletions.
23 changes: 23 additions & 0 deletions cli/src/main/java/com/devonfw/tools/ide/cli/CliArguments.java
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ public void stopSplitShortOptions() {
this.splitShortOpts = false;
}

/**
* @return {@code true} if short options (e.g. "-bdf") should not be split (e.g. into "-b -d -f" for "--batch --debug --force"), {@code false} otherwise.
*/
public boolean isSplitShortOpts() {

return splitShortOpts;
}

/**
* @return {@code true} if the options have ended, {@code false} otherwise.
* @see CliArgument#isEndOptions()
Expand All @@ -87,6 +95,21 @@ private void setCurrent(CliArgument arg) {
}
}

/**
* @return {@code true} if the last argument shall be {@link CliArgument#isCompletion() completed}, {@code false}.
*/
public boolean isCompletion() {

CliArgument arg = this.currentArg;
while ((arg != null) && !arg.isEnd()) {
if (arg.isCompletion()) {
return true;
}
arg = arg.next;
}
return false;
}

/**
* @return the initial {@link CliArgument}.
*/
Expand Down
13 changes: 11 additions & 2 deletions cli/src/main/java/com/devonfw/tools/ide/commandlet/Commandlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public List<Property<?>> getValues() {
/**
* Clear the set values on all properties of the {@link Commandlet#propertiesList}
*/
public void clearProperties() {
public void reset() {

for (Property<?> property : this.propertiesList) {
property.clearValue();
Expand All @@ -94,10 +94,19 @@ public Property<?> getOption(String nameOrAlias) {
*/
protected void addKeyword(String keyword) {

addKeyword(keyword, null);
}

/**
* @param keyword the {@link KeywordProperty keyword} to {@link #add(Property) add}.
* @param alias the optional {@link KeywordProperty#getAlias() alias}.
*/
protected void addKeyword(String keyword, String alias) {

if (this.properties.isEmpty()) {
this.firstKeyword = keyword;
}
add(new KeywordProperty(keyword, true, null));
add(new KeywordProperty(keyword, true, alias));
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.devonfw.tools.ide.commandlet;

import java.util.Collection;
import java.util.Iterator;

import com.devonfw.tools.ide.cli.CliArguments;
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
import com.devonfw.tools.ide.property.KeywordProperty;
import com.devonfw.tools.ide.property.Property;
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
Expand Down Expand Up @@ -92,4 +95,15 @@ default LocalToolCommandlet getRequiredLocalToolCommandlet(String name) {
throw new IllegalArgumentException("The commandlet " + name + " is not a LocalToolCommandlet!");
}

/**
* @param arguments the {@link CliArguments}.
* @param collector the optional {@link CompletionCandidateCollector}. Will be {@code null} if no argument {@link CliArguments#isCompletion() completion}
* shall be performed.
* @return an {@link Iterator} of the matching {@link Commandlet}(s). Typically empty or containing a single {@link Commandlet}. Only in edge-cases multiple
* {@link Commandlet}s could be found (e.g. if two {@link Commandlet}s exist with the same keyword but with different mandatory properties such as in our
* legacy devonfw-ide "ide get version ..." and "ide get edition ..." - however, we redesigned our CLI to "ide get-version ..." and "ide get-edition ..."
* to simplify this).
*/
Iterator<Commandlet> findCommandlet(CliArguments arguments, CompletionCandidateCollector collector);

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import com.devonfw.tools.ide.cli.CliArgument;
import com.devonfw.tools.ide.cli.CliArguments;
import com.devonfw.tools.ide.completion.CompletionCandidateCollector;
import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.property.KeywordProperty;
import com.devonfw.tools.ide.property.Property;
Expand Down Expand Up @@ -44,6 +49,8 @@
*/
public class CommandletManagerImpl implements CommandletManager {

private final IdeContext context;

private final Map<Class<? extends Commandlet>, Commandlet> commandletTypeMap;

private final Map<String, Commandlet> commandletNameMap;
Expand All @@ -60,6 +67,7 @@ public class CommandletManagerImpl implements CommandletManager {
public CommandletManagerImpl(IdeContext context) {

super();
this.context = context;
this.commandletTypeMap = new HashMap<>();
this.commandletNameMap = new HashMap<>();
this.firstKeywordMap = new HashMap<>();
Expand Down Expand Up @@ -123,14 +131,14 @@ protected void add(Commandlet commandlet) {
boolean hasRequiredProperty = false;
List<Property<?>> properties = commandlet.getProperties();
int propertyCount = properties.size();
String keyword = commandlet.getKeyword();
if (keyword != null) {
this.firstKeywordMap.putIfAbsent(keyword, commandlet);
}
for (int i = 0; i < propertyCount; i++) {
Property<?> property = properties.get(i);
if (property.isRequired()) {
hasRequiredProperty = true;
if ((i == 0) && (property instanceof KeywordProperty)) {
String keyword = property.getName();
this.firstKeywordMap.putIfAbsent(keyword, commandlet);
}
break;
}
}
Expand Down Expand Up @@ -163,8 +171,7 @@ public <C extends Commandlet> C getCommandlet(Class<C> commandletType) {
@Override
public Commandlet getCommandlet(String name) {

Commandlet commandlet = this.commandletNameMap.get(name);
return commandlet;
return this.commandletNameMap.get(name);
}

@Override
Expand All @@ -173,4 +180,90 @@ public Commandlet getCommandletByFirstKeyword(String keyword) {
return this.firstKeywordMap.get(keyword);
}

@Override
public Iterator<Commandlet> findCommandlet(CliArguments arguments, CompletionCandidateCollector collector) {

CliArgument current = arguments.current();
if (current.isEnd()) {
return Collections.emptyIterator();
}
String keyword = current.get();
Commandlet commandlet = getCommandletByFirstKeyword(keyword);
if ((commandlet == null) && (collector == null)) {
return Collections.emptyIterator();
}
return new CommandletFinder(commandlet, arguments.copy(), collector);
}

private final class CommandletFinder implements Iterator<Commandlet> {

private final Commandlet firstCandidate;

private final Iterator<Commandlet> commandletIterator;

private final CliArguments arguments;

private final CompletionCandidateCollector collector;

private Commandlet next;

private CommandletFinder(Commandlet firstCandidate, CliArguments arguments, CompletionCandidateCollector collector) {

this.firstCandidate = firstCandidate;
this.commandletIterator = getCommandlets().iterator();
this.arguments = arguments;
this.collector = collector;
if (isSuitable(firstCandidate)) {
this.next = firstCandidate;
} else {
this.next = findNext();
}
}

@Override
public boolean hasNext() {

return this.next != null;
}

@Override
public Commandlet next() {

if (this.next == null) {
throw new NoSuchElementException();
}
Commandlet result = this.next;
this.next = findNext();
return result;
}

private boolean isSuitable(Commandlet commandlet) {

return (commandlet != null) && (!commandlet.isIdeHomeRequired() || (context.getIdeHome() != null));
}

private Commandlet findNext() {
while (this.commandletIterator.hasNext()) {
Commandlet cmd = this.commandletIterator.next();
if ((cmd != this.firstCandidate) && isSuitable(cmd)) {
List<Property<?>> properties = cmd.getProperties();
// validation should already be done in add method and could be removed here...
if (properties.isEmpty()) {
assert false : cmd.getClass().getSimpleName() + " has no properties!";
} else {
Property<?> property = properties.get(0);
if (property instanceof KeywordProperty) {
boolean matches = property.apply(arguments.copy(), context, cmd, this.collector);
if (matches) {
return cmd;
}
} else {
assert false : cmd.getClass().getSimpleName() + " is invalid as first property must be keyword property but is " + property;
}
}
}
}
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.property.FlagProperty;
import com.devonfw.tools.ide.version.IdeVersion;

/**
Expand All @@ -18,7 +17,7 @@ public class VersionCommandlet extends Commandlet {
public VersionCommandlet(IdeContext context) {

super(context);
addKeyword(new FlagProperty(getName(), true, "-v"));
addKeyword(getName(), "-v");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public IdeCompleter(AbstractIdeContext context) {
public void complete(LineReader reader, ParsedLine commandLine, List<Candidate> candidates) {
List<String> words = commandLine.words();
CliArguments args = CliArguments.ofCompletion(words.toArray(String[]::new));
List<CompletionCandidate> completion = this.context.complete(args, false);
List<CompletionCandidate> completion = this.context.complete(args, true);
int i = 0;
for (CompletionCandidate candidate : completion) {
candidates.add(new Candidate(candidate.text(), candidate.text(), null, null, null, null, true, i++));
Expand Down
Loading

0 comments on commit 2a4e15c

Please sign in to comment.