Add Proper support for Default Completion Handlers

This commit is contained in:
Aikar
2019-03-14 22:28:13 -04:00
parent aa2e6ad1f8
commit 4e103adca4
7 changed files with 75 additions and 16 deletions
@@ -23,6 +23,7 @@
package co.aikar.commands;
import co.aikar.commands.bukkit.contexts.OnlinePlayer;
import org.apache.commons.lang.Validate;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
@@ -65,7 +66,7 @@ public class BukkitCommandCompletions extends CommandCompletions<BukkitCommandCo
});
registerAsyncCompletion("dyecolors", c -> ACFUtil.enumNames(DyeColor.values()));
registerCompletion("worlds", c -> (
Bukkit.getWorlds().stream().map(World::getName).collect(Collectors.toList())
Bukkit.getWorlds().stream().map(World::getName).collect(Collectors.toList())
));
registerCompletion("players", c -> {
@@ -86,6 +87,9 @@ public class BukkitCommandCompletions extends CommandCompletions<BukkitCommandCo
matchedPlayers.sort(String.CASE_INSENSITIVE_ORDER);
return matchedPlayers;
});
setDefaultCompletion("players", OnlinePlayer.class, co.aikar.commands.contexts.OnlinePlayer.class, Player.class);
setDefaultCompletion("worlds", World.class);
}
}
@@ -680,7 +680,7 @@ public abstract class BaseCommand {
* @return All results to complete the command.
*/
private List<String> completeCommand(CommandIssuer issuer, RegisteredCommand cmd, String[] args, String commandLabel, boolean isAsync) {
if (!cmd.hasPermission(issuer) || args.length > cmd.consumeInputResolvers || args.length == 0 || cmd.complete == null) {
if (!cmd.hasPermission(issuer) || args.length > cmd.consumeInputResolvers || args.length == 0) {
return Collections.emptyList();
}
@@ -39,7 +39,9 @@ import java.util.stream.IntStream;
@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
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<Class, String> defaultCompletions = new HashMap<>();
@@ -75,7 +77,7 @@ public class CommandCompletions<C extends CommandCompletionContext> {
* @return
*/
public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler<C> handler) {
return this.completionMap.put("@" + id.toLowerCase(), handler);
return this.completionMap.put(prepareCompletionId(id), handler);
}
/**
@@ -94,7 +96,7 @@ public class CommandCompletions<C extends CommandCompletionContext> {
* @return
*/
public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler<C> handler) {
return this.completionMap.put("@" + id.toLowerCase(), handler);
return this.completionMap.put(prepareCompletionId(id), handler);
}
/**
@@ -146,14 +148,16 @@ public class CommandCompletions<C extends CommandCompletionContext> {
}
/**
* Registers a completion handler such as @players to default apply to all command parameters of the specified types
* <p>
* This enables automatic completion support for parameters without manually defining it for custom objects
*
* @param id
* @param classes
* @return
* @deprecated Feature Not done yet
*/
CommandCompletionHandler setDefaultCompletion(String id, Class... classes) {
public void setDefaultCompletion(String id, Class... classes) {
// get completion with specified id
id = id.toLowerCase();
id = prepareCompletionId(id);
CommandCompletionHandler completion = completionMap.get(id);
if (completion == null) {
@@ -164,8 +168,11 @@ public class CommandCompletions<C extends CommandCompletionContext> {
for (Class clazz : classes) {
defaultCompletions.put(clazz, id);
}
}
return completion;
@NotNull
private static String prepareCompletionId(String id) {
return (id.startsWith("@") ? "" : "@") + id.toLowerCase();
}
@NotNull
@@ -176,6 +183,10 @@ public class CommandCompletions<C extends CommandCompletionContext> {
String input = args[argIndex];
String completion = argIndex < completions.length ? completions[argIndex] : null;
if (completion == null || "*".equals(completion)) {
completion = findDefaultCompletion(cmd, args);
}
if (completion == null && completions.length > 0) {
String last = completions[completions.length - 1];
if (last.startsWith("repeat@")) {
@@ -190,7 +201,35 @@ public class CommandCompletions<C extends CommandCompletionContext> {
return getCompletionValues(cmd, sender, completion, args, isAsync);
}
String findDefaultCompletion(RegisteredCommand cmd, String[] args) {
int i = 0;
for (CommandParameter param : cmd.parameters) {
if (param.canConsumeInput() && ++i == args.length) {
Class type = param.getType();
while (type != null) {
String completion = this.defaultCompletions.get(type);
if (completion != null) {
return completion;
}
type = type.getSuperclass();
}
if (param.getType().isEnum()) {
CommandOperationContext ctx = CommandManager.getCurrentCommandOperationContext();
//noinspection unchecked
ctx.enumCompletionValues = ACFUtil.enumNames((Class<? extends Enum<?>>) param.getType());
return DEFAULT_ENUM_ID;
}
break;
}
}
return null;
}
List<String> getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args, boolean isAsync) {
if (DEFAULT_ENUM_ID.equals(completion)) {
CommandOperationContext<?> ctx = CommandManager.getCurrentCommandOperationContext();
return ctx.enumCompletionValues;
}
if (completion.startsWith("repeat@")) {
completion = completion.substring(6);
}
@@ -205,7 +205,7 @@ public class CommandContexts<R extends CommandExecutionContext<?, ? extends Comm
Enum<?> match = ACFUtil.simpleMatch(enumCls, first);
if (match == null) {
List<String> names = ACFUtil.enumNames(enumCls);
throw new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF, "{valid}", ACFUtil.join(names));
throw new InvalidCommandArgument(MessageKeys.PLEASE_SPECIFY_ONE_OF, "{valid}", ACFUtil.join(names, ", "));
}
return match;
});
@@ -24,11 +24,12 @@
package co.aikar.commands;
import java.lang.annotation.Annotation;
import java.util.List;
/**
* Holds information about the currently executing command on this thread
*/
public class CommandOperationContext <I extends CommandIssuer> {
public class CommandOperationContext<I extends CommandIssuer> {
private final CommandManager manager;
private final I issuer;
@@ -37,6 +38,7 @@ public class CommandOperationContext <I extends CommandIssuer> {
private final String[] args;
private final boolean isAsync;
private RegisteredCommand registeredCommand;
List<String> enumCompletionValues;
CommandOperationContext(CommandManager manager, I issuer, BaseCommand command, String commandLabel, String[] args, boolean isAsync) {
this.manager = manager;
@@ -81,6 +83,7 @@ public class CommandOperationContext <I extends CommandIssuer> {
/**
* This method will not support annotation processors!! use getAnnotationValue or hasAnnotation
*
* @deprecated Use {@link #getAnnotationValue(Class)}
*/
@Deprecated
@@ -88,7 +88,7 @@ public class RegisteredCommand<CEC extends CommandExecutionContext<CEC, ? extend
this.prefSubCommand = prefSubCommand;
this.permission = annotations.getAnnotationValue(method, CommandPermission.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY);
this.complete = annotations.getAnnotationValue(method, CommandCompletion.class);
this.complete = annotations.getAnnotationValue(method, CommandCompletion.class, Annotations.DEFAULT_EMPTY); // no replacements as it should be per-issuer
this.helpText = annotations.getAnnotationValue(method, Description.class, Annotations.REPLACEMENTS | Annotations.DEFAULT_EMPTY);
this.conditions = annotations.getAnnotationValue(method, Conditions.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY);
this.helpSearchTags = annotations.getAnnotationValue(method, HelpSearchTags.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY);
@@ -262,6 +262,9 @@ public class RegisteredCommand<CEC extends CommandExecutionContext<CEC, ? extend
Set<String> possible = new HashSet<>();
CommandCompletions commandCompletions = this.manager.getCommandCompletions();
for (String s : parameter.getValues()) {
if ("*".equals(s) || "@completions".equals(s)) {
s = commandCompletions.findDefaultCompletion(this, origArgs);
}
//noinspection unchecked
List<String> check = commandCompletions.getCompletionValues(this, sender, s, origArgs, opContext.isAsync());
if (!check.isEmpty()) {
@@ -63,7 +63,7 @@ public class SomeCommand extends BaseCommand {
private String testString3;
@Subcommand("testDI")
public void onTestDI(CommandSender sender){
public void onTestDI(CommandSender sender) {
sender.sendMessage("The value for the injected SomeHandler is " + someHandler.getSomeField());
sender.sendMessage("Plugin is null? " + (plugin == null));
sender.sendMessage("Test String 1: " + testString);
@@ -117,11 +117,15 @@ public class SomeCommand extends BaseCommand {
// /acf Aikar wo<tab> with a world named "world" would provide world as a completion.
// @test was custom defined in ACFExample as foobar, so only that value would show up as a completion
// then finally /acfc Aikar world foobar <tab> would show foo1, foo2, foo3 statically
// * means 'default', which is registered to @players for OnlinePlayer. Same for worlds.
// @test is a manually defined handler to a String input, can't default that one.
// the foo1|foo2|foo3 defines a static list of completion values for the foo1 parameter
// Then the enum will also pick up default of its values even though it was left off of the completion
@Subcommand("completions")
@CommandAlias("acfcompletions|acfc")
@CommandCompletion("@players @worlds @test foo1|foo2|foo3")
public void onTestCompletion(CommandSender sender, OnlinePlayer player, World world, String test, String misc) {
sender.sendMessage("You got " + player.getPlayer().getName() + " - " + world.getName() + " - " + test + " - " + misc);
@CommandCompletion("* * @test foo1|foo2|foo3")
public void onTestCompletion(CommandSender sender, OnlinePlayer player, World world, String test, String foo1, TestEnum e) {
sender.sendMessage("You got " + player.getPlayer().getName() + " - " + world.getName() + " - " + test + " - " + foo1 + " - " + e.name());
}
@@ -166,6 +170,7 @@ public class SomeCommand extends BaseCommand {
public void onTest1(Player player, String testX) {
player.sendMessage("You got test inner inner test3: " + testX);
}
// Requires /acf test next test4 or simply /deepinner command alias
@CommandAlias("deepinner")
@Subcommand("test4|td4")
@@ -175,4 +180,9 @@ public class SomeCommand extends BaseCommand {
}
}
}
public enum TestEnum {
FOOTEST1,
BARTEST2
}
}