From 68595f9657eea8d30a1addd887f3b22d4ca0152f Mon Sep 17 00:00:00 2001 From: Ben Woo <30431861+benwoo1110@users.noreply.github.com> Date: Mon, 4 May 2026 23:46:46 +0800 Subject: [PATCH] Add support for having custom completion filters --- .../java/co/aikar/commands/BaseCommand.java | 5 +- .../commands/CommandCompletionFilter.java | 96 ++++++++++++++ .../co/aikar/commands/CommandCompletions.java | 123 ++++++++++++++++-- .../java/co/aikar/commands/RootCommand.java | 4 +- 4 files changed, 209 insertions(+), 19 deletions(-) create mode 100644 core/src/main/java/co/aikar/commands/CommandCompletionFilter.java diff --git a/core/src/main/java/co/aikar/commands/BaseCommand.java b/core/src/main/java/co/aikar/commands/BaseCommand.java index 60a0b188..e68aeb32 100644 --- a/core/src/main/java/co/aikar/commands/BaseCommand.java +++ b/core/src/main/java/co/aikar/commands/BaseCommand.java @@ -651,7 +651,7 @@ public abstract class BaseCommand { } } - return filterTabComplete(args[args.length - 1], cmds); + return cmds; } finally { postCommandOperation(); } @@ -706,8 +706,7 @@ public abstract class BaseCommand { return Collections.emptyList(); } - List cmds = manager.getCommandCompletions().of(cmd, issuer, args, isAsync); - return filterTabComplete(args[args.length - 1], cmds); + return manager.getCommandCompletions().of(cmd, issuer, args, isAsync); } /** diff --git a/core/src/main/java/co/aikar/commands/CommandCompletionFilter.java b/core/src/main/java/co/aikar/commands/CommandCompletionFilter.java new file mode 100644 index 00000000..2121bc72 --- /dev/null +++ b/core/src/main/java/co/aikar/commands/CommandCompletionFilter.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2016-2026 Daniel Ennis (Aikar) - MIT License + * + * 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 co.aikar.commands; + + +import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil; + +import java.util.Locale; + +/** + * A filter for completions, allowing you to control which completions are shown to user based on the input and context. + * + * @param + */ +public interface CommandCompletionFilter { + + /** + * Accepts all completions without filtering. + */ + CommandCompletionFilter NONE = (context, completion) -> true; + + /** + * Filters completions that start with the input (case-insensitive). + */ + CommandCompletionFilter STARTS_WITH = (context, completion) -> + ApacheCommonsLangUtil.startsWithIgnoreCase(completion, context.getInput()); + + /** + * Filters completions that contain the input (case-insensitive). + */ + CommandCompletionFilter CONTAINS = (context, completion) -> + completion.toLowerCase(Locale.ENGLISH).contains(context.getInput().toLowerCase(Locale.ENGLISH)); + + /** + * Gets a filter that accepts all completions. Use this instead of {@link CommandCompletionFilter#NONE} to prevent + * rawtype unchecked warnings. + * + * @param completion context type + * @return the no-op filter + */ + static CommandCompletionFilter none() { + return NONE; + } + + /** + * Gets a filter that matches completions starting with the input (case-insensitive). Use this instead of + * {@link CommandCompletionFilter#STARTS_WITH} to prevent rawtype unchecked warnings. + * + * @param completion context type + * @return the starts-with filter + */ + static CommandCompletionFilter startsWith() { + return STARTS_WITH; + } + + /** + * Gets a filter that matches completions containing the input (case-insensitive). Use this instead of + * {@link CommandCompletionFilter#CONTAINS} to prevent rawtype unchecked warnings. + * + * @param completion context type + * @return the contains filter + */ + static CommandCompletionFilter contains() { + return CONTAINS; + } + + /** + * Tests whether a completion should be included based on the context. + * + * @param context the completion context + * @param completion the completion string to test + * @return true if the completion passes the filter, false otherwise. + */ + boolean test(C context, String completion); +} diff --git a/core/src/main/java/co/aikar/commands/CommandCompletions.java b/core/src/main/java/co/aikar/commands/CommandCompletions.java index 7825f431..4551676c 100644 --- a/core/src/main/java/co/aikar/commands/CommandCompletions.java +++ b/core/src/main/java/co/aikar/commands/CommandCompletions.java @@ -44,7 +44,7 @@ public class CommandCompletions { private static final String DEFAULT_ENUM_ID = "@__defaultenum__"; private final CommandManager manager; // TODO: use a CompletionProvider that can return a delegated Id or provide values such as enum support - private Map completionMap = new HashMap<>(); + private Map completionMap = new HashMap<>(); private Map defaultCompletions = new HashMap<>(); public CommandCompletions(CommandManager manager) { @@ -72,14 +72,27 @@ public class CommandCompletions { } /** - * Registr a completion handler to provide command completions based on the user input. + * Register a completion handler to provide command completions based on the user input. * * @param id * @param handler * @return */ public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler handler) { - return this.completionMap.put(prepareCompletionId(id), handler); + return registerCompletion(id, handler, CommandCompletionFilter.startsWith()); + } + + /** + * Register a completion handler to provide command completions based on the user input and with a custom filter. + * + * @param id + * @param handler + * @param filter + * @return + */ + public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler handler, CommandCompletionFilter filter) { + CompletionEntry previous = this.completionMap.put(prepareCompletionId(id), new CompletionEntry(handler, filter)); + return previous != null ? previous.handler : null; } /** @@ -93,7 +106,7 @@ public class CommandCompletions { throw new IllegalStateException("The supplied key " + id + " does not exist in any completions"); } - return this.completionMap.remove(id); + return this.completionMap.remove(id).handler; } /** @@ -112,7 +125,28 @@ public class CommandCompletions { * @return */ public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler handler) { - return this.completionMap.put(prepareCompletionId(id), handler); + return registerAsyncCompletion(id, handler, CommandCompletionFilter.startsWith()); + } + + /** + * Registr a completion handler to provide command completions based on the user input with a custom filter. + * This handler is declared to be safe to be executed asynchronously. + *

+ * Not all platforms support this, so if the platform does not support asynchronous execution, + * your handler will be executed on the main thread. + *

+ * Use this anytime your handler does not need to access state that is not considered thread safe. + *

+ * Use context.isAsync() to determine if you are async or not. + * + * @param id + * @param handler + * @param filter + * @return + */ + public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler handler, CommandCompletionFilter filter) { + CompletionEntry previous = this.completionMap.put(prepareCompletionId(id), new CompletionEntry(handler, filter)); + return previous != null ? previous.handler : null; } /** @@ -126,7 +160,20 @@ public class CommandCompletions { * @return */ public CommandCompletionHandler registerStaticCompletion(String id, String list) { - return registerStaticCompletion(id, ACFPatterns.PIPE.split(list)); + return registerStaticCompletion(id, ACFPatterns.PIPE.split(list), CommandCompletionFilter.startsWith()); + } + + /** + * Register a static list of command completions that will never change with a custom filter. + * Like @CommandCompletion, values are | (PIPE) separated. + * + * @param id + * @param list + * @param filter + * @return + */ + public CommandCompletionHandler registerStaticCompletion(String id, String list, CommandCompletionFilter filter) { + return registerStaticCompletion(id, ACFPatterns.PIPE.split(list), filter); } /** @@ -137,7 +184,19 @@ public class CommandCompletions { * @return */ public CommandCompletionHandler registerStaticCompletion(String id, String[] completions) { - return registerStaticCompletion(id, Arrays.asList(completions)); + return registerStaticCompletion(id, Arrays.asList(completions), CommandCompletionFilter.startsWith()); + } + + /** + * Register a static list of command completions that will never change with a custom filter. + * + * @param id + * @param completions + * @param filter + * @return + */ + public CommandCompletionHandler registerStaticCompletion(String id, String[] completions, CommandCompletionFilter filter) { + return registerStaticCompletion(id, Arrays.asList(completions), filter); } /** @@ -149,7 +208,20 @@ public class CommandCompletions { * @return */ public CommandCompletionHandler registerStaticCompletion(String id, Supplier> supplier) { - return registerStaticCompletion(id, supplier.get()); + return registerStaticCompletion(id, supplier.get(), CommandCompletionFilter.startsWith()); + } + + /** + * Register a static list of command completions that will never change with a custom filer. The list is obtained + * from the supplier immediately as part of this method call. + * + * @param id + * @param supplier + * @param filter + * @return + */ + public CommandCompletionHandler registerStaticCompletion(String id, Supplier> supplier, CommandCompletionFilter filter) { + return registerStaticCompletion(id, supplier.get(), filter); } /** @@ -163,6 +235,18 @@ public class CommandCompletions { return registerAsyncCompletion(id, x -> completions); } + /** + * Register a static list of command completions that will never change with a custom filter. + * + * @param id + * @param completions + * @param filter + * @return + */ + public CommandCompletionHandler registerStaticCompletion(String id, Collection completions, CommandCompletionFilter filter) { + return registerAsyncCompletion(id, x -> completions, filter); + } + /** * Registers a completion handler such as @players to default apply to all command parameters of the specified types *

@@ -174,7 +258,7 @@ public class CommandCompletions { public void setDefaultCompletion(String id, Class... classes) { // get completion with specified id id = prepareCompletionId(id); - CommandCompletionHandler completion = completionMap.get(id); + CompletionEntry completion = completionMap.get(id); if (completion == null) { // Throw something because no completion with specified id @@ -257,9 +341,9 @@ public class CommandCompletions { for (String value : ACFPatterns.PIPE.split(completion)) { String[] complete = ACFPatterns.COLONEQUALS.split(value, 2); - CommandCompletionHandler handler = this.completionMap.get(complete[0].toLowerCase(Locale.ENGLISH)); - if (handler != null) { - if (isAsync && !(handler instanceof AsyncCommandCompletionHandler)) { + CompletionEntry completionEntry = this.completionMap.get(complete[0].toLowerCase(Locale.ENGLISH)); + if (completionEntry != null) { + if (isAsync && !(completionEntry.handler instanceof AsyncCommandCompletionHandler)) { ACFUtil.sneaky(new SyncCompletionRequired()); return null; } @@ -268,7 +352,7 @@ public class CommandCompletions { try { //noinspection unchecked - Collection completions = handler.getCompletions(context); + Collection completions = completionEntry.handler.getCompletions(context); //Handle completions with more than one word: if (!repeat && completions != null @@ -287,7 +371,9 @@ public class CommandCompletions { } if (completions != null) { - allCompletions.addAll(completions); + completions.stream() + .filter(str -> completionEntry.filter.test(context, str)) + .forEach(allCompletions::add); continue; } //noinspection ConstantIfStatement,ConstantConditions @@ -319,4 +405,13 @@ public class CommandCompletions { public static class SyncCompletionRequired extends RuntimeException { } + private static final class CompletionEntry { + private final CommandCompletionHandler handler; + private final CommandCompletionFilter filter; + + private CompletionEntry(CommandCompletionHandler handler, CommandCompletionFilter filter) { + this.handler = handler; + this.filter = filter; + } + } } diff --git a/core/src/main/java/co/aikar/commands/RootCommand.java b/core/src/main/java/co/aikar/commands/RootCommand.java index e0eff4d3..2b76b676 100644 --- a/core/src/main/java/co/aikar/commands/RootCommand.java +++ b/core/src/main/java/co/aikar/commands/RootCommand.java @@ -27,10 +27,10 @@ import co.aikar.commands.CommandRouter.CommandRouteResult; import co.aikar.commands.CommandRouter.RouteSearch; import com.google.common.collect.SetMultimap; -import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.stream.Collectors; public interface RootCommand { void addChild(BaseCommand command); @@ -128,7 +128,7 @@ public interface RootCommand { } completions.addAll(child.getCommandsForCompletion(sender, args)); }); - return new ArrayList<>(completions); + return completions.stream().distinct().collect(Collectors.toList()); }