From f372eb3b989ca3866965b8cb5fc6e61f2863ec76 Mon Sep 17 00:00:00 2001 From: Nathan Adams Date: Thu, 22 Jun 2017 09:05:16 +0200 Subject: [PATCH] Added support for suggestions tabcompletions of a command input --- .../mojang/brigadier/CommandDispatcher.java | 34 ++++++++++- .../arguments/CommandArgumentType.java | 4 ++ .../arguments/IntegerArgumentType.java | 12 +++- .../brigadier/tree/ArgumentCommandNode.java | 9 ++- .../mojang/brigadier/tree/CommandNode.java | 3 + .../brigadier/tree/LiteralCommandNode.java | 13 ++++- .../brigadier/tree/RootCommandNode.java | 6 ++ .../CommandDispatcherCompletionsTest.java | 56 +++++++++++++++++++ .../arguments/IntegerArgumentTypeTest.java | 10 ++++ .../tree/ArgumentCommandNodeTest.java | 13 ++++- .../tree/LiteralCommandNodeTest.java | 25 ++++++++- .../brigadier/tree/RootCommandNodeTest.java | 11 ++++ 12 files changed, 186 insertions(+), 10 deletions(-) create mode 100644 src/test/java/com/mojang/brigadier/CommandDispatcherCompletionsTest.java diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java index e043bff..575c97c 100644 --- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java +++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java @@ -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 { } }; - 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 { 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 { return result.toString(); } + + private Set findSuggestions(CommandNode node, String command, CommandContextBuilder contextBuilder, Set result) { + for (CommandNode child : node.getChildren()) { + try { + CommandContextBuilder 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 nodes = findSuggestions(root, command, new CommandContextBuilder<>(source), Sets.newLinkedHashSet()); + + return nodes.toArray(new String[nodes.size()]); + } } diff --git a/src/main/java/com/mojang/brigadier/arguments/CommandArgumentType.java b/src/main/java/com/mojang/brigadier/arguments/CommandArgumentType.java index 5a869d7..f968a74 100644 --- a/src/main/java/com/mojang/brigadier/arguments/CommandArgumentType.java +++ b/src/main/java/com/mojang/brigadier/arguments/CommandArgumentType.java @@ -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 { ParsedArgument parse(String command) throws CommandException; + + void listSuggestions(String command, Set output); } diff --git a/src/main/java/com/mojang/brigadier/arguments/IntegerArgumentType.java b/src/main/java/com/mojang/brigadier/arguments/IntegerArgumentType.java index d597c68..43bb9f4 100644 --- a/src/main/java/com/mojang/brigadier/arguments/IntegerArgumentType.java +++ b/src/main/java/com/mojang/brigadier/arguments/IntegerArgumentType.java @@ -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 { - 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 { } } + @Override + public void listSuggestions(String command, Set output) { + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java b/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java index aea7769..b34fefc 100644 --- a/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java @@ -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 extends CommandNode { private static final String USAGE_ARGUMENT_OPEN = "<"; private static final String USAGE_ARGUMENT_CLOSE = ">"; @@ -46,12 +48,17 @@ public class ArgumentCommandNode 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 output) { + type.listSuggestions(command, output); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/mojang/brigadier/tree/CommandNode.java b/src/main/java/com/mojang/brigadier/tree/CommandNode.java index 1802bb4..be66cbe 100644 --- a/src/main/java/com/mojang/brigadier/tree/CommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/CommandNode.java @@ -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 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 output); } diff --git a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java index 1e40c0e..ac42c1a 100644 --- a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java @@ -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 output) { + if (literal.startsWith(command)) { + output.add(literal); + } + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java b/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java index f692eef..4d2e4ca 100644 --- a/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java @@ -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 output) { + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/test/java/com/mojang/brigadier/CommandDispatcherCompletionsTest.java b/src/test/java/com/mojang/brigadier/CommandDispatcherCompletionsTest.java new file mode 100644 index 0000000..7956497 --- /dev/null +++ b/src/test/java/com/mojang/brigadier/CommandDispatcherCompletionsTest.java @@ -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 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())); + } +} \ No newline at end of file diff --git a/src/test/java/com/mojang/brigadier/arguments/IntegerArgumentTypeTest.java b/src/test/java/com/mojang/brigadier/arguments/IntegerArgumentTypeTest.java index c3f3f0d..699fbd6 100644 --- a/src/test/java/com/mojang/brigadier/arguments/IntegerArgumentTypeTest.java +++ b/src/test/java/com/mojang/brigadier/arguments/IntegerArgumentTypeTest.java @@ -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 set = Sets.newHashSet(); + type.listSuggestions("", set); + assertThat(set, is(empty())); + } + @Test public void testEquals() throws Exception { new EqualsTester() diff --git a/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java b/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java index eb01037..f433529 100644 --- a/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java +++ b/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java @@ -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("")); } + @Test + public void testSuggestions() throws Exception { + Set set = Sets.newHashSet(); + node.listSuggestions("", set); + assertThat(set, is(empty())); + } + @Test public void testEquals() throws Exception { Command command = mock(Command.class); diff --git a/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java b/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java index 1ddcfdb..8005f07 100644 --- a/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java +++ b/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java @@ -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 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); diff --git a/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java b/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java index 5bb45f5..2dfa392 100644 --- a/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java +++ b/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java @@ -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 set = Sets.newHashSet(); + node.listSuggestions("", set); + assertThat(set, is(empty())); + } + @Test public void testEquals() throws Exception { new EqualsTester()