Merge pull request #440 from benwoo1110/feat/completion-filter

Add support for having custom completion filters
This commit is contained in:
chickeneer
2026-05-11 17:10:10 -05:00
committed by GitHub
4 changed files with 209 additions and 19 deletions
@@ -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<String> cmds = manager.getCommandCompletions().of(cmd, issuer, args, isAsync);
return filterTabComplete(args[args.length - 1], cmds);
return manager.getCommandCompletions().of(cmd, issuer, args, isAsync);
}
/**
@@ -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 <C>
*/
public interface CommandCompletionFilter<C extends CommandCompletionContext> {
/**
* 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 <C> completion context type
* @return the no-op filter
*/
static <C extends CommandCompletionContext> CommandCompletionFilter<C> 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 <C> completion context type
* @return the starts-with filter
*/
static <C extends CommandCompletionContext> CommandCompletionFilter<C> 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 <C> completion context type
* @return the contains filter
*/
static <C extends CommandCompletionContext> CommandCompletionFilter<C> 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);
}
@@ -44,7 +44,7 @@ public class CommandCompletions<C extends CommandCompletionContext> {
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<String, CommandCompletionHandler> completionMap = new HashMap<>();
private Map<String, CompletionEntry> completionMap = new HashMap<>();
private Map<Class, String> defaultCompletions = new HashMap<>();
public CommandCompletions(CommandManager manager) {
@@ -72,14 +72,27 @@ public class CommandCompletions<C extends CommandCompletionContext> {
}
/**
* 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<C> 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<C> handler, CommandCompletionFilter<C> 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<C extends CommandCompletionContext> {
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<C extends CommandCompletionContext> {
* @return
*/
public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler<C> 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.
* <p>
* Not all platforms support this, so if the platform does not support asynchronous execution,
* your handler will be executed on the main thread.
* <p>
* Use this anytime your handler does not need to access state that is not considered thread safe.
* <p>
* Use context.isAsync() to determine if you are async or not.
*
* @param id
* @param handler
* @param filter
* @return
*/
public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler<C> handler, CommandCompletionFilter<C> 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<C extends CommandCompletionContext> {
* @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<C> filter) {
return registerStaticCompletion(id, ACFPatterns.PIPE.split(list), filter);
}
/**
@@ -137,7 +184,19 @@ public class CommandCompletions<C extends CommandCompletionContext> {
* @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<C> filter) {
return registerStaticCompletion(id, Arrays.asList(completions), filter);
}
/**
@@ -149,7 +208,20 @@ public class CommandCompletions<C extends CommandCompletionContext> {
* @return
*/
public CommandCompletionHandler registerStaticCompletion(String id, Supplier<Collection<String>> 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<Collection<String>> supplier, CommandCompletionFilter<C> filter) {
return registerStaticCompletion(id, supplier.get(), filter);
}
/**
@@ -163,6 +235,18 @@ public class CommandCompletions<C extends CommandCompletionContext> {
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<String> completions, CommandCompletionFilter<C> 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
* <p>
@@ -174,7 +258,7 @@ public class CommandCompletions<C extends CommandCompletionContext> {
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<C extends CommandCompletionContext> {
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<C extends CommandCompletionContext> {
try {
//noinspection unchecked
Collection<String> completions = handler.getCompletions(context);
Collection<String> completions = completionEntry.handler.getCompletions(context);
//Handle completions with more than one word:
if (!repeat && completions != null
@@ -287,7 +371,9 @@ public class CommandCompletions<C extends CommandCompletionContext> {
}
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<C extends CommandCompletionContext> {
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;
}
}
}
@@ -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());
}