diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java index 08a70d8..ecd1cba 100644 --- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java +++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java @@ -7,7 +7,6 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContextBuilder; import com.mojang.brigadier.exceptions.CommandException; -import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType; import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; @@ -98,7 +97,11 @@ public class CommandDispatcher { throw ERROR_EXPECTED_ARGUMENT_SEPARATOR.createWithContext(reader); } reader.skip(); - return parseNodes(child, reader, context); + if (child.getRedirect() != null) { + return parseNodes(child.getRedirect(), reader, context.redirect()); + } else { + return parseNodes(child, reader, context); + } } else { return new ParseResults<>(context); } @@ -122,7 +125,9 @@ public class CommandDispatcher { result.add(prefix); } - if (!node.getChildren().isEmpty()) { + if (node.getRedirect() != null) { + result.add(prefix.isEmpty() ? node.getUsageText() + ARGUMENT_SEPARATOR + "..." : prefix + ARGUMENT_SEPARATOR + "..."); + } else if (!node.getChildren().isEmpty()) { for (final CommandNode child : node.getChildren()) { getAllUsage(child, source, result, prefix.isEmpty() ? child.getUsageText() : prefix + ARGUMENT_SEPARATOR + child.getUsageText()); } @@ -153,36 +158,40 @@ public class CommandDispatcher { final String close = childOptional ? USAGE_OPTIONAL_CLOSE : USAGE_REQUIRED_CLOSE; if (!deep) { - final Collection> children = node.getChildren().stream().filter(c -> c.canUse(source)).collect(Collectors.toList()); - if (children.size() == 1) { - final String usage = getSmartUsage(children.iterator().next(), source, childOptional, childOptional); - if (usage != null) { - return self + ARGUMENT_SEPARATOR + usage; - } - } else if (children.size() > 1) { - final Set childUsage = Sets.newLinkedHashSet(); - for (final CommandNode child : children) { - final String usage = getSmartUsage(child, source, childOptional, true); + if (node.getRedirect() != null) { + return self + ARGUMENT_SEPARATOR + "..."; + } else { + final Collection> children = node.getChildren().stream().filter(c -> c.canUse(source)).collect(Collectors.toList()); + if (children.size() == 1) { + final String usage = getSmartUsage(children.iterator().next(), source, childOptional, childOptional); if (usage != null) { - childUsage.add(usage); + return self + ARGUMENT_SEPARATOR + usage; } - } - if (childUsage.size() == 1) { - final String usage = childUsage.iterator().next(); - return self + ARGUMENT_SEPARATOR + (childOptional ? USAGE_OPTIONAL_OPEN + usage + USAGE_OPTIONAL_CLOSE : usage); - } else if (childUsage.size() > 1) { - final StringBuilder builder = new StringBuilder(open); - int count = 0; + } else if (children.size() > 1) { + final Set childUsage = Sets.newLinkedHashSet(); for (final CommandNode child : children) { - if (count > 0) { - builder.append(USAGE_OR); + final String usage = getSmartUsage(child, source, childOptional, true); + if (usage != null) { + childUsage.add(usage); } - builder.append(child.getUsageText()); - count++; } - if (count > 0) { - builder.append(close); - return self + ARGUMENT_SEPARATOR + builder.toString(); + if (childUsage.size() == 1) { + final String usage = childUsage.iterator().next(); + return self + ARGUMENT_SEPARATOR + (childOptional ? USAGE_OPTIONAL_OPEN + usage + USAGE_OPTIONAL_CLOSE : usage); + } else if (childUsage.size() > 1) { + final StringBuilder builder = new StringBuilder(open); + int count = 0; + for (final CommandNode child : children) { + if (count > 0) { + builder.append(USAGE_OR); + } + builder.append(child.getUsageText()); + count++; + } + if (count > 0) { + builder.append(close); + return self + ARGUMENT_SEPARATOR + builder.toString(); + } } } } @@ -192,6 +201,9 @@ public class CommandDispatcher { } private Set findSuggestions(final CommandNode node, final StringReader reader, final CommandContextBuilder contextBuilder, final Set result) { + if (node.getRedirect() != null) { + return findSuggestions(node.getRedirect(), reader, contextBuilder, result); + } final S source = contextBuilder.getSource(); for (final CommandNode child : node.getChildren()) { if (!child.canUse(source)) { diff --git a/src/main/java/com/mojang/brigadier/builder/ArgumentBuilder.java b/src/main/java/com/mojang/brigadier/builder/ArgumentBuilder.java index 152727a..0c67737 100644 --- a/src/main/java/com/mojang/brigadier/builder/ArgumentBuilder.java +++ b/src/main/java/com/mojang/brigadier/builder/ArgumentBuilder.java @@ -11,10 +11,14 @@ public abstract class ArgumentBuilder> { private final RootCommandNode arguments = new RootCommandNode<>(); private Command command; private Predicate requirement = s -> true; + private CommandNode target; protected abstract T getThis(); public T then(final ArgumentBuilder argument) { + if (target != null) { + throw new IllegalStateException("Cannot add children to a redirected node"); + } arguments.addChild(argument.build()); return getThis(); } @@ -41,5 +45,17 @@ public abstract class ArgumentBuilder> { return requirement; } + public T redirect(final CommandNode target) { + if (!arguments.getChildren().isEmpty()) { + throw new IllegalStateException("Cannot redirect a node with children"); + } + this.target = target; + return getThis(); + } + + public CommandNode getRedirect() { + return target; + } + public abstract CommandNode build(); } diff --git a/src/main/java/com/mojang/brigadier/builder/LiteralArgumentBuilder.java b/src/main/java/com/mojang/brigadier/builder/LiteralArgumentBuilder.java index 90a7c27..8d14d46 100644 --- a/src/main/java/com/mojang/brigadier/builder/LiteralArgumentBuilder.java +++ b/src/main/java/com/mojang/brigadier/builder/LiteralArgumentBuilder.java @@ -25,7 +25,7 @@ public class LiteralArgumentBuilder extends ArgumentBuilder build() { - final LiteralCommandNode result = new LiteralCommandNode<>(getLiteral(), getCommand(), getRequirement()); + final LiteralCommandNode result = new LiteralCommandNode<>(getLiteral(), getCommand(), getRequirement(), getRedirect()); for (final CommandNode argument : getArguments()) { result.addChild(argument); diff --git a/src/main/java/com/mojang/brigadier/builder/RequiredArgumentBuilder.java b/src/main/java/com/mojang/brigadier/builder/RequiredArgumentBuilder.java index 742764e..c9cb585 100644 --- a/src/main/java/com/mojang/brigadier/builder/RequiredArgumentBuilder.java +++ b/src/main/java/com/mojang/brigadier/builder/RequiredArgumentBuilder.java @@ -31,7 +31,7 @@ public class RequiredArgumentBuilder extends ArgumentBuilder build() { - final ArgumentCommandNode result = new ArgumentCommandNode<>(getName(), getType(), getCommand(), getRequirement()); + final ArgumentCommandNode result = new ArgumentCommandNode<>(getName(), getType(), getCommand(), getRequirement(), getRedirect()); for (final CommandNode argument : getArguments()) { result.addChild(argument); diff --git a/src/main/java/com/mojang/brigadier/context/CommandContext.java b/src/main/java/com/mojang/brigadier/context/CommandContext.java index dbaa51b..c91dc46 100644 --- a/src/main/java/com/mojang/brigadier/context/CommandContext.java +++ b/src/main/java/com/mojang/brigadier/context/CommandContext.java @@ -1,7 +1,6 @@ package com.mojang.brigadier.context; import com.google.common.collect.Iterables; -import com.google.common.collect.Maps; import com.google.common.primitives.Primitives; import com.mojang.brigadier.Command; import com.mojang.brigadier.tree.CommandNode; @@ -14,13 +13,19 @@ public class CommandContext { private final Map> arguments; private final Map, String> nodes; private final String input; + private final CommandContext parent; - public CommandContext(final S source, final Map> arguments, final Command command, final Map, String> nodes, final String input) { + public CommandContext(final S source, final Map> arguments, final Command command, final Map, String> nodes, final String input, final CommandContext parent) { this.source = source; this.arguments = arguments; this.command = command; this.nodes = nodes; this.input = input; + this.parent = parent; + } + + public CommandContext getParent() { + return parent; } public Command getCommand() { @@ -58,6 +63,7 @@ public class CommandContext { if (!Iterables.elementsEqual(nodes.entrySet(), that.nodes.entrySet())) return false; if (command != null ? !command.equals(that.command) : that.command != null) return false; if (!source.equals(that.source)) return false; + if (parent != null ? !parent.equals(that.parent) : that.parent != null) return false; return true; } @@ -68,6 +74,7 @@ public class CommandContext { result = 31 * result + arguments.hashCode(); result = 31 * result + (command != null ? command.hashCode() : 0); result = 31 * result + nodes.hashCode(); + result = 31 * result + (parent != null ? parent.hashCode() : 0); return result; } diff --git a/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java b/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java index 989fee6..065b5c5 100644 --- a/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java +++ b/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java @@ -14,6 +14,7 @@ public class CommandContextBuilder { private final StringBuilder input = new StringBuilder(); private S source; private Command command; + private CommandContext parent; public CommandContextBuilder(final CommandDispatcher dispatcher, final S source) { this.dispatcher = dispatcher; @@ -54,13 +55,24 @@ public class CommandContextBuilder { public CommandContextBuilder copy() { final CommandContextBuilder copy = new CommandContextBuilder<>(dispatcher, source); - copy.command = this.command; + copy.command = command; copy.arguments.putAll(arguments); - copy.nodes.putAll(this.nodes); + copy.nodes.putAll(nodes); copy.input.append(input); + copy.parent = parent; return copy; } + public CommandContextBuilder redirect() { + final CommandContextBuilder result = new CommandContextBuilder<>(dispatcher, source); + result.parent = build(); + return result; + } + + public CommandContext getParent() { + return parent; + } + public String getInput() { return input.toString(); } @@ -70,7 +82,7 @@ public class CommandContextBuilder { } public CommandContext build() { - return new CommandContext<>(source, arguments, command, nodes, input.toString()); + return new CommandContext<>(source, arguments, command, nodes, input.toString(), parent); } public CommandDispatcher getDispatcher() { diff --git a/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java b/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java index 7f1589c..4d7c3c2 100644 --- a/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/ArgumentCommandNode.java @@ -18,8 +18,8 @@ public class ArgumentCommandNode extends CommandNode { private final String name; private final ArgumentType type; - public ArgumentCommandNode(final String name, final ArgumentType type, final Command command, final Predicate requirement) { - super(command, requirement); + public ArgumentCommandNode(final String name, final ArgumentType type, final Command command, final Predicate requirement, final CommandNode redirect) { + super(command, requirement, redirect); this.name = name; this.type = type; } @@ -69,6 +69,7 @@ public class ArgumentCommandNode extends CommandNode { public RequiredArgumentBuilder createBuilder() { final RequiredArgumentBuilder builder = RequiredArgumentBuilder.argument(name, type); builder.requires(getRequirement()); + builder.redirect(getRedirect()); if (getCommand() != null) { builder.executes(getCommand()); } diff --git a/src/main/java/com/mojang/brigadier/tree/CommandNode.java b/src/main/java/com/mojang/brigadier/tree/CommandNode.java index dbe401c..db4f34c 100644 --- a/src/main/java/com/mojang/brigadier/tree/CommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/CommandNode.java @@ -18,11 +18,13 @@ import java.util.stream.Collectors; public abstract class CommandNode implements Comparable> { private Map> children = Maps.newLinkedHashMap(); private final Predicate requirement; + private final CommandNode redirect; private Command command; - protected CommandNode(final Command command, final Predicate requirement) { + protected CommandNode(final Command command, final Predicate requirement, final CommandNode redirect) { this.command = command; this.requirement = requirement; + this.redirect = redirect; } public Command getCommand() { @@ -33,6 +35,10 @@ public abstract class CommandNode implements Comparable> { return children.values(); } + public CommandNode getRedirect() { + return redirect; + } + public boolean canUse(final S source) { return requirement.test(source); } diff --git a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java index 625aa99..1bddb83 100644 --- a/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/LiteralCommandNode.java @@ -15,8 +15,8 @@ public class LiteralCommandNode extends CommandNode { private final String literal; - public LiteralCommandNode(final String literal, final Command command, final Predicate requirement) { - super(command, requirement); + public LiteralCommandNode(final String literal, final Command command, final Predicate requirement, final CommandNode redirect) { + super(command, requirement, redirect); this.literal = literal; } @@ -78,6 +78,7 @@ public class LiteralCommandNode extends CommandNode { public LiteralArgumentBuilder createBuilder() { final LiteralArgumentBuilder builder = LiteralArgumentBuilder.literal(this.literal); builder.requires(getRequirement()); + builder.redirect(getRedirect()); if (getCommand() != null) { builder.executes(getCommand()); } diff --git a/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java b/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java index fc7c201..d90e4e3 100644 --- a/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java +++ b/src/main/java/com/mojang/brigadier/tree/RootCommandNode.java @@ -9,7 +9,7 @@ import java.util.Set; public class RootCommandNode extends CommandNode { public RootCommandNode() { - super(null, c -> true); + super(null, c -> true, null); } @Override diff --git a/src/test/java/com/mojang/brigadier/CommandDispatcherCompletionsTest.java b/src/test/java/com/mojang/brigadier/CommandDispatcherCompletionsTest.java index da10f35..a69d5bb 100644 --- a/src/test/java/com/mojang/brigadier/CommandDispatcherCompletionsTest.java +++ b/src/test/java/com/mojang/brigadier/CommandDispatcherCompletionsTest.java @@ -39,6 +39,14 @@ public class CommandDispatcherCompletionsTest { assertThat(subject.getCompletionSuggestions("q", source), is(emptyArray())); } + @Test + public void testCommand_redirect() throws Exception { + subject.register(literal("foo")); + subject.register(literal("bar")); + subject.register(literal("redirect").redirect(subject.getRoot())); + assertThat(subject.getCompletionSuggestions("redirect ", source), equalTo(new String[]{"bar", "foo", "redirect"})); + } + @Test public void testSubCommand() throws Exception { subject.register(literal("foo").then(literal("abc")).then(literal("def")).then(literal("ghi").requires(s -> false))); diff --git a/src/test/java/com/mojang/brigadier/CommandDispatcherTest.java b/src/test/java/com/mojang/brigadier/CommandDispatcherTest.java index 693fdc1..df2b29b 100644 --- a/src/test/java/com/mojang/brigadier/CommandDispatcherTest.java +++ b/src/test/java/com/mojang/brigadier/CommandDispatcherTest.java @@ -14,9 +14,12 @@ import java.util.Collections; import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; +import static org.mockito.Matchers.notNull; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -164,6 +167,30 @@ public class CommandDispatcherTest { verify(subCommand).run(any(CommandContext.class)); } + @SuppressWarnings("unchecked") + @Test + public void testExecuteRedirected() throws Exception { + subject.register(literal("actual").executes(command)); + subject.register(literal("redirected").redirect(subject.getRoot())); + + final ParseResults parse = subject.parse("redirected redirected actual", source); + assertThat(parse.getContext().getInput(), equalTo("actual")); + assertThat(parse.getContext().getNodes().size(), is(1)); + + final CommandContext parent1 = parse.getContext().getParent(); + assertThat(parent1, is(notNullValue())); + assertThat(parent1.getInput(), equalTo("redirected")); + assertThat(parent1.getNodes().size(), is(1)); + + final CommandContext parent2 = parent1.getParent(); + assertThat(parent2, is(notNullValue())); + assertThat(parent2.getInput(), equalTo("redirected")); + assertThat(parent2.getNodes().size(), is(1)); + + assertThat(subject.execute(parse), is(42)); + verify(command).run(any(CommandContext.class)); + } + @Test public void testExecuteOrphanedSubcommand() throws Exception { subject.register(literal("foo").then( diff --git a/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java b/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java index c549b00..3023a78 100644 --- a/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java +++ b/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java @@ -87,6 +87,10 @@ public class CommandDispatcherUsagesTest { .then(literal("1").executes(command)) .then(literal("2").executes(command)) ); + subject.register( + literal("j") + .redirect(subject.getRoot()) + ); } private CommandNode get(final String command) { @@ -136,6 +140,7 @@ public class CommandDispatcherUsagesTest { "i", "i 1", "i 2", + "j ...", })); } @@ -151,6 +156,7 @@ public class CommandDispatcherUsagesTest { .put(get("g"), "g [1]") .put(get("h"), "h [1|2|3]") .put(get("i"), "i [1|2]") + .put(get("j"), "j ...") .build() )); } diff --git a/src/test/java/com/mojang/brigadier/builder/ArgumentBuilderTest.java b/src/test/java/com/mojang/brigadier/builder/ArgumentBuilderTest.java index 6063ea8..779a56c 100644 --- a/src/test/java/com/mojang/brigadier/builder/ArgumentBuilderTest.java +++ b/src/test/java/com/mojang/brigadier/builder/ArgumentBuilderTest.java @@ -3,12 +3,16 @@ package com.mojang.brigadier.builder; import com.mojang.brigadier.tree.CommandNode; import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; +import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; import static com.mojang.brigadier.builder.RequiredArgumentBuilder.argument; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; import static org.hamcrest.core.IsCollectionContaining.hasItem; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; public class ArgumentBuilderTest { private TestableArgumentBuilder builder; @@ -28,6 +32,27 @@ public class ArgumentBuilderTest { assertThat(builder.getArguments(), hasItem((CommandNode) argument.build())); } + @Test + public void testRedirect() throws Exception { + final CommandNode target = mock(CommandNode.class); + builder.redirect(target); + assertThat(builder.getRedirect(), is(target)); + } + + @Test(expected = IllegalStateException.class) + public void testRedirect_withChild() throws Exception { + final CommandNode target = mock(CommandNode.class); + builder.then(literal("foo")); + builder.redirect(target); + } + + @Test(expected = IllegalStateException.class) + public void testThen_withRedirect() throws Exception { + final CommandNode target = mock(CommandNode.class); + builder.redirect(target); + builder.then(literal("foo")); + } + private static class TestableArgumentBuilder extends ArgumentBuilder> { @Override protected TestableArgumentBuilder getThis() {