Skip to content

Commit

Permalink
Fix multi-word flag suggestions (#736)
Browse files Browse the repository at this point in the history
* Half-baked fix for multi-word flag suggestions

* Don't ignore result future for flag parsing

* fix jd

* restore old cursor restoration logic

* Keep suggesting after successfully parsed flag value if there is no following space

We could better support greedy flags by using similar logic to flag yielding strings here. However, for now this solves the multi-word suggestion issue.

* Flag yielding parsers will consume a single trailing dash

---------

Co-authored-by: Alexander Söderberg <[email protected]>
  • Loading branch information
jpenilla and Citymonstret authored May 13, 2024
1 parent 5d31f7d commit c11969f
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 31 deletions.
25 changes: 15 additions & 10 deletions cloud-core/src/main/java/org/incendo/cloud/CommandTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ private boolean matchesLiteral(final @NonNull List<@NonNull CommandNode<C>> chil
continue;
}
suggestionFuture = suggestionFuture
.thenCompose(ctx -> this.addSuggestionsForDynamicArgument(context, commandInput, child, executor));
.thenCompose(ctx -> this.addSuggestionsForDynamicArgument(context, commandInput, child, executor, false));
}

return suggestionFuture;
Expand Down Expand Up @@ -769,28 +769,33 @@ private boolean matchesLiteral(final @NonNull List<@NonNull CommandNode<C>> chil
final @NonNull SuggestionContext<C, ?> context,
final @NonNull CommandInput commandInput,
final @NonNull CommandNode<C> child,
final @NonNull Executor executor
final @NonNull Executor executor,
final boolean inFlag
) {
final CommandComponent<C> component = child.component();
if (component == null) {
return CompletableFuture.completedFuture(context);
}

if (component.parser() instanceof CommandFlagParser) {
if (!inFlag && component.parser() instanceof CommandFlagParser) {
// Use the flag argument parser to deduce what flag is being suggested right now
// If empty, then no flag value is being typed, and the different flag options should
// be suggested instead.
final CommandFlagParser<C> parser = (CommandFlagParser<C>) component.parser();
final Optional<String> lastFlag = parser.parseCurrentFlag(context.commandContext(), commandInput);
if (lastFlag.isPresent()) {
context.commandContext().store(CommandFlagParser.FLAG_META_KEY, lastFlag.get());
} else {
context.commandContext().remove(CommandFlagParser.FLAG_META_KEY);
}

return parser.parseCurrentFlag(context.commandContext(), commandInput, executor).thenCompose(lastFlag -> {
if (lastFlag.isPresent()) {
context.commandContext().store(CommandFlagParser.FLAG_META_KEY, lastFlag.get());
} else {
context.commandContext().remove(CommandFlagParser.FLAG_META_KEY);
}
return this.addSuggestionsForDynamicArgument(context, commandInput, child, executor, true);
});
}

if (commandInput.isEmpty() || commandInput.remainingTokens() == 1
|| (child.isLeaf() && child.component().parser() instanceof AggregateParser)) {
|| (child.isLeaf() && child.component().parser() instanceof AggregateParser)
|| (child.isLeaf() && child.component().parser() instanceof CommandFlagParser)) {
return this.addArgumentSuggestions(context, child, commandInput, executor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apiguardian.api.API;
Expand Down Expand Up @@ -63,6 +64,10 @@ public final class CommandFlagParser<C> implements ArgumentParser.FutureArgument
* Metadata for the last argument that was suggested
*/
public static final CloudKey<String> FLAG_META_KEY = CloudKey.of("__last_flag__", TypeToken.get(String.class));
/**
* Metadata for the position of the cursor before parsing the last flag's value
*/
public static final CloudKey<Integer> FLAG_CURSOR_KEY = CloudKey.of("__flag_cursor__", TypeToken.get(Integer.class));
/**
* Metadata for the set of parsed flags, used to detect duplicates.
*/
Expand Down Expand Up @@ -110,35 +115,35 @@ public CommandFlagParser(final @NonNull Collection<@NonNull CommandFlag<?>> flag
*
* @param commandContext Command context
* @param commandInput The input arguments
* @param completionExecutor The completion executor
* @return current flag being typed, or {@code empty()} if none is
*/
@API(status = API.Status.STABLE)
public @NonNull Optional<String> parseCurrentFlag(
public @NonNull CompletableFuture<Optional<String>> parseCurrentFlag(
final @NonNull CommandContext<@NonNull C> commandContext,
final @NonNull CommandInput commandInput
final @NonNull CommandInput commandInput,
final @NonNull Executor completionExecutor
) {
/* If empty, nothing to do */
if (commandInput.isEmpty()) {
return Optional.empty();
return CompletableFuture.completedFuture(Optional.empty());
}

/* Before parsing, retrieve the last known input of the queue */
final String lastInputValue = commandInput.lastRemainingToken();

/* Parse, but ignore the result of parsing */
final FlagParser parser = new FlagParser();
parser.parse(commandContext, commandInput);

/*
* If the parser parsed the entire queue, restore the last typed
* input obtained earlier.
*/
if (commandInput.isEmpty()) {
final int count = lastInputValue.length();
commandInput.moveCursor(-count);
}

return Optional.ofNullable(parser.lastParsedFlag());
final CompletableFuture<@NonNull ArgumentParseResult<Object>> result = parser.parse(commandContext, commandInput);

return result.thenApplyAsync(parseResult -> {
if (commandContext.contains(FLAG_CURSOR_KEY)) {
commandInput.cursor(commandContext.get(FLAG_CURSOR_KEY));
} else if (parser.lastParsedFlag() == null && commandInput.isEmpty()) {
final int count = lastInputValue.length();
commandInput.moveCursor(-count);
}
return Optional.ofNullable(parser.lastParsedFlag());
}, completionExecutor);
}

@Override
Expand Down Expand Up @@ -485,6 +490,7 @@ private final class FlagParser {

// The flag has no argument, so we're done.
if (flag.commandComponent() == null) {
commandContext.remove(FLAG_CURSOR_KEY);
commandContext.flags().addPresenceFlag(flag);
parsedFlags.add(flag);
return CompletableFuture.completedFuture(null);
Expand Down Expand Up @@ -515,12 +521,17 @@ private final class FlagParser {

// We then attempt to parse the flag.
final CommandFlag parsingFlag = flag;
final CommandInput commandInputCopy = commandInput.copy();
return ((CommandComponent<C>) flag.commandComponent())
.parser()
.parseFuture(
commandContext,
commandInput
).thenApply(parsedValue -> {
if (parsedValue.failure().isPresent() || commandInput.isEmpty() || commandInput.peek() != ' ') {
commandContext.store(FLAG_CURSOR_KEY, commandInputCopy.cursor());
}

// Forward parsing errors.
if (parsedValue.failure().isPresent()) {
return (ArgumentParseResult<Object>) parsedValue;
Expand All @@ -531,8 +542,12 @@ private final class FlagParser {
// At this point we know the flag parsed successfully.
parsedFlags.add(parsingFlag);

// We're no longer parsing a flag.
this.lastParsedFlag = null;
if (!commandInput.isEmpty(false)) {
if (commandInput.peek() == ' ') {
// We're no longer parsing a flag.
this.lastParsedFlag = null;
}
}

return null;
});
Expand All @@ -548,7 +563,7 @@ private final class FlagParser {
}

private @NonNull CompletableFuture<ArgumentParseResult<Object>> fail(final @NonNull Throwable exception) {
return CompletableFuture.completedFuture(ArgumentParseResult.failure(exception));
return ArgumentParseResult.failureFuture(exception);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -404,11 +404,11 @@ void testCompoundFlags() {

final String input5 = "flags3 --compound 22 ";
final List<? extends Suggestion> suggestions5 = this.manager.suggestionFactory().suggestImmediately(new TestCommandSender(), input5).list();
Assertions.assertEquals(suggestionList("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"), suggestions5);
Assertions.assertEquals(suggestionList("22 0", "22 1", "22 2", "22 3", "22 4", "22 5", "22 6", "22 7", "22 8", "22 9"), suggestions5);

final String input6 = "flags3 --compound 22 1";
final List<? extends Suggestion> suggestions6 = this.manager.suggestionFactory().suggestImmediately(new TestCommandSender(), input6).list();
Assertions.assertEquals(suggestionList("1", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19"), suggestions6);
Assertions.assertEquals(suggestionList("22 1", "22 10", "22 11", "22 12", "22 13", "22 14", "22 15", "22 16", "22 17", "22 18", "22 19"), suggestions6);

/* We've typed compound already, so that flag should be omitted from the suggestions */
final String input7 = "flags3 --compound 22 33 44 ";
Expand Down

0 comments on commit c11969f

Please sign in to comment.