Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Named Parameters and Per Parameter Tab Completion #178

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions core/src/main/java/co/aikar/commands/BaseCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,6 @@ public List<String> tabComplete(CommandIssuer issuer, String commandLabel, Strin

final CommandSearch search = findSubCommand(args, true);


final List<String> cmds = new ArrayList<>();

if (search != null) {
Expand Down Expand Up @@ -772,7 +771,7 @@ List<String> getCommandsForCompletion(CommandIssuer issuer, String[] args) {
* @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 == 0 || cmd.complete == null) {
return Collections.emptyList();
}

Expand Down
17 changes: 9 additions & 8 deletions core/src/main/java/co/aikar/commands/CommandCompletions.java
Original file line number Diff line number Diff line change
Expand Up @@ -170,20 +170,21 @@ CommandCompletionHandler setDefaultCompletion(String id, Class... classes) {

@NotNull
List<String> 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<String> getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args, boolean isAsync) {
Expand Down
39 changes: 37 additions & 2 deletions core/src/main/java/co/aikar/commands/CommandParameter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -52,6 +55,8 @@ public class CommandParameter <CEC extends CommandExecutionContext<CEC, ? extend
private String defaultValue;
private String syntax;
private String conditions;
private String complete;
private String switches;
private boolean requiresInput;
private boolean commandIssuer;
private String[] values;
Expand All @@ -71,6 +76,8 @@ public CommandParameter(RegisteredCommand<CEC> 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);
Expand All @@ -94,9 +101,17 @@ public CommandParameter(RegisteredCommand<CEC> 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 + ">";
}
}
}
}
Expand Down Expand Up @@ -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);
}
}
151 changes: 141 additions & 10 deletions core/src/main/java/co/aikar/commands/RegisteredCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,12 @@ void invoke(CommandIssuer sender, List<String> args, CommandOperationContext con
try {
this.manager.conditions.validateConditions(context);
Map<String, Object> 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);
}
Expand Down Expand Up @@ -196,15 +199,17 @@ Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args) thr
}
@Nullable
Map<String, Object> resolveContexts(CommandIssuer sender, List<String> 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<String> arguments = new ArrayList<>(commandData.args);

String[] origArgs = arguments.toArray(new String[0]);
Map<String, Object> 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<CEC> parameter = parameters[i];
final CommandParameter parameter = commandData.parameters.get(i);
if (parameter.isCommandIssuer()) {
argLimit++;
}
Expand All @@ -213,14 +218,14 @@ Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args, int
//noinspection unchecked
final ContextResolver<?, CEC> 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()) {
Expand All @@ -237,7 +242,7 @@ Map<String, Object> resolveContexts(CommandIssuer sender, List<String> args, int
}
}
if (parameter.getValues() != null) {
String arg = !args.isEmpty() ? args.get(0) : "";
String arg = !arguments.isEmpty() ? arguments.get(0) : "";

Set<String> possible = new HashSet<>();
CommandCompletions commandCompletions = this.manager.getCommandCompletions();
Expand Down Expand Up @@ -318,4 +323,130 @@ public void addSubcommand(String cmd) {
public void addSubcommands(Collection<String> cmd) {
this.registeredSubcommands.addAll(cmd);
}

/**
* Resolve arguments and parameters with respect to positional and named parameters
*/
public CommandData parseArguments(String[] args) {

// Command completions
List<String> completions = new ArrayList<>();
if (complete != null) {
completions.addAll(Arrays.asList(ACFPatterns.SPACE.split(complete)));
}

// List of all parameters
List<CommandParameter> parameters = Arrays.stream(this.parameters).collect(Collectors.toList());

// Return data
CommandData data = new CommandData();

// All Arguments
List<String> 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<CommandParameter> parameters = new ArrayList<>();
public List<String> args = new ArrayList<>();
public String complete = null;
public List<String> switches = new ArrayList<>();
public boolean isSwitch = true;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
38 changes: 38 additions & 0 deletions core/src/main/java/co/aikar/commands/annotation/Switch.java
Original file line number Diff line number Diff line change
@@ -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();
}