Skip to content

Commit

Permalink
Support flags in any position
Browse files Browse the repository at this point in the history
Signed-off-by: Pablete1234 <[email protected]>
  • Loading branch information
Pablete1234 committed Nov 19, 2022
1 parent e471919 commit 4c07994
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 68 deletions.
36 changes: 36 additions & 0 deletions cloud-core/src/main/java/cloud/commandframework/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public class Command<C> {

private final List<@NonNull CommandComponent<C>> components;
private final List<@NonNull CommandArgument<C, ?>> arguments;
private final @Nullable FlagArgument<C> flagArgument;
private final CommandExecutionHandler<C> commandExecutionHandler;
private final Class<? extends C> senderType;
private final CommandPermission commandPermission;
Expand All @@ -78,6 +79,7 @@ public class Command<C> {
* @since 1.3.0
*/
@API(status = API.Status.STABLE, since = "1.3.0")
@SuppressWarnings("unchecked")
public Command(
final @NonNull List<@NonNull CommandComponent<C>> commandComponents,
final @NonNull CommandExecutionHandler<@NonNull C> commandExecutionHandler,
Expand All @@ -90,6 +92,14 @@ public Command(
if (this.components.isEmpty()) {
throw new IllegalArgumentException("At least one command component is required");
}

this.flagArgument =
this.arguments.stream()
.filter(ca -> ca instanceof FlagArgument)
.map(ca -> (FlagArgument<C>) ca)
.findFirst()
.orElse(null);

// Enforce ordering of command arguments
boolean foundOptional = false;
for (final CommandArgument<C, ?> argument : this.arguments) {
Expand Down Expand Up @@ -318,6 +328,32 @@ public Command(
return new ArrayList<>(this.arguments);
}

/**
* Return a mutable copy of the command arguments, ignoring flag arguments.
*
* @return argument list
* @since 1.8.0
*/
@API(status = API.Status.EXPERIMENTAL, since = "1.8.0")
public @NonNull List<CommandArgument<@NonNull C, @NonNull ?>> nonFlagArguments() {
List<CommandArgument<C, ?>> arguments = new ArrayList<>(this.arguments);
if (this.flagArgument != null) {
arguments.remove(this.flagArgument);
}
return arguments;
}

/**
* Returns the flag argument for this command, or null if no flags are supported.
*
* @return flag argument or null
* @since 1.8.0
*/
@API(status = API.Status.EXPERIMENTAL, since = "1.8.0")
public @Nullable FlagArgument<@NonNull C> flagArgument() {
return this.flagArgument;
}

/**
* Returns a copy of the command component array
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1417,7 +1417,17 @@ public enum ManagerSettings {
* @since 1.2.0
*/
@API(status = API.Status.STABLE, since = "1.2.0")
OVERRIDE_EXISTING_COMMANDS
OVERRIDE_EXISTING_COMMANDS,

/**
* Allows parsing flags at any position by appending flag argument nodes between each command node.
* It can have some conflicts when integrating with other command systems like Brigadier,
* and code inspecting the command tree may need to be adjusted.
*
* @since 1.8.0
*/
@API(status = API.Status.EXPERIMENTAL, since = "1.8.0")
ALLOW_FLAGS_EVERYWHERE
}


Expand Down
77 changes: 66 additions & 11 deletions cloud-core/src/main/java/cloud/commandframework/CommandTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,8 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
));
}
if (child.getValue() != null) {
if (commandQueue.isEmpty()) {
// Flag arguments need to be skipped over, so that further defaults are handled
if (commandQueue.isEmpty() && !(child.getValue() instanceof FlagArgument)) {
if (child.getValue().hasDefaultValue()) {
commandQueue.add(child.getValue().getDefaultValue());
} else if (!child.getValue().isRequired()) {
Expand Down Expand Up @@ -603,14 +604,14 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
* Use the flag argument parser to deduce what flag is being suggested right now
* If empty, then no flag value is being typed, and the different flag options should
* be suggested instead.
*
* Note: the method parseCurrentFlag() will remove all but the last element from
* the queue!
*/
@SuppressWarnings("unchecked")
FlagArgument.FlagArgumentParser<C> parser = (FlagArgument.FlagArgumentParser<C>) child.getValue().getParser();
Optional<String> lastFlag = parser.parseCurrentFlag(commandContext, commandQueue);
lastFlag.ifPresent(s -> commandContext.store(FlagArgument.FLAG_META_KEY, s));
if (!lastFlag.isPresent()) {
commandContext.remove(FlagArgument.FLAG_META_KEY);
}
} else if (GenericTypeReflector.erase(child.getValue().getValueType().getType()).isArray()) {
while (commandQueue.size() > 1) {
commandQueue.remove();
Expand All @@ -628,8 +629,7 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
if (commandQueue.isEmpty()) {
return Collections.emptyList();
} else if (child.isLeaf() && commandQueue.size() < 2) {
commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.peek());
return this.directSuggestions(commandContext, child, commandQueue.peek());
} else if (child.isLeaf()) {
if (child.getValue() instanceof CompoundArgument) {
final String last = ((LinkedList<String>) commandQueue).getLast();
Expand All @@ -638,8 +638,7 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
}
return Collections.emptyList();
} else if (commandQueue.peek().isEmpty()) {
commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, commandQueue.remove());
return this.directSuggestions(commandContext, child, commandQueue.peek());
}

// Store original input command queue before the parsers below modify it
Expand Down Expand Up @@ -670,8 +669,7 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
commandQueue.addAll(commandQueueOriginal);

// Fallback: use suggestion provider of argument
commandContext.setCurrentArgument(child.getValue());
return child.getValue().getSuggestionsProvider().apply(commandContext, this.stringOrEmpty(commandQueue.peek()));
return this.directSuggestions(commandContext, child, commandQueue.peek());
}

private @NonNull String stringOrEmpty(final @Nullable String string) {
Expand All @@ -681,6 +679,31 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
return string;
}

private @NonNull List<@NonNull String> directSuggestions(
final @NonNull CommandContext<C> commandContext,
final @NonNull Node<@NonNull CommandArgument<C, ?>> current,
final @NonNull String text) {
CommandArgument<C, ?> argument = Objects.requireNonNull(current.getValue());

commandContext.setCurrentArgument(argument);
List<String> suggestions = argument.getSuggestionsProvider().apply(commandContext, text);

// When suggesting a flag, potentially suggest following nodes too
if (argument instanceof FlagArgument
&& !current.getChildren().isEmpty() // Has children
&& !text.startsWith("-") // Not a flag
&& !commandContext.getOptional(FlagArgument.FLAG_META_KEY).isPresent()) {
suggestions = new ArrayList<>(suggestions);
for (final Node<CommandArgument<C, ?>> child : current.getChildren()) {
argument = Objects.requireNonNull(child.getValue());
commandContext.setCurrentArgument(argument);
suggestions.addAll(argument.getSuggestionsProvider().apply(commandContext, text));
}
}

return suggestions;
}

/**
* Insert a new command into the command tree
*
Expand All @@ -690,7 +713,15 @@ private CommandTree(final @NonNull CommandManager<C> commandManager) {
public void insertCommand(final @NonNull Command<C> command) {
synchronized (this.commandLock) {
Node<CommandArgument<C, ?>> node = this.internalTree;
for (final CommandArgument<C, ?> argument : command.getArguments()) {
FlagArgument<C> flags = command.flagArgument();

List<CommandArgument<C, ?>> nonFlagArguments = command.nonFlagArguments();

int flagStartIdx = this.flagStartIndex(nonFlagArguments, flags);

for (int i = 0; i < nonFlagArguments.size(); i++) {
final CommandArgument<C, ?> argument = nonFlagArguments.get(i);

Node<CommandArgument<C, ?>> tempNode = node.getChild(argument);
if (tempNode == null) {
tempNode = node.addChild(argument);
Expand All @@ -704,7 +735,12 @@ public void insertCommand(final @NonNull Command<C> command) {
}
tempNode.setParent(node);
node = tempNode;

if (i >= flagStartIdx) {
node = node.addChild(flags);
}
}

if (node.getValue() != null) {
if (node.getValue().getOwningCommand() != null) {
throw new IllegalStateException(String.format(
Expand All @@ -719,6 +755,25 @@ public void insertCommand(final @NonNull Command<C> command) {
}
}

private int flagStartIndex(final List<CommandArgument<C, ?>> arguments, final FlagArgument<C> flags) {
// Do not append flags
if (flags == null) {
return Integer.MAX_VALUE;
}

// Append flags before the first non-static argument
if (this.commandManager.getSetting(CommandManager.ManagerSettings.ALLOW_FLAGS_EVERYWHERE)) {
for (int i = 1; i < arguments.size(); i++) {
if (!(arguments.get(i) instanceof StaticArgument)) {
return i - 1;
}
}
}

// Append flags after the last argument
return arguments.size() - 1;
}

private @Nullable CommandPermission isPermitted(
final @NonNull C sender,
final @NonNull Node<@Nullable CommandArgument<C, ?>> node
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ public final class FlagArgument<C> extends CommandArgument<C, Object> {
*/
public static final CloudKey<String> FLAG_META_KEY = SimpleCloudKey.of("__last_flag__", TypeToken.get(String.class));

/**
* Meta data for the set of parsed flags, used to detect duplicates.
* @since 1.8.0
*/
@API(status = API.Status.EXPERIMENTAL, since = "1.8.0")
public static final CloudKey<Set<CommandFlag<?>>> PARSED_FLAGS = SimpleCloudKey.of("__parsed_flags__",
new TypeToken<Set<CommandFlag<?>>>(){});

private static final String FLAG_ARGUMENT_NAME = "flags";

private final Collection<@NonNull CommandFlag<?>> flags;
Expand Down Expand Up @@ -157,16 +165,11 @@ private FlagArgumentParser(final @NonNull CommandFlag<?>[] flags) {
parser.parse(commandContext, inputQueue);

/*
* Remove all but the last element from the command input queue
* If the parser parsed the entire queue, restore the last typed
* input obtained earlier.
*/
if (inputQueue.isEmpty()) {
inputQueue.add(lastInputValue);
} else {
while (inputQueue.size() > 1) {
inputQueue.remove();
}
}

/*
Expand Down Expand Up @@ -309,11 +312,8 @@ private class FlagParser {
final @NonNull CommandContext<@NonNull C> commandContext,
final @NonNull Queue<@NonNull String> inputQueue
) {
/*
This argument must necessarily be the last so we can just consume all remaining input. This argument type
is similar to a greedy string in that sense. But, we need to keep all flag logic contained to the parser
*/
final Set<CommandFlag<?>> parsedFlags = new HashSet<>();
Set<CommandFlag<?>> parsedFlags = commandContext.computeIfAbsent(PARSED_FLAGS, k -> new HashSet());

CommandFlag<?> currentFlag = null;
String currentFlagName = null;

Expand All @@ -323,8 +323,12 @@ private class FlagParser {
this.currentFlagBeingParsed = Optional.empty();
this.currentFlagNameBeingParsed = Optional.empty();

/* Parse next flag name to set */
if (string.startsWith("-") && currentFlag == null) {
if (!string.startsWith("-") && currentFlag == null) {
/* Not flag waiting to be parsed */
return ArgumentParseResult.success(FLAG_PARSE_RESULT_OBJECT);
} else if (currentFlag == null) {
/* Parse next flag name to set */

/* Remove flag argument from input queue */
inputQueue.poll();

Expand Down Expand Up @@ -418,43 +422,35 @@ private class FlagParser {
currentFlag = null;
}
} else {
if (currentFlag == null) {
/* Mark this flag as the one currently being typed */
this.currentFlagBeingParsed = Optional.of(currentFlag);
this.currentFlagNameBeingParsed = Optional.of(currentFlagName);

// Don't attempt to parse empty strings
if (inputQueue.peek().isEmpty()) {
return ArgumentParseResult.failure(new FlagParseException(
string,
FailureReason.NO_FLAG_STARTED,
currentFlag.getName(),
FailureReason.MISSING_ARGUMENT,
commandContext
));
} else {
/* Mark this flag as the one currently being typed */
this.currentFlagBeingParsed = Optional.of(currentFlag);
this.currentFlagNameBeingParsed = Optional.of(currentFlagName);

// Don't attempt to parse empty strings
if (inputQueue.peek().isEmpty()) {
return ArgumentParseResult.failure(new FlagParseException(
currentFlag.getName(),
FailureReason.MISSING_ARGUMENT,
commandContext
));
}
}

final ArgumentParseResult<?> result =
((CommandArgument) currentFlag.getCommandArgument())
.getParser()
.parse(
commandContext,
inputQueue
);
if (result.getFailure().isPresent()) {
return ArgumentParseResult.failure(result.getFailure().get());
} else if (result.getParsedValue().isPresent()) {
final CommandFlag erasedFlag = currentFlag;
final Object value = result.getParsedValue().get();
commandContext.flags().addValueFlag(erasedFlag, value);
currentFlag = null;
} else {
throw new IllegalStateException("Neither result or value were present. Panicking.");
}
final ArgumentParseResult<?> result =
((CommandArgument) currentFlag.getCommandArgument())
.getParser()
.parse(
commandContext,
inputQueue
);
if (result.getFailure().isPresent()) {
return ArgumentParseResult.failure(result.getFailure().get());
} else if (result.getParsedValue().isPresent()) {
final CommandFlag erasedFlag = currentFlag;
final Object value = result.getParsedValue().get();
commandContext.flags().addValueFlag(erasedFlag, value);
currentFlag = null;
} else {
throw new IllegalStateException("Neither result or value were present. Panicking.");
}
}
}
Expand Down
Loading

0 comments on commit 4c07994

Please sign in to comment.