From 107b852c74e80d5b8f504d9bef6a09c2500b0857 Mon Sep 17 00:00:00 2001 From: boq Date: Mon, 30 Jul 2018 10:41:31 +0200 Subject: [PATCH] Implement calculation of suggestions for any text position --- .../mojang/brigadier/CommandDispatcher.java | 78 +++++++------------ .../com/mojang/brigadier/ParseResults.java | 10 +-- .../brigadier/context/CommandContext.java | 23 ++++-- .../context/CommandContextBuilder.java | 52 +++++++++++-- .../brigadier/context/ParsedCommandNode.java | 47 +++++++++++ .../brigadier/context/SuggestionContext.java | 16 ++++ .../brigadier/CommandDispatcherTest.java | 36 ++++++--- .../CommandDispatcherUsagesTest.java | 13 ++-- .../brigadier/CommandSuggestionsTest.java | 70 ++++++++++++++++- .../brigadier/context/CommandContextTest.java | 29 ++++--- .../tree/ArgumentCommandNodeTest.java | 2 +- .../tree/LiteralCommandNodeTest.java | 2 +- .../brigadier/tree/RootCommandNodeTest.java | 2 +- 13 files changed, 277 insertions(+), 103 deletions(-) create mode 100644 src/main/java/com/mojang/brigadier/context/ParsedCommandNode.java create mode 100644 src/main/java/com/mojang/brigadier/context/SuggestionContext.java diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java index 78d8b5b..4883db9 100644 --- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java +++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java @@ -3,14 +3,13 @@ package com.mojang.brigadier; -import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; import com.mojang.brigadier.context.CommandContextBuilder; -import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.context.SuggestionContext; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; @@ -57,6 +56,7 @@ public class CommandDispatcher { private static final String USAGE_OR = "|"; private final RootCommandNode root; + private final Predicate> hasCommand = new Predicate>() { @Override public boolean test(final CommandNode input) { @@ -231,7 +231,7 @@ public class CommandDispatcher { final CommandContext child = context.getChild(); if (child != null) { forked |= context.isForked(); - if (!child.getNodes().isEmpty()) { + if (child.hasNodes()) { foundCommand = true; final RedirectModifier modifier = context.getRedirectModifier(); if (modifier == null) { @@ -345,24 +345,14 @@ public class CommandDispatcher { * @see #execute(String, Object) */ public ParseResults parse(final StringReader command, final S source) { - final CommandContextBuilder context = new CommandContextBuilder<>(this, source, 0); + final CommandContextBuilder context = new CommandContextBuilder<>(this, source, root, command.getCursor()); return parseNodes(root, command, context); } - private static class PartialParse { - public final CommandContextBuilder context; - public final ParseResults parse; - - private PartialParse(final CommandContextBuilder context, final ParseResults parse) { - this.context = context; - this.parse = parse; - } - } - private ParseResults parseNodes(final CommandNode node, final StringReader originalReader, final CommandContextBuilder contextSoFar) { final S source = contextSoFar.getSource(); Map, CommandSyntaxException> errors = null; - List> potentials = null; + List> potentials = null; final int cursor = originalReader.getCursor(); for (final CommandNode child : node.getRelevantNodes(originalReader)) { @@ -395,48 +385,47 @@ public class CommandDispatcher { if (reader.canRead(child.getRedirect() == null ? 2 : 1)) { reader.skip(); if (child.getRedirect() != null) { - final CommandContextBuilder childContext = new CommandContextBuilder<>(this, source, reader.getCursor()); - childContext.withNode(child.getRedirect(), StringRange.between(cursor, reader.getCursor() - 1)); + final CommandContextBuilder childContext = new CommandContextBuilder<>(this, source, child.getRedirect(), reader.getCursor()); final ParseResults parse = parseNodes(child.getRedirect(), reader, childContext); context.withChild(parse.getContext()); - return new ParseResults<>(context, originalReader.getCursor(), parse.getReader(), parse.getExceptions()); + return new ParseResults<>(context, parse.getReader(), parse.getExceptions()); } else { final ParseResults parse = parseNodes(child, reader, context); if (potentials == null) { potentials = new ArrayList<>(1); } - potentials.add(new PartialParse<>(context, parse)); + potentials.add(parse); } } else { if (potentials == null) { potentials = new ArrayList<>(1); } - potentials.add(new PartialParse<>(context, new ParseResults<>(context, originalReader.getCursor(), reader, Collections.emptyMap()))); + potentials.add(new ParseResults<>(context, reader, Collections.emptyMap())); } } if (potentials != null) { if (potentials.size() > 1) { potentials.sort((a, b) -> { - if (!a.parse.getReader().canRead() && b.parse.getReader().canRead()) { + if (!a.getReader().canRead() && b.getReader().canRead()) { return -1; } - if (a.parse.getReader().canRead() && !b.parse.getReader().canRead()) { + if (a.getReader().canRead() && !b.getReader().canRead()) { return 1; } - if (a.parse.getExceptions().isEmpty() && !b.parse.getExceptions().isEmpty()) { + if (a.getExceptions().isEmpty() && !b.getExceptions().isEmpty()) { return -1; } - if (!a.parse.getExceptions().isEmpty() && b.parse.getExceptions().isEmpty()) { + if (!a.getExceptions().isEmpty() && b.getExceptions().isEmpty()) { return 1; } return 0; }); } - return potentials.get(0).parse; + return potentials.get(0); } - return new ParseResults<>(contextSoFar, originalReader.getCursor(), originalReader, errors == null ? Collections.emptyMap() : errors); + return new ParseResults<>(contextSoFar, originalReader, errors == null ? Collections.emptyMap() : errors); } /** @@ -589,37 +578,24 @@ public class CommandDispatcher { * @return a future that will eventually resolve into a {@link Suggestions} object */ public CompletableFuture getCompletionSuggestions(final ParseResults parse) { - final CommandContextBuilder rootContext = parse.getContext(); - final CommandContextBuilder context = rootContext.getLastChild(); - final CommandNode parent; - final int start; + return getCompletionSuggestions(parse, parse.getReader().getTotalLength()); + } - if (context.getNodes().isEmpty()) { - parent = root; - start = parse.getStartIndex(); - } else if (parse.getReader().canRead()) { - final Map.Entry, StringRange> entry = Iterables.getLast(context.getNodes().entrySet()); - parent = entry.getKey(); - start = entry.getValue().getEnd() + 1; - } else if (context.getNodes().size() > 1) { - final Map.Entry, StringRange> entry = Iterables.get(context.getNodes().entrySet(), context.getNodes().size() - 2); - parent = entry.getKey(); - start = entry.getValue().getEnd() + 1; - } else if (rootContext != context && context.getNodes().size() > 0) { - final Map.Entry, StringRange> entry = Iterables.getLast(context.getNodes().entrySet()); - parent = entry.getKey(); - start = entry.getValue().getEnd() + 1; - } else { - parent = root; - start = parse.getStartIndex(); - } + public CompletableFuture getCompletionSuggestions(final ParseResults parse, int cursor) { + final CommandContextBuilder context = parse.getContext(); + final SuggestionContext nodeBeforeCursor = context.findSuggestionContext(cursor); + final CommandNode parent = nodeBeforeCursor.parent; + final int start = Math.min(nodeBeforeCursor.startPos, cursor); + + final String fullInput = parse.getReader().getString(); + final String truncatedInput = fullInput.substring(0, cursor); @SuppressWarnings("unchecked") final CompletableFuture[] futures = new CompletableFuture[parent.getChildren().size()]; int i = 0; for (final CommandNode node : parent.getChildren()) { CompletableFuture future = Suggestions.empty(); try { - future = node.listSuggestions(context.build(parse.getReader().getString()), new SuggestionsBuilder(parse.getReader().getString(), start)); + future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, start)); } catch (final CommandSyntaxException ignored) { } futures[i++] = future; @@ -631,7 +607,7 @@ public class CommandDispatcher { for (final CompletableFuture future : futures) { suggestions.add(future.join()); } - result.complete(Suggestions.merge(parse.getReader().getString(), suggestions)); + result.complete(Suggestions.merge(fullInput, suggestions)); }); return result; diff --git a/src/main/java/com/mojang/brigadier/ParseResults.java b/src/main/java/com/mojang/brigadier/ParseResults.java index 61c1c73..1c1bc46 100644 --- a/src/main/java/com/mojang/brigadier/ParseResults.java +++ b/src/main/java/com/mojang/brigadier/ParseResults.java @@ -13,22 +13,16 @@ import java.util.Map; public class ParseResults { private final CommandContextBuilder context; private final Map, CommandSyntaxException> exceptions; - private final int startIndex; private final ImmutableStringReader reader; - public ParseResults(final CommandContextBuilder context, final int startIndex, final ImmutableStringReader reader, final Map, CommandSyntaxException> exceptions) { + public ParseResults(final CommandContextBuilder context, final ImmutableStringReader reader, final Map, CommandSyntaxException> exceptions) { this.context = context; - this.startIndex = startIndex; this.reader = reader; this.exceptions = exceptions; } public ParseResults(final CommandContextBuilder context) { - this(context, 0, new StringReader(""), Collections.emptyMap()); - } - - public int getStartIndex() { - return startIndex; + this(context, new StringReader(""), Collections.emptyMap()); } public CommandContextBuilder getContext() { diff --git a/src/main/java/com/mojang/brigadier/context/CommandContext.java b/src/main/java/com/mojang/brigadier/context/CommandContext.java index 1d62b76..73924db 100644 --- a/src/main/java/com/mojang/brigadier/context/CommandContext.java +++ b/src/main/java/com/mojang/brigadier/context/CommandContext.java @@ -9,6 +9,7 @@ import com.mojang.brigadier.Command; import com.mojang.brigadier.RedirectModifier; import com.mojang.brigadier.tree.CommandNode; +import java.util.List; import java.util.Map; public class CommandContext { @@ -16,17 +17,19 @@ public class CommandContext { private final String input; private final Command command; private final Map> arguments; - private final Map, StringRange> nodes; + private final CommandNode rootNode; + private final List> nodes; private final StringRange range; private final CommandContext child; private final RedirectModifier modifier; private final boolean forks; - public CommandContext(final S source, final String input, final Map> arguments, final Command command, final Map, StringRange> nodes, final StringRange range, final CommandContext child, final RedirectModifier modifier, boolean forks) { + public CommandContext(final S source, final String input, final Map> arguments, final Command command, final CommandNode rootNode, final List> nodes, final StringRange range, final CommandContext child, final RedirectModifier modifier, boolean forks) { this.source = source; this.input = input; this.arguments = arguments; this.command = command; + this.rootNode = rootNode; this.nodes = nodes; this.range = range; this.child = child; @@ -38,7 +41,7 @@ public class CommandContext { if (this.source == source) { return this; } - return new CommandContext<>(source, input, arguments, command, nodes, range, child, modifier, forks); + return new CommandContext<>(source, input, arguments, command, rootNode, nodes, range, child, modifier, forks); } public CommandContext getChild() { @@ -85,7 +88,8 @@ public class CommandContext { final CommandContext that = (CommandContext) o; if (!arguments.equals(that.arguments)) return false; - if (!Iterables.elementsEqual(nodes.entrySet(), that.nodes.entrySet())) return false; + if (!rootNode.equals(that.rootNode)) return false; + if (!Iterables.elementsEqual(nodes, that.nodes)) return false; if (command != null ? !command.equals(that.command) : that.command != null) return false; if (!source.equals(that.source)) return false; if (child != null ? !child.equals(that.child) : that.child != null) return false; @@ -98,6 +102,7 @@ public class CommandContext { int result = source.hashCode(); result = 31 * result + arguments.hashCode(); result = 31 * result + (command != null ? command.hashCode() : 0); + result = 31 * result + rootNode.hashCode(); result = 31 * result + nodes.hashCode(); result = 31 * result + (child != null ? child.hashCode() : 0); return result; @@ -115,10 +120,18 @@ public class CommandContext { return input; } - public Map, StringRange> getNodes() { + public CommandNode getRootNode() { + return rootNode; + } + + public List> getNodes() { return nodes; } + public boolean hasNodes() { + return !nodes.isEmpty(); + } + public boolean isForked() { return forks; } diff --git a/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java b/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java index 1cc1b91..e9ef918 100644 --- a/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java +++ b/src/main/java/com/mojang/brigadier/context/CommandContextBuilder.java @@ -3,17 +3,21 @@ package com.mojang.brigadier.context; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.RedirectModifier; import com.mojang.brigadier.tree.CommandNode; +import java.util.List; import java.util.Map; public class CommandContextBuilder { private final Map> arguments = Maps.newLinkedHashMap(); - private final Map, StringRange> nodes = Maps.newLinkedHashMap(); + private final CommandNode rootNode; + private final List> nodes = Lists.newArrayList(); private final CommandDispatcher dispatcher; private S source; private Command command; @@ -22,7 +26,8 @@ public class CommandContextBuilder { private RedirectModifier modifier = null; private boolean forks; - public CommandContextBuilder(final CommandDispatcher dispatcher, final S source, final int start) { + public CommandContextBuilder(final CommandDispatcher dispatcher, final S source, final CommandNode rootNode, final int start) { + this.rootNode = rootNode; this.dispatcher = dispatcher; this.source = source; this.range = StringRange.at(start); @@ -37,6 +42,10 @@ public class CommandContextBuilder { return source; } + public CommandNode getRootNode() { + return rootNode; + } + public CommandContextBuilder withArgument(final String name, final ParsedArgument argument) { this.arguments.put(name, argument); return this; @@ -52,7 +61,7 @@ public class CommandContextBuilder { } public CommandContextBuilder withNode(final CommandNode node, final StringRange range) { - nodes.put(node, range); + nodes.add(new ParsedCommandNode<>(node, range)); this.range = StringRange.encompassing(this.range, range); this.modifier = node.getRedirectModifier(); this.forks = node.isFork(); @@ -60,10 +69,10 @@ public class CommandContextBuilder { } public CommandContextBuilder copy() { - final CommandContextBuilder copy = new CommandContextBuilder<>(dispatcher, source, range.getStart()); + final CommandContextBuilder copy = new CommandContextBuilder<>(dispatcher, source, rootNode, range.getStart()); copy.command = command; copy.arguments.putAll(arguments); - copy.nodes.putAll(nodes); + copy.nodes.addAll(nodes); copy.child = child; copy.range = range; copy.forks = forks; @@ -91,12 +100,12 @@ public class CommandContextBuilder { return command; } - public Map, StringRange> getNodes() { + public List> getNodes() { return nodes; } public CommandContext build(final String input) { - return new CommandContext<>(source, input, arguments, command, nodes, range, child == null ? null : child.build(input), modifier, forks); + return new CommandContext<>(source, input, arguments, command, rootNode, nodes, range, child == null ? null : child.build(input), modifier, forks); } public CommandDispatcher getDispatcher() { @@ -106,4 +115,33 @@ public class CommandContextBuilder { public StringRange getRange() { return range; } + + public SuggestionContext findSuggestionContext(final int cursor) { + if (range.getStart() <= cursor) { + if (range.getEnd() < cursor) { + if (child != null) { + return child.findSuggestionContext(cursor); + } else if (!nodes.isEmpty()) { + final ParsedCommandNode last = Iterables.getLast(nodes); + return new SuggestionContext<>(last.getNode(), last.getRange().getEnd() + 1); + } else { + return new SuggestionContext<>(rootNode, range.getStart()); + } + } else { + CommandNode prev = rootNode; + for (final ParsedCommandNode node : nodes) { + final StringRange nodeRange = node.getRange(); + if (nodeRange.getStart() <= cursor && cursor <= nodeRange.getEnd()) { + return new SuggestionContext<>(prev, nodeRange.getStart()); + } + prev = node.getNode(); + } + if (prev == null) { + throw new IllegalStateException("Can't find node before cursor"); + } + return new SuggestionContext<>(prev, range.getStart()); + } + } + throw new IllegalStateException("Can't find node before cursor"); + } } diff --git a/src/main/java/com/mojang/brigadier/context/ParsedCommandNode.java b/src/main/java/com/mojang/brigadier/context/ParsedCommandNode.java new file mode 100644 index 0000000..260a753 --- /dev/null +++ b/src/main/java/com/mojang/brigadier/context/ParsedCommandNode.java @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +package com.mojang.brigadier.context; + +import com.mojang.brigadier.tree.CommandNode; + +import java.util.Objects; + +public class ParsedCommandNode { + + private final CommandNode node; + + private final StringRange range; + + public ParsedCommandNode(CommandNode node, StringRange range) { + this.node = node; + this.range = range; + } + + public CommandNode getNode() { + return node; + } + + public StringRange getRange() { + return range; + } + + @Override + public String toString() { + return node + "@" + range; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ParsedCommandNode that = (ParsedCommandNode) o; + return Objects.equals(node, that.node) && + Objects.equals(range, that.range); + } + + @Override + public int hashCode() { + return Objects.hash(node, range); + } +} diff --git a/src/main/java/com/mojang/brigadier/context/SuggestionContext.java b/src/main/java/com/mojang/brigadier/context/SuggestionContext.java new file mode 100644 index 0000000..2d43f69 --- /dev/null +++ b/src/main/java/com/mojang/brigadier/context/SuggestionContext.java @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +package com.mojang.brigadier.context; + +import com.mojang.brigadier.tree.CommandNode; + +public class SuggestionContext { + public final CommandNode parent; + public final int startPos; + + public SuggestionContext(CommandNode parent, int startPos) { + this.parent = parent; + this.startPos = startPos; + } +} diff --git a/src/test/java/com/mojang/brigadier/CommandDispatcherTest.java b/src/test/java/com/mojang/brigadier/CommandDispatcherTest.java index 9ec4a53..f2bab4a 100644 --- a/src/test/java/com/mojang/brigadier/CommandDispatcherTest.java +++ b/src/test/java/com/mojang/brigadier/CommandDispatcherTest.java @@ -259,23 +259,33 @@ public class CommandDispatcherTest { @SuppressWarnings("unchecked") @Test public void testExecuteRedirectedMultipleTimes() throws Exception { - subject.register(literal("actual").executes(command)); - subject.register(literal("redirected").redirect(subject.getRoot())); + final LiteralCommandNode concreteNode = subject.register(literal("actual").executes(command)); + final LiteralCommandNode redirectNode = subject.register(literal("redirected").redirect(subject.getRoot())); final String input = "redirected redirected actual"; + final ParseResults parse = subject.parse(input, source); assertThat(parse.getContext().getRange().get(input), equalTo("redirected")); assertThat(parse.getContext().getNodes().size(), is(1)); + assertThat(parse.getContext().getRootNode(), is(subject.getRoot())); + assertThat(parse.getContext().getNodes().get(0).getRange(), equalTo(parse.getContext().getRange())); + assertThat(parse.getContext().getNodes().get(0).getNode(), is(redirectNode)); final CommandContextBuilder child1 = parse.getContext().getChild(); assertThat(child1, is(notNullValue())); - assertThat(child1.getRange().get(input), equalTo("redirected redirected")); - assertThat(child1.getNodes().size(), is(2)); + assertThat(child1.getRange().get(input), equalTo("redirected")); + assertThat(child1.getNodes().size(), is(1)); + assertThat(child1.getRootNode(), is(subject.getRoot())); + assertThat(child1.getNodes().get(0).getRange(), equalTo(child1.getRange())); + assertThat(child1.getNodes().get(0).getNode(), is(redirectNode)); final CommandContextBuilder child2 = child1.getChild(); assertThat(child2, is(notNullValue())); - assertThat(child2.getRange().get(input), equalTo("redirected actual")); - assertThat(child2.getNodes().size(), is(2)); + assertThat(child2.getRange().get(input), equalTo("actual")); + assertThat(child2.getNodes().size(), is(1)); + assertThat(child2.getRootNode(), is(subject.getRoot())); + assertThat(child2.getNodes().get(0).getRange(), equalTo(child2.getRange())); + assertThat(child2.getNodes().get(0).getNode(), is(concreteNode)); assertThat(subject.execute(parse), is(42)); verify(command).run(any(CommandContext.class)); @@ -290,19 +300,25 @@ public class CommandDispatcherTest { when(modifier.apply(argThat(hasProperty("source", is(source))))).thenReturn(Lists.newArrayList(source1, source2)); - subject.register(literal("actual").executes(command)); - subject.register(literal("redirected").fork(subject.getRoot(), modifier)); + final LiteralCommandNode concreteNode = subject.register(literal("actual").executes(command)); + final LiteralCommandNode redirectNode = subject.register(literal("redirected").fork(subject.getRoot(), modifier)); final String input = "redirected actual"; final ParseResults parse = subject.parse(input, source); assertThat(parse.getContext().getRange().get(input), equalTo("redirected")); assertThat(parse.getContext().getNodes().size(), is(1)); + assertThat(parse.getContext().getRootNode(), equalTo(subject.getRoot())); + assertThat(parse.getContext().getNodes().get(0).getRange(), equalTo(parse.getContext().getRange())); + assertThat(parse.getContext().getNodes().get(0).getNode(), is(redirectNode)); assertThat(parse.getContext().getSource(), is(source)); final CommandContextBuilder parent = parse.getContext().getChild(); assertThat(parent, is(notNullValue())); - assertThat(parent.getRange().get(input), equalTo("redirected actual")); - assertThat(parent.getNodes().size(), is(2)); + assertThat(parent.getRange().get(input), equalTo("actual")); + assertThat(parent.getNodes().size(), is(1)); + assertThat(parse.getContext().getRootNode(), equalTo(subject.getRoot())); + assertThat(parent.getNodes().get(0).getRange(), equalTo(parent.getRange())); + assertThat(parent.getNodes().get(0).getNode(), is(concreteNode)); assertThat(parent.getSource(), is(source)); assertThat(subject.execute(parse), is(2)); diff --git a/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java b/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java index 837f7b6..eef96b3 100644 --- a/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java +++ b/src/test/java/com/mojang/brigadier/CommandDispatcherUsagesTest.java @@ -4,6 +4,7 @@ package com.mojang.brigadier; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; import com.google.common.collect.Iterators; import com.mojang.brigadier.exceptions.CommandSyntaxException; import com.mojang.brigadier.tree.CommandNode; @@ -102,11 +103,11 @@ public class CommandDispatcherUsagesTest { } private CommandNode get(final String command) { - return Iterators.getLast(subject.parse(command, source).getContext().getNodes().keySet().iterator()); + return Iterables.getLast(subject.parse(command, source).getContext().getNodes()).getNode(); } private CommandNode get(final StringReader command) { - return Iterators.getLast(subject.parse(command, source).getContext().getNodes().keySet().iterator()); + return Iterables.getLast(subject.parse(command, source).getContext().getNodes()).getNode(); } @Test @@ -189,10 +190,10 @@ public class CommandDispatcherUsagesTest { final Map, String> results = subject.getSmartUsage(get(offsetH), source); assertThat(results, equalTo(ImmutableMap.builder() - .put(get("h 1"), "[1] i") - .put(get("h 2"), "[2] i ii") - .put(get("h 3"), "[3]") - .build() + .put(get("h 1"), "[1] i") + .put(get("h 2"), "[2] i ii") + .put(get("h 3"), "[3]") + .build() )); } } \ No newline at end of file diff --git a/src/test/java/com/mojang/brigadier/CommandSuggestionsTest.java b/src/test/java/com/mojang/brigadier/CommandSuggestionsTest.java index f0fe663..80018f4 100644 --- a/src/test/java/com/mojang/brigadier/CommandSuggestionsTest.java +++ b/src/test/java/com/mojang/brigadier/CommandSuggestionsTest.java @@ -14,6 +14,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; +import java.util.List; + import static com.mojang.brigadier.arguments.IntegerArgumentType.integer; import static com.mojang.brigadier.arguments.StringArgumentType.word; import static com.mojang.brigadier.builder.LiteralArgumentBuilder.literal; @@ -33,6 +35,18 @@ public class CommandSuggestionsTest { subject = new CommandDispatcher<>(); } + private void testSuggestions(final String contents, final int cursor, final StringRange range, final String... suggestions) { + final Suggestions result = subject.getCompletionSuggestions(subject.parse(contents, source), cursor).join(); + assertThat(result.getRange(), equalTo(range)); + + final List expected = Lists.newArrayList(); + for (final String suggestion : suggestions) { + expected.add(new Suggestion(range, suggestion)); + } + + assertThat(result.getList(), equalTo(expected)); + } + private static StringReader inputWithOffset(final String input, final int offset) { final StringReader result = new StringReader(input); result.setCursor(offset); @@ -102,6 +116,31 @@ public class CommandSuggestionsTest { assertThat(result.getList(), equalTo(Lists.newArrayList(new Suggestion(StringRange.at(7), "bar"), new Suggestion(StringRange.at(7), "baz"), new Suggestion(StringRange.at(7), "foo")))); } + @Test + public void getCompletionSuggestions_movingCursor_subCommands() throws Exception { + subject.register( + literal("parent_one") + .then(literal("faz")) + .then(literal("fbz")) + .then(literal("gaz")) + ); + + subject.register( + literal("parent_two") + ); + + testSuggestions("parent_one faz ", 0, StringRange.at(0), "parent_one", "parent_two"); + testSuggestions("parent_one faz ", 1, StringRange.between(0, 1), "parent_one", "parent_two"); + testSuggestions("parent_one faz ", 7, StringRange.between(0, 7), "parent_one", "parent_two"); + testSuggestions("parent_one faz ", 8, StringRange.between(0, 8), "parent_one"); + testSuggestions("parent_one faz ", 10, StringRange.at(0)); + testSuggestions("parent_one faz ", 11, StringRange.at(11), "faz", "fbz", "gaz"); + testSuggestions("parent_one faz ", 12, StringRange.between(11, 12), "faz", "fbz"); + testSuggestions("parent_one faz ", 13, StringRange.between(11, 13), "faz"); + testSuggestions("parent_one faz ", 14, StringRange.at(0)); + testSuggestions("parent_one faz ", 15, StringRange.at(0)); + } + @Test public void getCompletionSuggestions_subCommands_partial() throws Exception { subject.register( @@ -121,10 +160,10 @@ public class CommandSuggestionsTest { @Test public void getCompletionSuggestions_subCommands_partial_withInputOffset() throws Exception { subject.register( - literal("parent") - .then(literal("foo")) - .then(literal("bar")) - .then(literal("baz")) + literal("parent") + .then(literal("foo")) + .then(literal("bar")) + .then(literal("baz")) ); final ParseResults parse = subject.parse(inputWithOffset("junk parent b", 5), source); @@ -158,6 +197,29 @@ public class CommandSuggestionsTest { assertThat(result.getList(), equalTo(Lists.newArrayList(new Suggestion(StringRange.between(9, 10), "sub")))); } + @Test + public void getCompletionSuggestions_movingCursor_redirect() throws Exception { + final LiteralCommandNode actualOne = subject.register(literal("actual_one") + .then(literal("faz")) + .then(literal("fbz")) + .then(literal("gaz")) + ); + + final LiteralCommandNode actualTwo = subject.register(literal("actual_two")); + + subject.register(literal("redirect_one").redirect(actualOne)); + subject.register(literal("redirect_two").redirect(actualOne)); + + testSuggestions("redirect_one faz ", 0, StringRange.at(0), "actual_one", "actual_two", "redirect_one", "redirect_two"); + testSuggestions("redirect_one faz ", 9, StringRange.between(0, 9), "redirect_one", "redirect_two"); + testSuggestions("redirect_one faz ", 10, StringRange.between(0, 10), "redirect_one"); + testSuggestions("redirect_one faz ", 12, StringRange.at(0)); + testSuggestions("redirect_one faz ", 13, StringRange.at(13), "faz", "fbz", "gaz"); + testSuggestions("redirect_one faz ", 14, StringRange.between(13, 14), "faz", "fbz"); + testSuggestions("redirect_one faz ", 15, StringRange.between(13, 15), "faz"); + testSuggestions("redirect_one faz ", 16, StringRange.at(0)); + testSuggestions("redirect_one faz ", 17, StringRange.at(0)); + } @Test public void getCompletionSuggestions_redirectPartial_withInputOffset() throws Exception { diff --git a/src/test/java/com/mojang/brigadier/context/CommandContextTest.java b/src/test/java/com/mojang/brigadier/context/CommandContextTest.java index 3fb9648..95d58a0 100644 --- a/src/test/java/com/mojang/brigadier/context/CommandContextTest.java +++ b/src/test/java/com/mojang/brigadier/context/CommandContextTest.java @@ -7,13 +7,13 @@ import com.google.common.testing.EqualsTester; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.RootCommandNode; 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 org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; import static org.mockito.Mockito.mock; @@ -26,9 +26,12 @@ public class CommandContextTest { @Mock private CommandDispatcher dispatcher; + @Mock + private CommandNode rootNode; + @Before public void setUp() throws Exception { - builder = new CommandContextBuilder<>(dispatcher, source, 0); + builder = new CommandContextBuilder<>(dispatcher, source, rootNode, 0); } @Test(expected = IllegalArgumentException.class) @@ -53,22 +56,30 @@ public class CommandContextTest { assertThat(builder.build("").getSource(), is(source)); } + @Test + public void testRootNode() throws Exception { + assertThat(builder.build("").getRootNode(), is(rootNode)); + } + @SuppressWarnings("unchecked") @Test public void testEquals() throws Exception { final Object otherSource = new Object(); final Command command = mock(Command.class); final Command otherCommand = mock(Command.class); + final CommandNode rootNode = mock(CommandNode.class); + final CommandNode otherRootNode = mock(CommandNode.class); final CommandNode node = mock(CommandNode.class); final CommandNode otherNode = mock(CommandNode.class); new EqualsTester() - .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, 0).build(""), new CommandContextBuilder<>(dispatcher, source, 0).build("")) - .addEqualityGroup(new CommandContextBuilder<>(dispatcher, otherSource, 0).build(""), new CommandContextBuilder<>(dispatcher, otherSource, 0).build("")) - .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, 0).withCommand(command).build(""), new CommandContextBuilder<>(dispatcher, source, 0).withCommand(command).build("")) - .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, 0).withCommand(otherCommand).build(""), new CommandContextBuilder<>(dispatcher, source, 0).withCommand(otherCommand).build("")) - .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, 0).withArgument("foo", new ParsedArgument<>(0, 1, 123)).build("123"), new CommandContextBuilder<>(dispatcher, source, 0).withArgument("foo", new ParsedArgument<>(0, 1, 123)).build("123")) - .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, 0).withNode(node, StringRange.between(0, 3)).withNode(otherNode, StringRange.between(4, 6)).build("123 456"), new CommandContextBuilder<>(dispatcher, source, 0).withNode(node, StringRange.between(0, 3)).withNode(otherNode, StringRange.between(4, 6)).build("123 456")) - .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, 0).withNode(otherNode, StringRange.between(0, 3)).withNode(node, StringRange.between(4, 6)).build("123 456"), new CommandContextBuilder<>(dispatcher, source, 0).withNode(otherNode, StringRange.between(0, 3)).withNode(node, StringRange.between(4, 6)).build("123 456")) + .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, rootNode, 0).build(""), new CommandContextBuilder<>(dispatcher, source, rootNode, 0).build("")) + .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, otherRootNode, 0).build(""), new CommandContextBuilder<>(dispatcher, source, otherRootNode, 0).build("")) + .addEqualityGroup(new CommandContextBuilder<>(dispatcher, otherSource, rootNode, 0).build(""), new CommandContextBuilder<>(dispatcher, otherSource, rootNode, 0).build("")) + .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withCommand(command).build(""), new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withCommand(command).build("")) + .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withCommand(otherCommand).build(""), new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withCommand(otherCommand).build("")) + .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withArgument("foo", new ParsedArgument<>(0, 1, 123)).build("123"), new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withArgument("foo", new ParsedArgument<>(0, 1, 123)).build("123")) + .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withNode(node, StringRange.between(0, 3)).withNode(otherNode, StringRange.between(4, 6)).build("123 456"), new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withNode(node, StringRange.between(0, 3)).withNode(otherNode, StringRange.between(4, 6)).build("123 456")) + .addEqualityGroup(new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withNode(otherNode, StringRange.between(0, 3)).withNode(node, StringRange.between(4, 6)).build("123 456"), new CommandContextBuilder<>(dispatcher, source, rootNode, 0).withNode(otherNode, StringRange.between(0, 3)).withNode(node, StringRange.between(4, 6)).build("123 456")) .testEquals(); } } \ No newline at end of file diff --git a/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java b/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java index 48eb5f9..e7525e5 100644 --- a/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java +++ b/src/test/java/com/mojang/brigadier/tree/ArgumentCommandNodeTest.java @@ -32,7 +32,7 @@ public class ArgumentCommandNodeTest extends AbstractCommandNodeTest { @Before public void setUp() throws Exception { node = argument("foo", integer()).build(); - contextBuilder = new CommandContextBuilder<>(new CommandDispatcher<>(), new Object(), 0); + contextBuilder = new CommandContextBuilder<>(new CommandDispatcher<>(), new Object(), new RootCommandNode<>(), 0); } @Test diff --git a/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java b/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java index 5ac8fc1..a053cea 100644 --- a/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java +++ b/src/test/java/com/mojang/brigadier/tree/LiteralCommandNodeTest.java @@ -37,7 +37,7 @@ public class LiteralCommandNodeTest extends AbstractCommandNodeTest { @Before public void setUp() throws Exception { node = literal("foo").build(); - contextBuilder = new CommandContextBuilder<>(new CommandDispatcher<>(), new Object(), 0); + contextBuilder = new CommandContextBuilder<>(new CommandDispatcher<>(), new Object(), new RootCommandNode<>(), 0); } @Test diff --git a/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java b/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java index 9c10124..e1b1066 100644 --- a/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java +++ b/src/test/java/com/mojang/brigadier/tree/RootCommandNodeTest.java @@ -34,7 +34,7 @@ public class RootCommandNodeTest extends AbstractCommandNodeTest { @Test public void testParse() throws Exception { final StringReader reader = new StringReader("hello world"); - node.parse(reader, new CommandContextBuilder<>(new CommandDispatcher<>(), new Object(), 0)); + node.parse(reader, new CommandContextBuilder<>(new CommandDispatcher<>(), new Object(), new RootCommandNode<>(), 0)); assertThat(reader.getCursor(), is(0)); }