Add Support for Async Tab Completions for Paper Servers

This adds the ability for plugins to define completion handlers as async safe (not on games main thread)

When they are defined async safe, and ran on a Paper 1.12.2+ server, with a Paper ACF manager,
completions will be handled mostly async, letting you safely do heavier operations in tab completions.
This commit is contained in:
Aikar
2017-11-26 23:21:15 -05:00
parent 229192f99c
commit fbed6f2be3
31 changed files with 278 additions and 103 deletions
@@ -23,7 +23,6 @@
package co.aikar.commands;
import com.google.common.collect.Maps;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
@@ -25,7 +25,6 @@ package co.aikar.commands;
import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.math.BigDecimal;
@@ -23,16 +23,37 @@
package co.aikar.commands;
import co.aikar.commands.annotation.*;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Default;
import co.aikar.commands.annotation.HelpCommand;
import co.aikar.commands.annotation.PreCommand;
import co.aikar.commands.annotation.Subcommand;
import co.aikar.commands.annotation.UnknownHandler;
import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil;
import com.google.common.collect.*;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.Stack;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -307,7 +328,7 @@ public abstract class BaseCommand {
public void execute(CommandIssuer issuer, String commandLabel, String[] args) {
commandLabel = commandLabel.toLowerCase();
try {
CommandOperationContext commandContext = preCommandOperation(issuer, commandLabel, args);
CommandOperationContext commandContext = preCommandOperation(issuer, commandLabel, args, false);
if (args.length > 0) {
CommandSearch cmd = findSubCommand(args);
@@ -319,7 +340,6 @@ public abstract class BaseCommand {
}
}
if (subCommands.get(DEFAULT) != null && args.length == 0) {
executeSubcommand(commandContext, DEFAULT, issuer, args);
} else if (subCommands.get(UNKNOWN) != null) {
@@ -335,6 +355,11 @@ public abstract class BaseCommand {
}
}
RegisteredCommand<?> getRegisteredCommand(String[] args) {
final CommandSearch cmd = findSubCommand(args);
return cmd != null ? cmd.cmd : null;
}
private void postCommandOperation() {
CommandManager.commandOperationContext.get().pop();
execSubcommand = null;
@@ -342,9 +367,9 @@ public abstract class BaseCommand {
origArgs = new String[]{};
}
private CommandOperationContext preCommandOperation(CommandIssuer issuer, String commandLabel, String[] args) {
private CommandOperationContext preCommandOperation(CommandIssuer issuer, String commandLabel, String[] args, boolean isAsync) {
Stack<CommandOperationContext> contexts = CommandManager.commandOperationContext.get();
CommandOperationContext context = this.manager.createCommandOperationContext(this, issuer, commandLabel, args);
CommandOperationContext context = this.manager.createCommandOperationContext(this, issuer, commandLabel, args, isAsync);
contexts.push(context);
lastCommandOperationContext = context;
execSubcommand = null;
@@ -417,37 +442,28 @@ public abstract class BaseCommand {
return true;
}
public List<String> tabComplete(CommandIssuer issuer, String commandLabel, String[] args)
public List<String> tabComplete(CommandIssuer issuer, String commandLabel, String[] args) {
return tabComplete(issuer, commandLabel, args, false);
}
public List<String> tabComplete(CommandIssuer issuer, String commandLabel, String[] args, boolean isAsync)
throws IllegalArgumentException {
commandLabel = commandLabel.toLowerCase();
if (args.length == 0) {
args = new String[]{""};
}
try {
CommandOperationContext commandOperationContext = preCommandOperation(issuer, commandLabel, args);
CommandOperationContext commandOperationContext = preCommandOperation(issuer, commandLabel, args, isAsync);
final CommandSearch search = findSubCommand(args, true);
String argString = ApacheCommonsLangUtil.join(args, " ").toLowerCase();
final List<String> cmds = new ArrayList<>();
if (search != null) {
cmds.addAll(completeCommand(commandOperationContext, issuer, search.cmd, Arrays.copyOfRange(args, search.argIndex, args.length), commandLabel));
cmds.addAll(completeCommand(issuer, search.cmd, Arrays.copyOfRange(args, search.argIndex, args.length), commandLabel, isAsync));
} else if (subCommands.get(UNKNOWN).size() == 1) {
cmds.addAll(completeCommand(commandOperationContext, issuer, Iterables.getOnlyElement(subCommands.get(UNKNOWN)), args, commandLabel));
}
for (Map.Entry<String, RegisteredCommand> entry : subCommands.entries()) {
final String key = entry.getKey();
if (key.startsWith(argString) && !UNKNOWN.equals(key) && !DEFAULT.equals(key)) {
final RegisteredCommand value = entry.getValue();
if (!value.hasPermission(issuer)) {
continue;
}
String prefCommand = value.prefSubCommand;
final String[] psplit = ACFPatterns.SPACE.split(prefCommand);
cmds.add(psplit[args.length - 1]);
}
cmds.addAll(completeCommand(issuer, Iterables.getOnlyElement(subCommands.get(UNKNOWN)), args, commandLabel, isAsync));
}
return filterTabComplete(args[args.length - 1], cmds);
@@ -456,7 +472,25 @@ public abstract class BaseCommand {
}
}
private List<String> completeCommand(CommandOperationContext commandOperationContext, CommandIssuer issuer, RegisteredCommand cmd, String[] args, String commandLabel) {
List<String> getCommandsForCompletion(CommandIssuer issuer, String[] args) {
final Set<String> cmds = new HashSet<>();
String argString = ApacheCommonsLangUtil.join(args, " ").toLowerCase();
for (Map.Entry<String, RegisteredCommand> entry : subCommands.entries()) {
final String key = entry.getKey();
if (key.startsWith(argString) && !UNKNOWN.equals(key) && !DEFAULT.equals(key)) {
final RegisteredCommand value = entry.getValue();
if (!value.hasPermission(issuer)) {
continue;
}
String[] split = ACFPatterns.SPACE.split(value.prefSubCommand);
cmds.add(split[args.length - 1]);
}
}
return new ArrayList<>(cmds);
}
private List<String> completeCommand(CommandIssuer issuer, RegisteredCommand cmd, String[] args, String commandLabel, boolean isAsync) {
if (!cmd.hasPermission(issuer) || args.length > cmd.requiredResolvers + cmd.optionalResolvers || args.length == 0
|| cmd.complete == null) {
return ImmutableList.of();
@@ -464,7 +498,7 @@ public abstract class BaseCommand {
String[] completions = ACFPatterns.SPACE.split(cmd.complete);
List<String> cmds = manager.getCommandCompletions().of(commandOperationContext, cmd, issuer, completions, args);
List<String> cmds = manager.getCommandCompletions().of(cmd, issuer, completions, args, isAsync);
return filterTabComplete(args[args.length-1], cmds);
}
@@ -124,4 +124,8 @@ public class CommandCompletionContext {
public String getConfig() {
return config;
}
public boolean isAsync() {
return CommandManager.getCurrentCommandOperationContext().isAsync();
}
}
@@ -42,8 +42,8 @@ public class CommandCompletions <C extends CommandCompletionContext> {
public CommandCompletions(CommandManager manager) {
this.manager = manager;
registerCompletion("nothing", c -> ImmutableList.of());
registerCompletion("range", (c) -> {
registerAsyncCompletion("nothing", c -> ImmutableList.of());
registerAsyncCompletion("range", (c) -> {
String config = c.getConfig();
if (config == null) {
return ImmutableList.of();
@@ -60,15 +60,19 @@ public class CommandCompletions <C extends CommandCompletionContext> {
}
return IntStream.rangeClosed(start, end).mapToObj(Integer::toString).collect(Collectors.toList());
});
registerCompletion("timeunits", (c) -> ImmutableList.of("minutes", "hours", "days", "weeks", "months", "years"));
registerAsyncCompletion("timeunits", (c) -> ImmutableList.of("minutes", "hours", "days", "weeks", "months", "years"));
}
public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler<C> handler) {
return this.completionMap.put("@" + id.toLowerCase(), handler);
}
public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler<C> handler) {
return this.completionMap.put("@" + id.toLowerCase(), handler);
}
@NotNull
List<String> of(CommandOperationContext commandOperationContext, RegisteredCommand command, CommandIssuer sender, String[] completionInfo, String[] args) {
List<String> of(RegisteredCommand command, CommandIssuer sender, String[] completionInfo, String[] args, boolean isAsync) {
final int argIndex = args.length - 1;
String input = args[argIndex];
@@ -77,11 +81,10 @@ public class CommandCompletions <C extends CommandCompletionContext> {
return ImmutableList.of(input);
}
return getCompletionValues(command, sender, completion, args);
return getCompletionValues(command, sender, completion, args, isAsync);
}
@NotNull
List<String> getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args) {
List<String> getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args, boolean isAsync) {
completion = manager.getCommandReplacements().replace(completion);
List<String> allCompletions = Lists.newArrayList();
@@ -91,6 +94,10 @@ public class CommandCompletions <C extends CommandCompletionContext> {
String[] complete = ACFPatterns.COLONEQUALS.split(value, 2);
CommandCompletionHandler handler = this.completionMap.get(complete[0].toLowerCase());
if (handler != null) {
if (isAsync && !(handler instanceof AsyncCommandCompletionHandler)) {
ACFUtil.sneaky(new SyncCompletionRequired());
return null;
}
String config = complete.length == 1 ? null : complete[1];
CommandCompletionContext context = manager.createCompletionContext(command, sender, input, config, args);
@@ -123,5 +130,7 @@ public class CommandCompletions <C extends CommandCompletionContext> {
public interface CommandCompletionHandler <C extends CommandCompletionContext> {
Collection<String> getCompletions(C context) throws InvalidCommandArgument;
}
public interface AsyncCommandCompletionHandler <C extends CommandCompletionContext> extends CommandCompletionHandler <C> {}
public static class SyncCompletionRequired extends Exception {}
}
@@ -31,6 +31,7 @@ import co.aikar.commands.contexts.IssuerAwareContextResolver;
import co.aikar.commands.contexts.IssuerOnlyContextResolver;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import java.lang.annotation.Annotation;
import java.lang.reflect.Parameter;
import java.util.List;
@@ -27,7 +27,12 @@ import co.aikar.locales.MessageKeyProvider;
import com.google.common.collect.SetMultimap;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
@SuppressWarnings("WeakerAccess")
@@ -132,7 +132,7 @@ public abstract class CommandManager <I, AI extends CommandIssuer, FT, F extends
/** @deprecated Unstable API */ @Deprecated @UnstableAPI
public CommandHelp generateCommandHelp(CommandIssuer issuer, @NotNull String command) {
verifyUnstableAPI("help");
return generateCommandHelp(issuer, obtainRootCommand(ACFPatterns.SPACE.split(command, 2)[0]));
return generateCommandHelp(issuer, obtainRootCommand(command));
}
/** @deprecated Unstable API */ @Deprecated @UnstableAPI
@@ -230,8 +230,16 @@ public abstract class CommandManager <I, AI extends CommandIssuer, FT, F extends
return true;
}
public synchronized RootCommand obtainRootCommand(String cmd) {
return rootCommands.computeIfAbsent(cmd.toLowerCase(), this::createRootCommand);
BaseCommand getBaseCommand(String commandLabel, @NotNull String[] args) {
RootCommand rootCommand = obtainRootCommand(commandLabel);
if (rootCommand == null) {
return null;
}
return rootCommand.getBaseCommand(args);
}
public synchronized RootCommand obtainRootCommand(@NotNull String cmd) {
return rootCommands.computeIfAbsent(ACFPatterns.SPACE.split(cmd.toLowerCase(), 2)[0], this::createRootCommand);
}
public RegisteredCommand createRegisteredCommand(BaseCommand command, String cmdName, Method method, String prefSubCommand) {
@@ -311,14 +319,14 @@ public abstract class CommandManager <I, AI extends CommandIssuer, FT, F extends
return getLocales().getDefaultLocale();
}
public CommandOperationContext createCommandOperationContext(BaseCommand command, CommandIssuer issuer, String commandLabel, String[] args) {
CommandOperationContext createCommandOperationContext(BaseCommand command, CommandIssuer issuer, String commandLabel, String[] args, boolean isAsync) {
return new CommandOperationContext(
this,
issuer,
command,
commandLabel,
args
args,
isAsync
);
}
@@ -35,14 +35,16 @@ public class CommandOperationContext {
private final BaseCommand command;
private final String commandLabel;
private final String[] args;
private final boolean isAsync;
private RegisteredCommand registeredCommand;
CommandOperationContext(CommandManager manager, CommandIssuer issuer, BaseCommand command, String commandLabel, String[] args) {
CommandOperationContext(CommandManager manager, CommandIssuer issuer, BaseCommand command, String commandLabel, String[] args, boolean isAsync) {
this.manager = manager;
this.issuer = issuer;
this.command = command;
this.commandLabel = commandLabel;
this.args = args;
this.isAsync = isAsync;
}
public CommandManager getCommandManager() {
@@ -65,6 +67,10 @@ public class CommandOperationContext {
return args;
}
public boolean isAsync() {
return isAsync;
}
public void setRegisteredCommand(RegisteredCommand registeredCommand) {
this.registeredCommand = registeredCommand;
}
@@ -23,7 +23,10 @@
package co.aikar.commands;
import java.util.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
/**
@@ -23,7 +23,14 @@
package co.aikar.commands;
import co.aikar.commands.annotation.*;
import co.aikar.commands.annotation.CommandAlias;
import co.aikar.commands.annotation.CommandCompletion;
import co.aikar.commands.annotation.CommandPermission;
import co.aikar.commands.annotation.Default;
import co.aikar.commands.annotation.Description;
import co.aikar.commands.annotation.Optional;
import co.aikar.commands.annotation.Syntax;
import co.aikar.commands.annotation.Values;
import co.aikar.commands.contexts.ContextResolver;
import co.aikar.commands.contexts.IssuerAwareContextResolver;
import co.aikar.commands.contexts.IssuerOnlyContextResolver;
@@ -188,6 +195,7 @@ public class RegisteredCommand <R extends CommandExecutionContext<? extends Comm
String[] origArgs = args.toArray(new String[args.size()]);
Map<String, Object> passedArgs = Maps.newLinkedHashMap();
int remainingRequired = requiredResolvers;
CommandOperationContext opContext = CommandManager.getCurrentCommandOperationContext();
for (int i = 0; i < parameters.length && i < argLimit; i++) {
boolean isLast = i == parameters.length - 1;
boolean allowOptional = remainingRequired == 0;
@@ -222,7 +230,7 @@ public class RegisteredCommand <R extends CommandExecutionContext<? extends Comm
final String[] split = ACFPatterns.PIPE.split(scope.manager.getCommandReplacements().replace(values.value()));
Set<String> possible = Sets.newHashSet();
for (String s : split) {
List<String> check = this.manager.getCommandCompletions().getCompletionValues(this, sender, s, origArgs);
List<String> check = this.manager.getCommandCompletions().getCompletionValues(this, sender, s, origArgs, opContext.isAsync());
if (!check.isEmpty()) {
possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList()));
} else {
@@ -26,6 +26,8 @@ package co.aikar.commands;
import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil;
import com.google.common.collect.SetMultimap;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -34,6 +36,7 @@ interface RootCommand {
CommandManager getManager();
SetMultimap<String, RegisteredCommand> getSubCommands();
List<BaseCommand> getChildren();
String getCommandName();
default void addChildShared(List<BaseCommand> children, SetMultimap<String, RegisteredCommand> subCommands, BaseCommand command) {
@@ -59,6 +62,13 @@ interface RootCommand {
}
default BaseCommand execute(CommandIssuer sender, String commandLabel, String[] args) {
BaseCommand command = getBaseCommand(args);
command.execute(sender, commandLabel, args);
return command;
}
default BaseCommand getBaseCommand(String[] args) {
BaseCommand command = getDefCommand();
for (int i = args.length; i >= 0; i--) {
String checkSub = ApacheCommonsLangUtil.join(args, " ", 0, i).toLowerCase();
@@ -68,11 +78,19 @@ interface RootCommand {
break;
}
}
command.execute(sender, commandLabel, args);
return command;
}
default List<String> getTabCompletions(CommandIssuer sender, String alias, String[] args) throws IllegalArgumentException {
Set<String> completions = new HashSet<>();
getChildren().forEach(child -> {
completions.addAll(child.tabComplete(sender, alias, args));
completions.addAll(child.getCommandsForCompletion(sender, args));
});
return new ArrayList<>(completions);
}
default RegisteredCommand getDefaultRegisteredCommand() {
BaseCommand defCommand = this.getDefCommand();
if (defCommand != null) {