From 882154a6a828be0698937ad785d76bf0889eca09 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 8 Oct 2020 04:12:07 -0700 Subject: [PATCH] :bug: Fix async completions (#38) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Alexander Söderberg --- .../commandframework/CommandManager.java | 21 +++++- .../brigadier/CloudBrigadierManager.java | 30 +++----- .../bukkit/BukkitCommand.java | 17 ----- .../bukkit/BukkitCommandManager.java | 32 +++++++++ .../BukkitPluginRegistrationHandler.java | 19 +++-- .../bukkit/CommandSuggestionsListener.java | 70 +++++++++++++++++++ .../AsyncCommandSuggestionsListener.java | 25 ++++--- 7 files changed, 160 insertions(+), 54 deletions(-) create mode 100644 cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/CommandSuggestionsListener.java diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java index 9fe2ef8c..7c49931e 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java @@ -75,6 +75,8 @@ import java.util.function.Function; */ public abstract class CommandManager { + private static final List SINGLE_EMPTY_SUGGESTION = Collections.unmodifiableList(Collections.singletonList("")); + private final Map, BiConsumer> exceptionHandlers = new HashMap<>(); private final EnumSet managerSettings = EnumSet.of( ManagerSettings.ENFORCE_INTERMEDIARY_PERMISSIONS); @@ -167,15 +169,23 @@ public abstract class CommandManager { ) { final CommandContext context = this.commandContextFactory.create(true, commandSender); final LinkedList inputQueue = tokenize(input); + + final List suggestions; if (this.preprocessContext(context, inputQueue) == State.ACCEPTED) { - return this.commandSuggestionProcessor.apply( + suggestions = this.commandSuggestionProcessor.apply( new CommandPreprocessingContext<>(context, inputQueue), this.commandTree.getSuggestions( context, inputQueue) ); } else { - return Collections.emptyList(); + suggestions = Collections.emptyList(); } + + if (this.getSetting(ManagerSettings.FORCE_SUGGESTION) && suggestions.isEmpty()) { + return SINGLE_EMPTY_SUGGESTION; + } + + return suggestions; } /** @@ -635,7 +645,12 @@ public abstract class CommandManager { * for child permission values, if a preceding command in the tree path * has a command handler attached */ - ENFORCE_INTERMEDIARY_PERMISSIONS + ENFORCE_INTERMEDIARY_PERMISSIONS, + /** + * Force sending of an empty suggestion (i.e. a singleton list containing an empty string) + * when no suggestions are present + */ + FORCE_SUGGESTION } } diff --git a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java index 49d05c16..fd164023 100644 --- a/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java +++ b/cloud-minecraft/cloud-brigadier/src/main/java/cloud/commandframework/brigadier/CloudBrigadierManager.java @@ -39,7 +39,6 @@ import cloud.commandframework.arguments.standard.IntegerArgument; import cloud.commandframework.arguments.standard.ShortArgument; import cloud.commandframework.arguments.standard.StringArgument; import cloud.commandframework.context.CommandContext; -import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext; import cloud.commandframework.permission.CommandPermission; import cloud.commandframework.permission.Permission; import cloud.commandframework.types.tuples.Pair; @@ -62,9 +61,7 @@ import io.leangen.geantyref.TypeToken; import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; -import java.util.Collections; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -438,28 +435,23 @@ public final class CloudBrigadierManager { final @NonNull SuggestionsBuilder builder ) { final CommandContext commandContext = this.dummyContextProvider.get(); - final LinkedList inputQueue = new LinkedList<>(Collections.singletonList(builder.getInput())); - final CommandPreprocessingContext commandPreprocessingContext = - new CommandPreprocessingContext<>(commandContext, inputQueue); - /* - List results = server.tabComplete(context.getSource().getBukkitSender(), builder.getInput(), - context.getSource().getWorld(), context.getSource().getPosition(), true); - */ String command = builder.getInput(); if (command.startsWith("/") /* Minecraft specific */) { command = command.substring(1); } - final List suggestions = this.commandManager.suggest(commandContext.getSender(), command); - /* - System.out.println("Filtering out with: " + builder.getInput()); - final CommandSuggestionProcessor processor = this.commandManager.getCommandSuggestionProcessor(); - final List filteredSuggestions = processor.apply(commandPreprocessingContext, suggestions); - System.out.println("Current suggestions: "); - for (final String suggestion : filteredSuggestions) { - System.out.printf("- %s\n", suggestion); - }*/ + /* Remove namespace */ + String leading = command.split(" ")[0]; + if (leading.contains(":")) { + command = command.substring(leading.split(":")[0].length() + 1); + } + + final List suggestions = this.commandManager.suggest( + commandContext.getSender(), + command + ); + for (final String suggestion : suggestions) { String tooltip = argument.getName(); if (!(argument instanceof StaticArgument)) { diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommand.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommand.java index daeba9bd..5c34cc8d 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommand.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommand.java @@ -144,23 +144,6 @@ final class BukkitCommand extends org.bukkit.command.Command implements Plugi return this.cloudCommand.getCommandMeta().getOrDefault("description", ""); } - @Override - public List tabComplete( - final @NonNull CommandSender sender, - final @NonNull String alias, - final @NonNull String @NonNull [] args - ) - throws IllegalArgumentException { - final StringBuilder builder = new StringBuilder(this.command.getName()); - for (final String string : args) { - builder.append(" ").append(string); - } - return this.manager.suggest( - this.manager.getCommandSenderMapper().apply(sender), - builder.toString() - ); - } - @Override public Plugin getPlugin() { return this.manager.getOwningPlugin(); diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java index 2aaa44b1..fd3daf66 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java @@ -156,6 +156,12 @@ public class BukkitCommandManager extends CommandManager { new MultipleEntitySelectorArgument.MultipleEntitySelectorParser<>()); this.getParserRegistry().registerParserSupplier(TypeToken.get(MultiplePlayerSelector.class), parserParameters -> new MultiplePlayerSelectorArgument.MultiplePlayerSelectorParser<>()); + + /* Register suggestion listener */ + this.owningPlugin.getServer().getPluginManager().registerEvents( + new CommandSuggestionsListener<>(this), + this.owningPlugin + ); } /** @@ -282,6 +288,32 @@ public class BukkitCommandManager extends CommandManager { } } + /** + * Strip the plugin namespace from a plugin namespaced command. This + * will also strip the leading '/' if it's present + * + * @param command Command + * @return Stripped command + */ + public final @NonNull String stripNamespace(final @NonNull String command) { + @NonNull String input; + + /* Remove leading '/' */ + if (command.charAt(0) == '/') { + input = command.substring(1); + } else { + input = command; + } + + /* Remove leading plugin namespace */ + final String namespace = String.format("%s:", this.getOwningPlugin().getName().toLowerCase()); + if (input.startsWith(namespace)) { + input = input.substring(namespace.length()); + } + + return input; + } + /** * Get the backwards command sender plugin * diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitPluginRegistrationHandler.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitPluginRegistrationHandler.java index 01aa07cf..3adf16a5 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitPluginRegistrationHandler.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitPluginRegistrationHandler.java @@ -89,13 +89,16 @@ public class BukkitPluginRegistrationHandler implements CommandRegistrationHa this.bukkitCommandManager ); this.registeredCommands.put(commandArgument, bukkitCommand); + if (!this.bukkitCommands.containsKey(label)) { + this.recognizedAliases.add(label); + } + this.recognizedAliases.add(getNamespacedLabel(label)); this.commandMap.register( label, this.bukkitCommandManager.getOwningPlugin().getName().toLowerCase(), bukkitCommand ); this.registerExternal(label, command, bukkitCommand); - this.recognizedAliases.add(label); if (this.bukkitCommandManager.getSplitAliases()) { for (final String alias : aliases) { @@ -107,12 +110,16 @@ public class BukkitPluginRegistrationHandler implements CommandRegistrationHa (CommandArgument) commandArgument, this.bukkitCommandManager ); - this.commandMap.register(alias, this.bukkitCommandManager.getOwningPlugin() - .getName().toLowerCase(), + if (!this.bukkitCommands.containsKey(alias)) { + this.recognizedAliases.add(alias); + } + this.recognizedAliases.add(getNamespacedLabel(alias)); + this.commandMap.register( + alias, + this.bukkitCommandManager.getOwningPlugin().getName().toLowerCase(), bukkitCommand ); this.registerExternal(alias, command, aliasCommand); - this.recognizedAliases.add(alias); } } } @@ -120,6 +127,10 @@ public class BukkitPluginRegistrationHandler implements CommandRegistrationHa return true; } + private @NonNull String getNamespacedLabel(final @NonNull String label) { + return String.format("%s:%s", this.bukkitCommandManager.getOwningPlugin().getName(), label).toLowerCase(); + } + /** * Check if the given alias is recognizable by this registration handler * diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/CommandSuggestionsListener.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/CommandSuggestionsListener.java new file mode 100644 index 00000000..fe7689a1 --- /dev/null +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/CommandSuggestionsListener.java @@ -0,0 +1,70 @@ +// +// MIT License +// +// Copyright (c) 2020 Alexander Söderberg & Contributors +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +package cloud.commandframework.bukkit; + +import org.bukkit.command.CommandSender; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.server.TabCompleteEvent; +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.util.ArrayList; +import java.util.List; + +final class CommandSuggestionsListener implements Listener { + + private final BukkitCommandManager bukkitCommandManager; + + CommandSuggestionsListener(final @NonNull BukkitCommandManager bukkitCommandManager) { + this.bukkitCommandManager = bukkitCommandManager; + } + + @EventHandler + void onTabCompletion(final @NonNull TabCompleteEvent event) { + if (event.getBuffer().isEmpty() || !event.getBuffer().startsWith("/")) { + return; + } + + @SuppressWarnings("unchecked") final BukkitPluginRegistrationHandler bukkitPluginRegistrationHandler = + (BukkitPluginRegistrationHandler) this.bukkitCommandManager.getCommandRegistrationHandler(); + + /* Turn '/plugin:command arg1 arg2 ...' into 'plugin:command' */ + final String commandLabel = event.getBuffer().substring(1).split(" ")[0]; + if (!bukkitPluginRegistrationHandler.isRecognized(commandLabel)) { + return; + } + + final CommandSender sender = event.getSender(); + final C cloudSender = this.bukkitCommandManager.getCommandSenderMapper().apply(sender); + final String inputBuffer = this.bukkitCommandManager.stripNamespace(event.getBuffer()); + + final List suggestions = new ArrayList<>(this.bukkitCommandManager.suggest( + cloudSender, + inputBuffer + )); + + event.setCompletions(suggestions); + } + +} diff --git a/cloud-minecraft/cloud-paper/src/main/java/cloud/commandframework/paper/AsyncCommandSuggestionsListener.java b/cloud-minecraft/cloud-paper/src/main/java/cloud/commandframework/paper/AsyncCommandSuggestionsListener.java index 191d2426..f2ed6f90 100644 --- a/cloud-minecraft/cloud-paper/src/main/java/cloud/commandframework/paper/AsyncCommandSuggestionsListener.java +++ b/cloud-minecraft/cloud-paper/src/main/java/cloud/commandframework/paper/AsyncCommandSuggestionsListener.java @@ -33,9 +33,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import java.util.ArrayList; import java.util.List; -class AsyncCommandSuggestionsListener implements Listener { - - private static final long CACHE_EXPIRATION_TIME = 30L; +final class AsyncCommandSuggestionsListener implements Listener { private final PaperCommandManager paperCommandManager; @@ -44,25 +42,30 @@ class AsyncCommandSuggestionsListener implements Listener { } @EventHandler - void onTabCompletion(final @NonNull AsyncTabCompleteEvent event) throws Exception { + void onTabCompletion(final @NonNull AsyncTabCompleteEvent event) { if (event.getBuffer().isEmpty() || !event.getBuffer().startsWith("/")) { return; } - final String[] arguments = event.getBuffer().substring(1).split(" "); - if (this.paperCommandManager.getCommandTree().getNamedNode(arguments[0]) == null) { - return; - } - @SuppressWarnings("unchecked") final BukkitPluginRegistrationHandler bukkitPluginRegistrationHandler = + + @SuppressWarnings("unchecked") + final BukkitPluginRegistrationHandler bukkitPluginRegistrationHandler = (BukkitPluginRegistrationHandler) this.paperCommandManager.getCommandRegistrationHandler(); - if (!bukkitPluginRegistrationHandler.isRecognized(arguments[0])) { + + /* Turn '/plugin:command arg1 arg2 ...' into 'plugin:command' */ + final String commandLabel = event.getBuffer().substring(1).split(" ")[0]; + if (!bukkitPluginRegistrationHandler.isRecognized(commandLabel)) { return; } + final CommandSender sender = event.getSender(); final C cloudSender = this.paperCommandManager.getCommandSenderMapper().apply(sender); + final String inputBuffer = this.paperCommandManager.stripNamespace(event.getBuffer()); + final List suggestions = new ArrayList<>(this.paperCommandManager.suggest( cloudSender, - event.getBuffer().substring(1) + inputBuffer )); + event.setCompletions(suggestions); event.setHandled(true); }