v0.4.0: Refactor Command Completions to look up context - API BREAK

Just add ", c" to your registerCompletion handler:

commandManager.getCommandCompletions().registerCompletion("test", (sender, config, input, c) -> {

});
This commit is contained in:
Aikar
2017-04-29 00:30:48 -04:00
parent b593038a61
commit ae87e7440a
6 changed files with 178 additions and 73 deletions
@@ -41,7 +41,7 @@ public final class ACFExample extends JavaPlugin {
private void registerCommands() {
commandManager = ACF.createManager(this);
commandManager.getCommandContexts().registerContext(SomeObject.class, SomeObject.getContextResolver());
commandManager.getCommandCompletions().registerCompletion("test", (sender, completionConfig, input) -> (
commandManager.getCommandCompletions().registerCompletion("test", (sender, config, input, c) -> (
Lists.newArrayList("foo", "bar", "baz")
));
commandManager.registerCommand(new SomeCommand());
+1 -1
View File
@@ -29,7 +29,7 @@
<groupId>co.aikar</groupId>
<artifactId>acf-core</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.4.0-SNAPSHOT</version>
<name>ACF</name>
@@ -393,16 +393,9 @@ public abstract class BaseCommand extends Command {
}
String[] completions = ACFPatterns.SPACE.split(cmd.complete.value());
final int argIndex = args.length - 1;
String input = args[argIndex];
final String completion = argIndex < completions.length ? completions[argIndex] : null;
List<String> cmds = manager.getCommandCompletions().of(sender, completion, input);
if (cmds.isEmpty()) {
cmds = ImmutableList.of(input);
}
return filterTabComplete(args[(argIndex)], cmds);
List<String> cmds = manager.getCommandCompletions().of(cmd, sender, completions, args);
return filterTabComplete(args[args.length-1], cmds);
}
private static List<String> filterTabComplete(String arg, List<String> cmds) {
@@ -38,16 +38,16 @@ import java.util.stream.Stream;
public class BukkitCommandCompletions extends CommandCompletions {
BukkitCommandCompletions() {
super();
registerCompletion("mobs", (sender, completionConfig, input) -> {
registerCompletion("mobs", (sender, config, input, c) -> {
final Stream<String> normal = Stream.of(EntityType.values())
.map(entityType -> ACFUtil.simplifyString(entityType.getName()));
return normal.collect(Collectors.toList());
});
registerCompletion("worlds", (sender, completionConfig, input) -> (
registerCompletion("worlds", (sender, config, input, c) -> (
Bukkit.getWorlds().stream().map(World::getName).collect(Collectors.toList())
));
registerCompletion("players", (sender, completionConfig, input) -> {
registerCompletion("players", (sender, config, input, c) -> {
Validate.notNull(sender, "Sender cannot be null");
Player senderPlayer = sender instanceof Player ? (Player) sender : null;
@@ -26,7 +26,10 @@ package co.aikar.commands;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.Parameter;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -39,11 +42,11 @@ public class CommandCompletions {
private Map<String, CommandCompletionHandler> completionMap = new HashMap<>();
public CommandCompletions() {
registerCompletion("range", (sender, completionConfig, input) -> {
if (completionConfig == null) {
registerCompletion("range", (sender, config, input, c) -> {
if (config == null) {
return ImmutableList.of();
}
final String[] ranges = ACFPatterns.DASH.split(completionConfig);
final String[] ranges = ACFPatterns.DASH.split(config);
int start;
int end;
if (ranges.length != 2) {
@@ -55,36 +58,131 @@ public class CommandCompletions {
}
return IntStream.rangeClosed(start, end).mapToObj(Integer::toString).collect(Collectors.toList());
});
registerCompletion("timeunits", (sender, completionConfig, input) -> ImmutableList.of("minutes", "hours", "days", "weeks", "months", "years"));
registerCompletion("timeunits", (sender, config, input, c) -> ImmutableList.of("minutes", "hours", "days", "weeks", "months", "years"));
}
public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler handler) {
return this.completionMap.put("@" + id.toLowerCase(), handler);
}
public List<String> of(CommandSender sender, String completion, String input) {
@NotNull
List<String> of(RegisteredCommand command, CommandSender sender, String[] completionInfo, String[] args) {
final int argIndex = args.length - 1;
String input = args[argIndex];
final String completion = argIndex < completionInfo.length ? completionInfo[argIndex] : null;
if (completion == null) {
return ImmutableList.of();
}
if (input == null) {
input = "";
return ImmutableList.of(input);
}
return getCompletionValues(command, sender, completion, args);
}
@NotNull
List<String> getCompletionValues(RegisteredCommand command, CommandSender sender, String completion, String[] args) {
final int argIndex = args.length - 1;
String input = args[argIndex];
String[] complete = ACFPatterns.COLON.split(completion, 2);
CommandCompletionHandler handler = this.completionMap.get(complete[0].toLowerCase());
if (handler != null) {
List<String> completions = handler.getCompletions(sender, complete.length == 1 ? null : complete[1], input);
if (completions == null) {
return ImmutableList.of();
} else {
return completions;
String config = complete.length == 1 ? null : complete[1];
CommandCompletionContext context = new CommandCompletionContext(command, sender, input, config, args);
try {
Collection<String> completions = handler.getCompletions(sender, config, input, context);
if (completions != null) {
return Lists.newArrayList(completions);
}
//noinspection ConstantIfStatement,ConstantConditions
if (false) { // Hack to fool compiler. since its sneakily thrown.
throw new CommandCompletionTextLookupException();
}
} catch (CommandCompletionTextLookupException ignored) {
// This should only happen if some other feedback error occured.
} catch (Exception e) {
ACFLog.exception(e);
}
// Something went wrong in lookup, fall back to input
return ImmutableList.of(input);
}
// Plaintext values.
return Lists.newArrayList(ACFPatterns.PIPE.split(completion));
}
public interface CommandCompletionHandler {
List<String> getCompletions(CommandSender sender, String config, String input);
Collection<String> getCompletions(CommandSender sender, String config, String input, CommandCompletionContext context) throws InvalidCommandArgument;
}
public class CommandCompletionContext {
private final RegisteredCommand command;
private final CommandSender sender;
private final String input;
private final String config;
private final List<String> args;
CommandCompletionContext(RegisteredCommand command, CommandSender sender, String input, String config, String[] args) {
this.command = command;
this.sender = sender;
this.input = input;
this.config = config;
this.args = Lists.newArrayList(args);
}
public <T> T getContextValue(Class<? extends T> clazz) throws InvalidCommandArgument {
return getContextValue(clazz, null);
}
public <T> T getContextValue(Class<? extends T> clazz, Integer paramIdx) throws InvalidCommandArgument {
String name = null;
if (paramIdx != null) {
if (paramIdx >= command.parameters.length) {
throw new IllegalArgumentException("Param index is higher than number of parameters");
}
Parameter param = command.parameters[paramIdx];
Class<?> paramType = param.getType();
if (!clazz.isAssignableFrom(paramType)) {
throw new IllegalArgumentException(param.getName() +":" + paramType.getName() + " can not satisfy " + clazz.getName());
}
name = param.getName();
} else {
Parameter[] parameters = command.parameters;
for (int i = 0; i < parameters.length; i++) {
Parameter param = parameters[i];
if (clazz.isAssignableFrom(param.getType())) {
paramIdx = i;
name = param.getName();
break;
}
}
if (paramIdx == null) {
throw new IllegalStateException("Can not find any parameter that can satisfy " + clazz.getName());
}
}
Map<String, Object> resolved = command.resolveContexts(sender, args, args.size());
if (resolved == null || paramIdx > resolved.size()) {
ACFLog.error("resolved: " + resolved + " paramIdx: " + paramIdx + " - size: " + (resolved != null ? resolved.size() : null ));
ACFUtil.sneaky(new CommandCompletionTextLookupException());
}
//noinspection unchecked
return (T) resolved.get(name);
}
public CommandSender getSender() {
return sender;
}
public String getInput() {
return input;
}
public String getConfig() {
return config;
}
}
private class CommandCompletionTextLookupException extends Throwable {
}
}
@@ -32,12 +32,14 @@ import co.aikar.commands.annotation.Syntax;
import co.aikar.commands.annotation.Values;
import co.aikar.commands.contexts.ContextResolver;
import co.aikar.commands.contexts.SenderAwareContextResolver;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -125,49 +127,8 @@ public class RegisteredCommand {
return;
}
try {
Map<String, Object> passedArgs = Maps.newLinkedHashMap();
for (int i = 0; i < parameters.length; i++) {
boolean isLast = i == parameters.length - 1;
final Parameter parameter = parameters[i];
final String parameterName = parameter.getName();
final Class<?> type = parameter.getType();
final ContextResolver<?> resolver = resolvers[i];
CommandExecutionContext context = new CommandExecutionContext(this, parameter, sender, args, i, passedArgs);
if (args.isEmpty() && !(isLast && type == String[].class)) {
Default def = parameter.getAnnotation(Default.class);
Optional opt = parameter.getAnnotation(Optional.class);
if (isLast && def != null) {
args.add(def.value());
} else if (isLast && opt != null) {
passedArgs.put(parameterName, resolver instanceof SenderAwareContextResolver ? resolver.getContext(context) : null);
//noinspection UnnecessaryContinue
continue;
} else if (!(resolver instanceof SenderAwareContextResolver)) {
scope.showSyntax(sender, this);
return;
}
}
final Values values = parameter.getAnnotation(Values.class);
if (values != null) {
String arg = args.get(0);
final String[] split = ACFPatterns.PIPE.split(values.value());
Set<String> possible = Sets.newHashSet();
for (String s : split) {
List<String> check = this.scope.manager.getCommandCompletions().of(sender, s, arg);
if (!check.isEmpty()) {
possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList()));
} else {
possible.add(s.toLowerCase());
}
}
if (!possible.contains(arg.toLowerCase())) {
throw new InvalidCommandArgument("Must be one of: " + ACFUtil.join(possible, ", "));
}
}
passedArgs.put(parameterName, resolver.getContext(context));
}
Map<String, Object> passedArgs = resolveContexts(sender, args);
if (passedArgs == null) return;
method.invoke(scope, passedArgs.values().toArray());
} catch (Exception e) {
@@ -175,7 +136,6 @@ public class RegisteredCommand {
e = (Exception) e.getCause();
}
if (e instanceof InvalidCommandArgument) {
if (e.getMessage() != null && !e.getMessage().isEmpty()) {
ACFUtil.sendMsg(sender, "&cError: " + e.getMessage());
}
@@ -189,6 +149,60 @@ public class RegisteredCommand {
}
}
@Nullable
Map<String, Object> resolveContexts(CommandSender sender, List<String> args) throws InvalidCommandArgument {
return resolveContexts(sender, args, parameters.length);
}
@Nullable
Map<String, Object> resolveContexts(CommandSender sender, List<String> args, int argLimit) throws InvalidCommandArgument {
args = Lists.newArrayList(args);
String[] origArgs = args.toArray(new String[args.size()]);
Map<String, Object> passedArgs = Maps.newLinkedHashMap();
for (int i = 0; i < parameters.length && i < argLimit; i++) {
boolean isLast = i == parameters.length - 1;
final Parameter parameter = parameters[i];
final String parameterName = parameter.getName();
final Class<?> type = parameter.getType();
final ContextResolver<?> resolver = resolvers[i];
CommandExecutionContext context = new CommandExecutionContext(this, parameter, sender, args, i, passedArgs);
if (args.isEmpty() && !(isLast && type == String[].class)) {
Default def = parameter.getAnnotation(Default.class);
Optional opt = parameter.getAnnotation(Optional.class);
if (isLast && def != null) {
args.add(def.value());
} else if (isLast && opt != null) {
passedArgs.put(parameterName, resolver instanceof SenderAwareContextResolver ? resolver.getContext(context) : null);
//noinspection UnnecessaryContinue
continue;
} else if (!(resolver instanceof SenderAwareContextResolver)) {
scope.showSyntax(sender, this);
return null;
}
}
final Values values = parameter.getAnnotation(Values.class);
if (values != null) {
String arg = args.get(0);
final String[] split = ACFPatterns.PIPE.split(values.value());
Set<String> possible = Sets.newHashSet();
for (String s : split) {
List<String> check = this.scope.manager.getCommandCompletions().getCompletionValues(this, sender, s, origArgs);
if (!check.isEmpty()) {
possible.addAll(check.stream().map(String::toLowerCase).collect(Collectors.toList()));
} else {
possible.add(s.toLowerCase());
}
}
if (!possible.contains(arg.toLowerCase())) {
throw new InvalidCommandArgument("Must be one of: " + ACFUtil.join(possible, ", "));
}
}
passedArgs.put(parameterName, resolver.getContext(context));
}
return passedArgs;
}
CommandTiming getTiming() {
if (this.timing == null) {
this.timing = ACFUtil.getTiming(scope, command);