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
@@ -2,6 +2,7 @@ package com.mojang.brigadier;
import com.google.common.collect.ComparisonChain; import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.context.CommandContextBuilder;
@@ -12,6 +13,7 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
import com.mojang.brigadier.tree.RootCommandNode; import com.mojang.brigadier.tree.RootCommandNode;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; 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 = " "; public static final String ARGUMENT_SEPARATOR = " ";
private static final String USAGE_OPTIONAL_OPEN = "["; private static final String USAGE_OPTIONAL_OPEN = "[";
private static final String USAGE_OPTIONAL_CLOSE = "]"; private static final String USAGE_OPTIONAL_CLOSE = "]";
@@ -52,7 +54,11 @@ public class CommandDispatcher<T> {
if (child.getCommand() != null) { if (child.getCommand() != null) {
context.withCommand(child.getCommand()); 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) { } catch (CommandException ex) {
exception = ex; exception = ex;
} }
@@ -106,4 +112,28 @@ public class CommandDispatcher<T> {
return result.toString(); 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()]);
}
} }
@@ -3,6 +3,10 @@ package com.mojang.brigadier.arguments;
import com.mojang.brigadier.context.ParsedArgument; import com.mojang.brigadier.context.ParsedArgument;
import com.mojang.brigadier.exceptions.CommandException; import com.mojang.brigadier.exceptions.CommandException;
import java.util.Set;
public interface CommandArgumentType<T> { public interface CommandArgumentType<T> {
ParsedArgument<T> parse(String command) throws CommandException; ParsedArgument<T> parse(String command) throws CommandException;
void listSuggestions(String command, Set<String> output);
} }
@@ -7,10 +7,12 @@ import com.mojang.brigadier.context.ParsedArgument;
import com.mojang.brigadier.exceptions.CommandException; import com.mojang.brigadier.exceptions.CommandException;
import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType; import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType;
import java.util.Set;
public class IntegerArgumentType implements CommandArgumentType<Integer> { 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_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_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_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); 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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -6,6 +6,8 @@ import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.context.ParsedArgument; import com.mojang.brigadier.context.ParsedArgument;
import com.mojang.brigadier.exceptions.CommandException; import com.mojang.brigadier.exceptions.CommandException;
import java.util.Set;
public class ArgumentCommandNode<T> extends CommandNode { public class ArgumentCommandNode<T> extends CommandNode {
private static final String USAGE_ARGUMENT_OPEN = "<"; private static final String USAGE_ARGUMENT_OPEN = "<";
private static final String USAGE_ARGUMENT_CLOSE = ">"; private static final String USAGE_ARGUMENT_CLOSE = ">";
@@ -46,12 +48,17 @@ public class ArgumentCommandNode<T> extends CommandNode {
contextBuilder.withNode(this, parsed.getRaw()); contextBuilder.withNode(this, parsed.getRaw());
if (command.length() > start) { if (command.length() > start) {
return command.substring(start + 1); return command.substring(start);
} else { } else {
return ""; return "";
} }
} }
@Override
public void listSuggestions(String command, Set<String> output) {
type.listSuggestions(command, output);
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -7,6 +7,7 @@ import com.mojang.brigadier.exceptions.CommandException;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Set;
public abstract class CommandNode { public abstract class CommandNode {
private final Map<Object, CommandNode> children = Maps.newLinkedHashMap(); private final Map<Object, CommandNode> children = Maps.newLinkedHashMap();
@@ -62,4 +63,6 @@ public abstract class CommandNode {
public abstract String getUsageText(); public abstract String getUsageText();
public abstract String parse(String command, CommandContextBuilder<?> contextBuilder) throws CommandException; public abstract String parse(String command, CommandContextBuilder<?> contextBuilder) throws CommandException;
public abstract void listSuggestions(String command, Set<String> output);
} }
@@ -6,8 +6,10 @@ import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.exceptions.CommandException; import com.mojang.brigadier.exceptions.CommandException;
import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType; import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType;
import java.util.Set;
public class LiteralCommandNode extends CommandNode { 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; private final String literal;
@@ -34,10 +36,17 @@ public class LiteralCommandNode extends CommandNode {
} }
contextBuilder.withNode(this, literal); contextBuilder.withNode(this, literal);
int start = expected.length(); int start = literal.length();
return command.substring(start); return command.substring(start);
} }
@Override
public void listSuggestions(String command, Set<String> output) {
if (literal.startsWith(command)) {
output.add(literal);
}
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -3,6 +3,8 @@ package com.mojang.brigadier.tree;
import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.context.CommandContextBuilder;
import com.mojang.brigadier.exceptions.CommandException; import com.mojang.brigadier.exceptions.CommandException;
import java.util.Set;
public class RootCommandNode extends CommandNode { public class RootCommandNode extends CommandNode {
public RootCommandNode() { public RootCommandNode() {
super(null); super(null);
@@ -23,6 +25,10 @@ public class RootCommandNode extends CommandNode {
return command; return command;
} }
@Override
public void listSuggestions(String command, Set<String> output) {
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;
@@ -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()));
}
}
@@ -1,6 +1,7 @@
package com.mojang.brigadier.arguments; package com.mojang.brigadier.arguments;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.testing.EqualsTester; import com.google.common.testing.EqualsTester;
import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.context.CommandContextBuilder;
@@ -11,8 +12,10 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.util.Map; import java.util.Map;
import java.util.Set;
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; 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.hasToString;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
@@ -90,6 +93,13 @@ public class IntegerArgumentTypeTest {
assertThat(IntegerArgumentType.getInteger(context, "foo"), is(100)); 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 @Test
public void testEquals() throws Exception { public void testEquals() throws Exception {
new EqualsTester() new EqualsTester()
@@ -1,6 +1,7 @@
package com.mojang.brigadier.tree; package com.mojang.brigadier.tree;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.testing.EqualsTester; import com.google.common.testing.EqualsTester;
import com.mojang.brigadier.Command; import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType;
@@ -11,9 +12,12 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.util.Map; import java.util.Map;
import java.util.Set;
import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; import static com.mojang.brigadier.arguments.IntegerArgumentType.integer;
import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument; 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.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@@ -36,7 +40,7 @@ public class ArgumentCommandNodeTest extends AbstractCommandNodeTest {
@Test @Test
public void testParse() throws Exception { 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().containsKey("foo"), is(true));
assertThat(contextBuilder.getArguments().get("foo").getResult(), is(123)); assertThat(contextBuilder.getArguments().get("foo").getResult(), is(123));
@@ -66,6 +70,13 @@ public class ArgumentCommandNodeTest extends AbstractCommandNodeTest {
assertThat(node.getUsageText(), is("<foo>")); assertThat(node.getUsageText(), is("<foo>"));
} }
@Test
public void testSuggestions() throws Exception {
Set<String> set = Sets.newHashSet();
node.listSuggestions("", set);
assertThat(set, is(empty()));
}
@Test @Test
public void testEquals() throws Exception { public void testEquals() throws Exception {
Command command = mock(Command.class); Command command = mock(Command.class);
@@ -1,6 +1,7 @@
package com.mojang.brigadier.tree; package com.mojang.brigadier.tree;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.testing.EqualsTester; import com.google.common.testing.EqualsTester;
import com.mojang.brigadier.Command; import com.mojang.brigadier.Command;
import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.context.CommandContextBuilder;
@@ -10,8 +11,11 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.util.Map; import java.util.Map;
import java.util.Set;
import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; 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.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
@@ -34,7 +38,7 @@ public class LiteralCommandNodeTest extends AbstractCommandNodeTest {
@Test @Test
public void testParse() throws Exception { public void testParse() throws Exception {
assertThat(node.parse("foo bar", contextBuilder), is("bar")); assertThat(node.parse("foo bar", contextBuilder), is(" bar"));
} }
@Test @Test
@@ -69,6 +73,25 @@ public class LiteralCommandNodeTest extends AbstractCommandNodeTest {
assertThat(node.getUsageText(), is("foo")); 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 @Test
public void testEquals() throws Exception { public void testEquals() throws Exception {
Command command = mock(Command.class); Command command = mock(Command.class);
@@ -1,11 +1,15 @@
package com.mojang.brigadier.tree; package com.mojang.brigadier.tree;
import com.google.common.collect.Sets;
import com.google.common.testing.EqualsTester; import com.google.common.testing.EqualsTester;
import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.context.CommandContextBuilder;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import java.util.Set;
import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat; import static org.junit.Assert.assertThat;
@@ -37,6 +41,13 @@ public class RootCommandNodeTest extends AbstractCommandNodeTest {
assertThat(node.getUsageText(), is("")); assertThat(node.getUsageText(), is(""));
} }
@Test
public void testSuggestions() throws Exception {
Set<String> set = Sets.newHashSet();
node.listSuggestions("", set);
assertThat(set, is(empty()));
}
@Test @Test
public void testEquals() throws Exception { public void testEquals() throws Exception {
new EqualsTester() new EqualsTester()