Added support for suggestions tabcompletions of a command input

This commit is contained in:
Nathan Adams 2017-06-22 09:05:16 +02:00
parent eba88b2773
commit f372eb3b98
12 changed files with 186 additions and 10 deletions

View file

@ -2,6 +2,7 @@ package com.mojang.brigadier;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.CommandContextBuilder;
@ -12,6 +13,7 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -23,7 +25,7 @@ public class CommandDispatcher<T> {
}
};
public static final SimpleCommandExceptionType ERROR_UNKNOWN_COMMAND = new SimpleCommandExceptionType("unknown_command", "Unknown command");
public static final SimpleCommandExceptionType ERROR_UNKNOWN_COMMAND = new SimpleCommandExceptionType("command.unknown", "Unknown command");
public static final String ARGUMENT_SEPARATOR = " ";
private static final String USAGE_OPTIONAL_OPEN = "[";
private static final String USAGE_OPTIONAL_CLOSE = "]";
@ -52,7 +54,11 @@ public class CommandDispatcher<T> {
if (child.getCommand() != null) {
context.withCommand(child.getCommand());
}
return parseNodes(child, remaining, context);
if (remaining.isEmpty()) {
return context.build();
} else {
return parseNodes(child, remaining.substring(1), context);
}
} catch (CommandException ex) {
exception = ex;
}
@ -106,4 +112,28 @@ public class CommandDispatcher<T> {
return result.toString();
}
private Set<String> findSuggestions(CommandNode node, String command, CommandContextBuilder<T> contextBuilder, Set<String> result) {
for (CommandNode child : node.getChildren()) {
try {
CommandContextBuilder<T> context = contextBuilder.copy();
String remaining = child.parse(command, context);
if (remaining.isEmpty()) {
child.listSuggestions(command, result);
} else {
return findSuggestions(child, remaining.substring(1), context, result);
}
} catch (CommandException e) {
child.listSuggestions(command, result);
}
}
return result;
}
public String[] getCompletionSuggestions(String command, T source) {
final Set<String> nodes = findSuggestions(root, command, new CommandContextBuilder<>(source), Sets.newLinkedHashSet());
return nodes.toArray(new String[nodes.size()]);
}
}

View file

@ -3,6 +3,10 @@ package com.mojang.brigadier.arguments;
import com.mojang.brigadier.context.ParsedArgument;
import com.mojang.brigadier.exceptions.CommandException;
import java.util.Set;
public interface CommandArgumentType<T> {
ParsedArgument<T> parse(String command) throws CommandException;
void listSuggestions(String command, Set<String> output);
}

View file

@ -7,10 +7,12 @@ import com.mojang.brigadier.context.ParsedArgument;
import com.mojang.brigadier.exceptions.CommandException;
import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType;
import java.util.Set;
public class IntegerArgumentType implements CommandArgumentType<Integer> {
public static final ParameterizedCommandExceptionType ERROR_NOT_A_NUMBER = new ParameterizedCommandExceptionType("argument-integer-invalid", "Expected an integer, found '${found}'", "found");
public static final ParameterizedCommandExceptionType ERROR_TOO_SMALL = new ParameterizedCommandExceptionType("argument-integer-low", "Integer must not be less than ${minimum}, found ${found}", "found", "minimum");
public static final ParameterizedCommandExceptionType ERROR_TOO_BIG = new ParameterizedCommandExceptionType("argument-integer-big", "Integer must not be more than ${maximum}, found ${found}", "found", "maximum");
public static final ParameterizedCommandExceptionType ERROR_NOT_A_NUMBER = new ParameterizedCommandExceptionType("argument.integer.invalid", "Expected an integer, found '${found}'", "found");
public static final ParameterizedCommandExceptionType ERROR_TOO_SMALL = new ParameterizedCommandExceptionType("argument.integer.low", "Integer must not be less than ${minimum}, found ${found}", "found", "minimum");
public static final ParameterizedCommandExceptionType ERROR_TOO_BIG = new ParameterizedCommandExceptionType("argument.integer.big", "Integer must not be more than ${maximum}, found ${found}", "found", "maximum");
private static final Splitter SPLITTER = Splitter.on(CommandDispatcher.ARGUMENT_SEPARATOR).limit(2);
@ -58,6 +60,10 @@ public class IntegerArgumentType implements CommandArgumentType<Integer> {
}
}
@Override
public void listSuggestions(String command, Set<String> output) {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -6,6 +6,8 @@ import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.ParsedArgument;
import com.mojang.brigadier.exceptions.CommandException;
import java.util.Set;
public class ArgumentCommandNode<T> extends CommandNode {
private static final String USAGE_ARGUMENT_OPEN = "<";
private static final String USAGE_ARGUMENT_CLOSE = ">";
@ -46,12 +48,17 @@ public class ArgumentCommandNode<T> extends CommandNode {
contextBuilder.withNode(this, parsed.getRaw());
if (command.length() > start) {
return command.substring(start + 1);
return command.substring(start);
} else {
return "";
}
}
@Override
public void listSuggestions(String command, Set<String> output) {
type.listSuggestions(command, output);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -7,6 +7,7 @@ import com.mojang.brigadier.exceptions.CommandException;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
public abstract class CommandNode {
private final Map<Object, CommandNode> children = Maps.newLinkedHashMap();
@ -62,4 +63,6 @@ public abstract class CommandNode {
public abstract String getUsageText();
public abstract String parse(String command, CommandContextBuilder<?> contextBuilder) throws CommandException;
public abstract void listSuggestions(String command, Set<String> output);
}

View file

@ -6,8 +6,10 @@ import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.exceptions.CommandException;
import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType;
import java.util.Set;
public class LiteralCommandNode extends CommandNode {
public static final ParameterizedCommandExceptionType ERROR_INCORRECT_LITERAL = new ParameterizedCommandExceptionType("incorrect_literal", "Expected literal ${expected}", "expected");
public static final ParameterizedCommandExceptionType ERROR_INCORRECT_LITERAL = new ParameterizedCommandExceptionType("argument.literal.incorrect", "Expected literal ${expected}", "expected");
private final String literal;
@ -34,10 +36,17 @@ public class LiteralCommandNode extends CommandNode {
}
contextBuilder.withNode(this, literal);
int start = expected.length();
int start = literal.length();
return command.substring(start);
}
@Override
public void listSuggestions(String command, Set<String> output) {
if (literal.startsWith(command)) {
output.add(literal);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -3,6 +3,8 @@ package com.mojang.brigadier.tree;
import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.exceptions.CommandException;
import java.util.Set;
public class RootCommandNode extends CommandNode {
public RootCommandNode() {
super(null);
@ -23,6 +25,10 @@ public class RootCommandNode extends CommandNode {
return command;
}
@Override
public void listSuggestions(String command, Set<String> output) {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View file

@ -0,0 +1,56 @@
package com.mojang.brigadier;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal;
import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
@RunWith(MockitoJUnitRunner.class)
public class CommandDispatcherCompletionsTest {
private CommandDispatcher<Object> subject;
@Mock
private Object source;
@Before
public void setUp() throws Exception {
subject = new CommandDispatcher<>();
}
@Test
public void testNoCommands() throws Exception {
assertThat(subject.getCompletionSuggestions("", source), is(emptyArray()));
}
@Test
public void testCommand() throws Exception {
subject.register(literal("foo"));
subject.register(literal("bar"));
assertThat(subject.getCompletionSuggestions("", source), equalTo(new String[] {"foo", "bar"}));
assertThat(subject.getCompletionSuggestions("f", source), equalTo(new String[] {"foo"}));
assertThat(subject.getCompletionSuggestions("b", source), equalTo(new String[] {"bar"}));
assertThat(subject.getCompletionSuggestions("q", source), is(emptyArray()));
}
@Test
public void testSubCommand() throws Exception {
subject.register(literal("foo").then(literal("abc")).then(literal("def")));
subject.register(literal("bar"));
assertThat(subject.getCompletionSuggestions("", source), equalTo(new String[] {"foo", "bar"}));
assertThat(subject.getCompletionSuggestions("f", source), equalTo(new String[] {"foo"}));
assertThat(subject.getCompletionSuggestions("foo", source), equalTo(new String[] {"foo"}));
assertThat(subject.getCompletionSuggestions("foo ", source), equalTo(new String[] {"abc", "def"}));
assertThat(subject.getCompletionSuggestions("foo a", source), equalTo(new String[] {"abc"}));
assertThat(subject.getCompletionSuggestions("foo d", source), equalTo(new String[] {"def"}));
assertThat(subject.getCompletionSuggestions("foo g", source), is(emptyArray()));
}
}

View file

@ -1,6 +1,7 @@
package com.mojang.brigadier.arguments;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.testing.EqualsTester;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.CommandContextBuilder;
@ -11,8 +12,10 @@ import org.junit.Before;
import org.junit.Test;
import java.util.Map;
import java.util.Set;
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasToString;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@ -90,6 +93,13 @@ public class IntegerArgumentTypeTest {
assertThat(IntegerArgumentType.getInteger(context, "foo"), is(100));
}
@Test
public void testSuggestions() throws Exception {
Set<String> set = Sets.newHashSet();
type.listSuggestions("", set);
assertThat(set, is(empty()));
}
@Test
public void testEquals() throws Exception {
new EqualsTester()

View file

@ -1,6 +1,7 @@
package com.mojang.brigadier.tree;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.testing.EqualsTester;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.IntegerArgumentType;
@ -11,9 +12,12 @@ import org.junit.Before;
import org.junit.Test;
import java.util.Map;
import java.util.Set;
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
@ -36,7 +40,7 @@ public class ArgumentCommandNodeTest extends AbstractCommandNodeTest {
@Test
public void testParse() throws Exception {
assertThat(node.parse("123 456", contextBuilder), is("456"));
assertThat(node.parse("123 456", contextBuilder), is(" 456"));
assertThat(contextBuilder.getArguments().containsKey("foo"), is(true));
assertThat(contextBuilder.getArguments().get("foo").getResult(), is(123));
@ -66,6 +70,13 @@ public class ArgumentCommandNodeTest extends AbstractCommandNodeTest {
assertThat(node.getUsageText(), is("<foo>"));
}
@Test
public void testSuggestions() throws Exception {
Set<String> set = Sets.newHashSet();
node.listSuggestions("", set);
assertThat(set, is(empty()));
}
@Test
public void testEquals() throws Exception {
Command command = mock(Command.class);

View file

@ -1,6 +1,7 @@
package com.mojang.brigadier.tree;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.testing.EqualsTester;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.context.CommandContextBuilder;
@ -10,8 +11,11 @@ import org.junit.Before;
import org.junit.Test;
import java.util.Map;
import java.util.Set;
import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
@ -34,7 +38,7 @@ public class LiteralCommandNodeTest extends AbstractCommandNodeTest {
@Test
public void testParse() throws Exception {
assertThat(node.parse("foo bar", contextBuilder), is("bar"));
assertThat(node.parse("foo bar", contextBuilder), is(" bar"));
}
@Test
@ -69,6 +73,25 @@ public class LiteralCommandNodeTest extends AbstractCommandNodeTest {
assertThat(node.getUsageText(), is("foo"));
}
@Test
public void testSuggestions() throws Exception {
Set<String> set = Sets.newHashSet();
node.listSuggestions("", set);
assertThat(set, equalTo(Sets.newHashSet("foo")));
set.clear();
node.listSuggestions("foo", set);
assertThat(set, equalTo(Sets.newHashSet("foo")));
set.clear();
node.listSuggestions("food", set);
assertThat(set, is(empty()));
set.clear();
node.listSuggestions("b", set);
assertThat(set, is(empty()));
}
@Test
public void testEquals() throws Exception {
Command command = mock(Command.class);

View file

@ -1,11 +1,15 @@
package com.mojang.brigadier.tree;
import com.google.common.collect.Sets;
import com.google.common.testing.EqualsTester;
import com.mojang.brigadier.context.CommandContextBuilder;
import org.junit.Before;
import org.junit.Test;
import java.util.Set;
import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
@ -37,6 +41,13 @@ public class RootCommandNodeTest extends AbstractCommandNodeTest {
assertThat(node.getUsageText(), is(""));
}
@Test
public void testSuggestions() throws Exception {
Set<String> set = Sets.newHashSet();
node.listSuggestions("", set);
assertThat(set, is(empty()));
}
@Test
public void testEquals() throws Exception {
new EqualsTester()