From 12753723881a26a7f21974aef191816d36253adb Mon Sep 17 00:00:00 2001 From: Brendan Grieve Date: Sat, 27 Oct 2018 15:55:48 +0800 Subject: [PATCH] Implement Naned Parameters and Per Parameter Completions A parameter can optionally be annoted with @Switch to provide one or more names it can be referred by. It can then be used anywhere before its normal position upto and including its normal position. It can also be used normally by position as well without the switch name. Due to the nature of how Tab completions work, a Switch MUST use the parameter CommandCompletion annotation to provide tab completions, otherwise it will assume to have none in those positions. Method Level CommandCompletion annotations will be respected as well. Both execution and tab completion use a method parseArgument to ensure both follow the same parsing rules. --- .../java/co/aikar/commands/BaseCommand.java | 3 +- .../co/aikar/commands/CommandCompletions.java | 17 +- .../co/aikar/commands/CommandParameter.java | 39 ++++- .../co/aikar/commands/RegisteredCommand.java | 151 ++++++++++++++++-- .../annotation/CommandCompletion.java | 5 +- .../co/aikar/commands/annotation/Switch.java | 38 +++++ 6 files changed, 230 insertions(+), 23 deletions(-) create mode 100644 core/src/main/java/co/aikar/commands/annotation/Switch.java diff --git a/core/src/main/java/co/aikar/commands/BaseCommand.java b/core/src/main/java/co/aikar/commands/BaseCommand.java index b60e437df..75f72635b 100644 --- a/core/src/main/java/co/aikar/commands/BaseCommand.java +++ b/core/src/main/java/co/aikar/commands/BaseCommand.java @@ -710,7 +710,6 @@ public List tabComplete(CommandIssuer issuer, String commandLabel, Strin final CommandSearch search = findSubCommand(args, true); - final List cmds = new ArrayList<>(); if (search != null) { @@ -772,7 +771,7 @@ List getCommandsForCompletion(CommandIssuer issuer, String[] args) { * @return All results to complete the command. */ private List 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 == 0 || cmd.complete == null) { return Collections.emptyList(); } diff --git a/core/src/main/java/co/aikar/commands/CommandCompletions.java b/core/src/main/java/co/aikar/commands/CommandCompletions.java index f901fa715..37e7bca02 100644 --- a/core/src/main/java/co/aikar/commands/CommandCompletions.java +++ b/core/src/main/java/co/aikar/commands/CommandCompletions.java @@ -170,20 +170,21 @@ CommandCompletionHandler setDefaultCompletion(String id, Class... classes) { @NotNull List of(RegisteredCommand cmd, CommandIssuer sender, String[] args, boolean isAsync) { - String[] completions = ACFPatterns.SPACE.split(cmd.complete); - final int argIndex = args.length - 1; + RegisteredCommand.CommandData commandData = cmd.parseArguments(args); - String input = args[argIndex]; + final int argIndex = commandData.args.size() - 1; + String input = argIndex >= 0 ? commandData.args.get(argIndex) : ""; - String completion = argIndex < completions.length ? completions[argIndex] : null; - if (completion == null && completions.length > 0) { - completion = completions[completions.length - 1]; + // Check if its a switch + if (commandData.isSwitch) { + return commandData.switches; } - if (completion == null) { + + if (commandData.complete == null) { return Collections.singletonList(input); } - return getCompletionValues(cmd, sender, completion, args, isAsync); + return getCompletionValues(cmd, sender, commandData.complete, args, isAsync); } List getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args, boolean isAsync) { diff --git a/core/src/main/java/co/aikar/commands/CommandParameter.java b/core/src/main/java/co/aikar/commands/CommandParameter.java index 029ff776f..823c09d34 100644 --- a/core/src/main/java/co/aikar/commands/CommandParameter.java +++ b/core/src/main/java/co/aikar/commands/CommandParameter.java @@ -23,11 +23,13 @@ package co.aikar.commands; +import co.aikar.commands.annotation.CommandCompletion; import co.aikar.commands.annotation.Conditions; import co.aikar.commands.annotation.Default; import co.aikar.commands.annotation.Description; import co.aikar.commands.annotation.Flags; import co.aikar.commands.annotation.Optional; +import co.aikar.commands.annotation.Switch; import co.aikar.commands.annotation.Syntax; import co.aikar.commands.annotation.Values; import co.aikar.commands.contexts.ContextResolver; @@ -36,6 +38,7 @@ import co.aikar.commands.contexts.OptionalContextResolver; import java.lang.reflect.Parameter; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -52,6 +55,8 @@ public class CommandParameter command, Parameter param, int par this.defaultValue = annotations.getAnnotationValue(param, Default.class, Annotations.REPLACEMENTS | (type != String.class ? Annotations.NO_EMPTY : 0)); this.description = annotations.getAnnotationValue(param, Description.class, Annotations.REPLACEMENTS | Annotations.DEFAULT_EMPTY); this.conditions = annotations.getAnnotationValue(param, Conditions.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); + this.complete = annotations.getAnnotationValue(param, CommandCompletion.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); + this.switches = annotations.getAnnotationValue(param, Switch.class, Annotations.REPLACEMENTS | Annotations.NO_EMPTY); //noinspection unchecked this.resolver = manager.getCommandContexts().getResolver(type); @@ -94,9 +101,17 @@ public CommandParameter(RegisteredCommand command, Parameter param, int par this.syntax = annotations.getAnnotationValue(param, Syntax.class); if (syntax == null) { if (!requiresInput && canConsumeInput) { - this.syntax = "[" + name + "]"; + if (switches != null) { + this.syntax = "[-" + switches.split(",")[0] + " <" + name + ">]"; + } else { + this.syntax = "[" + name + "]"; + } } else if (requiresInput) { - this.syntax = "<" + name + ">"; + if (switches != null) { + this.syntax = "[-" + switches.split(",")[0] + "] " + "<" + name + ">"; + } else { + this.syntax = "<" + name + ">"; + } } } } @@ -254,4 +269,24 @@ public String getConditions() { public void setConditions(String conditions) { this.conditions = conditions; } + + public String getComplete() { + return complete; + } + + public void setComplete(String complete) { + this.complete = complete; + } + + public String getSwitches() { + return switches; + } + + public void setSwitches(String value) { + this.switches = value; + } + + public boolean hasSwitch(String value) { + return switches != null && Arrays.asList(switches.split(",")).contains(value); + } } diff --git a/core/src/main/java/co/aikar/commands/RegisteredCommand.java b/core/src/main/java/co/aikar/commands/RegisteredCommand.java index 0439db875..0034311b0 100644 --- a/core/src/main/java/co/aikar/commands/RegisteredCommand.java +++ b/core/src/main/java/co/aikar/commands/RegisteredCommand.java @@ -142,9 +142,12 @@ void invoke(CommandIssuer sender, List args, CommandOperationContext con try { this.manager.conditions.validateConditions(context); Map passedArgs = resolveContexts(sender, args); + if (passedArgs == null) return; - method.invoke(scope, passedArgs.values().toArray()); + method.invoke(scope, Arrays.stream(parameters) + .map(p -> passedArgs.getOrDefault(p.getName(), null)) + .toArray()); } catch (Exception e) { handleException(sender, args, e); } @@ -196,15 +199,17 @@ Map resolveContexts(CommandIssuer sender, List args) thr } @Nullable Map resolveContexts(CommandIssuer sender, List args, int argLimit) throws InvalidCommandArgument { - args = new ArrayList<>(args); - String[] origArgs = args.toArray(new String[args.size()]); + CommandData commandData = parseArguments(args.toArray(new String[0])); + List arguments = new ArrayList<>(commandData.args); + + String[] origArgs = arguments.toArray(new String[0]); Map passedArgs = new LinkedHashMap<>(); int remainingRequired = requiredResolvers; CommandOperationContext opContext = CommandManager.getCurrentCommandOperationContext(); - for (int i = 0; i < parameters.length && i < argLimit; i++) { - boolean isLast = i == parameters.length - 1; + for (int i = 0; i < commandData.parameters.size() && i < argLimit; i++) { + boolean isLast = i == commandData.parameters.size() - 1; boolean allowOptional = remainingRequired == 0; - final CommandParameter parameter = parameters[i]; + final CommandParameter parameter = commandData.parameters.get(i); if (parameter.isCommandIssuer()) { argLimit++; } @@ -213,14 +218,14 @@ Map resolveContexts(CommandIssuer sender, List args, int //noinspection unchecked final ContextResolver resolver = parameter.getResolver(); //noinspection unchecked - CEC context = (CEC) this.manager.createCommandContext(this, parameter, sender, args, i, passedArgs); + CEC context = (CEC) this.manager.createCommandContext(this, parameter, sender, commandData.args, i, passedArgs); boolean requiresInput = parameter.requiresInput(); if (requiresInput && remainingRequired > 0) { remainingRequired--; } - if (args.isEmpty() && !(isLast && type == String[].class)) { + if (arguments.isEmpty() && !(isLast && type == String[].class)) { if (allowOptional && parameter.getDefaultValue() != null) { - args.add(parameter.getDefaultValue()); + arguments.add(parameter.getDefaultValue()); } else if (allowOptional && parameter.isOptional()) { Object value = parameter.isOptionalResolver() ? resolver.getContext(context) : null; if (value == null && parameter.getClass().isPrimitive()) { @@ -237,7 +242,7 @@ Map resolveContexts(CommandIssuer sender, List args, int } } if (parameter.getValues() != null) { - String arg = !args.isEmpty() ? args.get(0) : ""; + String arg = !arguments.isEmpty() ? arguments.get(0) : ""; Set possible = new HashSet<>(); CommandCompletions commandCompletions = this.manager.getCommandCompletions(); @@ -318,4 +323,130 @@ public void addSubcommand(String cmd) { public void addSubcommands(Collection cmd) { this.registeredSubcommands.addAll(cmd); } + + /** + * Resolve arguments and parameters with respect to positional and named parameters + */ + public CommandData parseArguments(String[] args) { + + // Command completions + List completions = new ArrayList<>(); + if (complete != null) { + completions.addAll(Arrays.asList(ACFPatterns.SPACE.split(complete))); + } + + // List of all parameters + List parameters = Arrays.stream(this.parameters).collect(Collectors.toList()); + + // Return data + CommandData data = new CommandData(); + + // All Arguments + List arguments = Arrays.stream(args).collect(Collectors.toList()); + boolean isSwitch = false; + + // Current Parameter + CommandParameter currentParameter = null; + + // Current Argument + String currentArg = null; + + while (arguments.size() > 0 && parameters.size() > 0) { + currentArg = arguments.remove(0); + currentParameter = null; + + // Check if its a switch + if (currentArg.startsWith("-")) { + isSwitch = true; + + String finalArg = currentArg; + currentParameter = parameters.stream() + .filter(CommandParameter::canConsumeInput) + .filter(p -> p.hasSwitch(finalArg.substring(1))) + .findFirst() + .orElse(null); + + if (currentParameter != null) { + + parameters.remove(currentParameter); + + // Arguments consumed + if (arguments.size() > 0) { + isSwitch = false; + currentArg = arguments.remove(0); + } else { + currentArg = null; + } + + if (currentParameter.getComplete() != null) { + data.complete = currentParameter.getComplete(); + } + } + } + + // Deal with positional parameter + if (currentParameter == null) { + while(parameters.size() > 0) { + currentParameter = parameters.remove(0); + if (!currentParameter.canConsumeInput()) { + data.parameters.add(currentParameter); + currentParameter = null; + } else { + break; + } + } + + if (currentParameter == null) { + break; + } + + // Save completions + if (currentParameter.getComplete() != null) { + data.complete = currentParameter.getComplete(); + } else { + data.complete = completions.size() > 0 ? completions.remove(0) : null; + } + } + + // Save switch result + if (arguments.size() < 1 && currentParameter.getSwitches() != null) { + data.switches.add("-" + currentParameter.getSwitches().split(",")[0]); + } + + // Save arg + if (currentArg != null) { + data.args.add(currentArg); + } + + // Save Parameter + data.parameters.add(currentParameter); + } + + // Add rest of data + data.switches.addAll(parameters.stream() + .filter(p -> p.getSwitches() != null) + .map(p -> "-" + p.getSwitches().split(",")[0]) + .collect(Collectors.toList())); + + if (arguments.size() > 0) { + isSwitch = false; + data.args.addAll(arguments); + data.complete = null; + } + data.parameters.addAll(parameters); + + data.isSwitch = isSwitch; + + return data; + } + + public static class CommandData { + + public List parameters = new ArrayList<>(); + public List args = new ArrayList<>(); + public String complete = null; + public List switches = new ArrayList<>(); + public boolean isSwitch = true; + } + } diff --git a/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java b/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java index 8ad0142ea..2f1223885 100644 --- a/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java +++ b/core/src/main/java/co/aikar/commands/annotation/CommandCompletion.java @@ -36,10 +36,13 @@ * or special @codes that let you define Completion Handlers to dynamically * populate completion values. * + * If used on a parameter it provides completion(s) for that parameter and will not + * consume any provided on the Method + * * @see {@link co.aikar.commands.CommandCompletions} */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD}) +@Target({ElementType.METHOD, ElementType.PARAMETER}) public @interface CommandCompletion { String value(); } diff --git a/core/src/main/java/co/aikar/commands/annotation/Switch.java b/core/src/main/java/co/aikar/commands/annotation/Switch.java new file mode 100644 index 000000000..1dc38b0b9 --- /dev/null +++ b/core/src/main/java/co/aikar/commands/annotation/Switch.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2016-2018 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.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Sets the name that the parameter can be accessed with + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.PARAMETER}) +public @interface Switch { + String value(); +}