Initial progress towards on a more advanced help system

This commit is contained in:
Alexander Söderberg 2020-09-21 15:21:56 +02:00
parent c38247b3ad
commit a50b36e41f
No known key found for this signature in database
GPG key ID: C0207FF7EA146678
3 changed files with 363 additions and 39 deletions

View file

@ -30,9 +30,11 @@ import com.intellectualsites.commands.meta.CommandMeta;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
@ -45,7 +47,7 @@ import java.util.function.Consumer;
@SuppressWarnings("unused")
public class Command<C> {
@Nonnull private final List<CommandArgument<C, ?>> arguments;
@Nonnull private final Map<CommandArgument<C, ?>, String> arguments;
@Nonnull private final CommandExecutionHandler<C> commandExecutionHandler;
@Nullable private final Class<? extends C> senderType;
@Nonnull private final String commandPermission;
@ -54,13 +56,13 @@ public class Command<C> {
/**
* Construct a new command
*
* @param commandArguments Command arguments
* @param commandArguments Command argument and description pairs
* @param commandExecutionHandler Execution handler
* @param senderType Required sender type. May be {@code null}
* @param commandPermission Command permission
* @param commandMeta Command meta instance
*/
public Command(@Nonnull final List<CommandArgument<C, ?>> commandArguments,
public Command(@Nonnull final Map<CommandArgument<C, ?>, String> commandArguments,
@Nonnull final CommandExecutionHandler<C> commandExecutionHandler,
@Nullable final Class<? extends C> senderType,
@Nonnull final String commandPermission,
@ -71,7 +73,7 @@ public class Command<C> {
}
// Enforce ordering of command arguments
boolean foundOptional = false;
for (final CommandArgument<C, ?> argument : this.arguments) {
for (final CommandArgument<C, ?> argument : this.arguments.keySet()) {
if (argument.getName().isEmpty()) {
throw new IllegalArgumentException("Argument names may not be empty");
}
@ -97,7 +99,7 @@ public class Command<C> {
* @param senderType Required sender type. May be {@code null}
* @param commandMeta Command meta instance
*/
public Command(@Nonnull final List<CommandArgument<C, ?>> commandArguments,
public Command(@Nonnull final Map<CommandArgument<C, ?>, String> commandArguments,
@Nonnull final CommandExecutionHandler<C> commandExecutionHandler,
@Nullable final Class<? extends C> senderType,
@Nonnull final CommandMeta commandMeta) {
@ -112,7 +114,7 @@ public class Command<C> {
* @param commandPermission Command permission
* @param commandMeta Command meta instance
*/
public Command(@Nonnull final List<CommandArgument<C, ?>> commandArguments,
public Command(@Nonnull final Map<CommandArgument<C, ?>, String> commandArguments,
@Nonnull final CommandExecutionHandler<C> commandExecutionHandler,
@Nonnull final String commandPermission,
@Nonnull final CommandMeta commandMeta) {
@ -133,8 +135,9 @@ public class Command<C> {
public static <C> Builder<C> newBuilder(@Nonnull final String commandName,
@Nonnull final CommandMeta commandMeta,
@Nonnull final String... aliases) {
return new Builder<>(null, commandMeta, null,
Collections.singletonList(StaticArgument.required(commandName, aliases)),
final Map<CommandArgument<C, ?>, String> map = new LinkedHashMap<>();
map.put(StaticArgument.required(commandName, aliases), "");
return new Builder<>(null, commandMeta, null, map,
new CommandExecutionHandler.NullCommandExecutionHandler<>(), "");
}
@ -145,7 +148,7 @@ public class Command<C> {
*/
@Nonnull
public List<CommandArgument<C, ?>> getArguments() {
return Collections.unmodifiableList(this.arguments);
return new ArrayList<>(this.arguments.keySet());
}
/**
@ -189,22 +192,14 @@ public class Command<C> {
}
/**
* Get the longest chain of similar arguments for
* two commands
* Get the description for an argument
*
* @param other Command to compare to
* @return List containing the longest shared argument chain
* @param argument Argument
* @return Argument description
*/
public List<CommandArgument<C, ?>> getSharedArgumentChain(@Nonnull final Command<C> other) {
final List<CommandArgument<C, ?>> commandArguments = new LinkedList<>();
for (int i = 0; i < this.arguments.size() && i < other.arguments.size(); i++) {
if (this.arguments.get(i).equals(other.arguments.get(i))) {
commandArguments.add(this.arguments.get(i));
} else {
break;
}
}
return commandArguments;
@Nonnull
public String getArgumentDescription(@Nonnull final CommandArgument<C, ?> argument) {
return this.arguments.get(argument);
}
@ -217,7 +212,7 @@ public class Command<C> {
public static final class Builder<C> {
@Nonnull private final CommandMeta commandMeta;
@Nonnull private final List<CommandArgument<C, ?>> commandArguments;
@Nonnull private final Map<CommandArgument<C, ?>, String> commandArguments;
@Nonnull private final CommandExecutionHandler<C> commandExecutionHandler;
@Nullable private final Class<? extends C> senderType;
@Nonnull private final String commandPermission;
@ -226,7 +221,7 @@ public class Command<C> {
private Builder(@Nullable final CommandManager<C> commandManager,
@Nonnull final CommandMeta commandMeta,
@Nullable final Class<? extends C> senderType,
@Nonnull final List<CommandArgument<C, ?>> commandArguments,
@Nonnull final Map<CommandArgument<C, ?>, String> commandArguments,
@Nonnull final CommandExecutionHandler<C> commandExecutionHandler,
@Nonnull final String commandPermission) {
this.commandManager = commandManager;
@ -264,7 +259,7 @@ public class Command<C> {
}
/**
* Add a new command argument to the command
* Add a new command argument with an empty description to the command
*
* @param argument Argument to add
* @param <T> Argument type
@ -272,12 +267,26 @@ public class Command<C> {
*/
@Nonnull
public <T> Builder<C> argument(@Nonnull final CommandArgument<C, T> argument) {
final List<CommandArgument<C, ?>> commandArguments = new LinkedList<>(this.commandArguments);
commandArguments.add(argument);
return new Builder<>(this.commandManager, this.commandMeta, this.senderType, commandArguments,
return this.argument(argument, "");
}
/**
* Add a new command argument to the command
*
* @param argument Argument to add
* @param description Argument description
* @param <T> Argument type
* @return New builder instance with the command argument inserted into the argument list
*/
@Nonnull
public <T> Builder<C> argument(@Nonnull final CommandArgument<C, T> argument, @Nonnull final String description) {
final Map<CommandArgument<C, ?>, String> commandArgumentMap = new LinkedHashMap<>(this.commandArguments);
commandArgumentMap.put(argument, description);
return new Builder<>(this.commandManager, this.commandMeta, this.senderType, commandArgumentMap,
this.commandExecutionHandler, this.commandPermission);
}
/**
* Add a new command argument by interacting with a constructed command argument builder
*
@ -342,8 +351,11 @@ public class Command<C> {
*/
@Nonnull
public Command<C> build() {
return new Command<>(Collections.unmodifiableList(this.commandArguments),
this.commandExecutionHandler, this.senderType, this.commandPermission, this.commandMeta);
return new Command<>(Collections.unmodifiableMap(this.commandArguments),
this.commandExecutionHandler,
this.senderType,
this.commandPermission,
this.commandMeta);
}
}

View file

@ -24,11 +24,13 @@
package com.intellectualsites.commands;
import com.intellectualsites.commands.arguments.CommandArgument;
import com.intellectualsites.commands.arguments.StaticArgument;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
public final class CommandHelpHandler<C> {
@ -49,8 +51,11 @@ public final class CommandHelpHandler<C> {
final List<VerboseHelpEntry<C>> syntaxHints = new ArrayList<>();
for (final Command<C> command : this.commandManager.getCommands()) {
final List<CommandArgument<C, ?>> arguments = command.getArguments();
final String description = command.getCommandMeta().getOrDefault("description", "");
syntaxHints.add(new VerboseHelpEntry<>(command,
this.commandManager.getCommandSyntaxFormatter().apply(arguments, null)));
this.commandManager.getCommandSyntaxFormatter()
.apply(arguments, null),
description));
}
syntaxHints.sort(Comparator.comparing(VerboseHelpEntry::getSyntaxString));
return syntaxHints;
@ -67,8 +72,11 @@ public final class CommandHelpHandler<C> {
public List<String> getLongestSharedChains() {
final List<String> chains = new ArrayList<>();
this.commandManager.getCommandTree().getRootNodes().forEach(node ->
chains.add(node.getValue().getName() + this.commandManager.getCommandSyntaxFormatter()
.apply(Collections.emptyList(), node)));
chains.add(node.getValue()
.getName() + this.commandManager.getCommandSyntaxFormatter()
.apply(Collections
.emptyList(),
node)));
chains.sort(String::compareTo);
return chains;
}
@ -78,10 +86,14 @@ public final class CommandHelpHandler<C> {
private final Command<C> command;
private final String syntaxString;
private final String description;
private VerboseHelpEntry(@Nonnull final Command<C> command, @Nonnull final String syntaxString) {
private VerboseHelpEntry(@Nonnull final Command<C> command,
@Nonnull final String syntaxString,
@Nonnull final String description) {
this.command = command;
this.syntaxString = syntaxString;
this.description = description;
}
/**
@ -103,6 +115,222 @@ public final class CommandHelpHandler<C> {
public String getSyntaxString() {
return this.syntaxString;
}
/**
* Get the command description
*
* @return Command description
*/
public String getDescription() {
return this.description;
}
}
/**
* Query for help
*
* @param query Query string
* @return Help topic, will return an empty {@link IndexHelpTopic} if no results were found
*/
public HelpTopic<C> queryHelp(@Nonnull final String query) {
if (query.replace(" ", "").isEmpty()) {
return new IndexHelpTopic<>(this.getAllCommands());
}
final String[] queryFragments = query.split(" ");
final List<VerboseHelpEntry<C>> verboseEntries = this.getAllCommands();
final String rootFragment = queryFragments[0];
/* Determine which command we are querying for */
Command<C> queryCommand = null;
String queryCommandName = "";
outer:
for (final VerboseHelpEntry<C> entry : verboseEntries) {
final Command<C> command = entry.getCommand();
@SuppressWarnings("unchecked") final StaticArgument<C> staticArgument = (StaticArgument<C>) command.getArguments()
.get(0);
for (final String alias : staticArgument.getAliases()) {
if (alias.equalsIgnoreCase(rootFragment)) {
/* We found our command */
queryCommand = command;
queryCommandName = staticArgument.getName();
break outer;
}
}
}
/* No command found, return all possible commands */
if (queryCommand == null) {
return new IndexHelpTopic<>(verboseEntries);
}
/* Traverse command to find the most specific help topic */
final CommandTree.Node<CommandArgument<C, ?>> node = this.commandManager.getCommandTree().getNamedNode(queryCommandName);
final List<CommandArgument<C, ?>> traversedNodes = new LinkedList<>();
CommandTree.Node<CommandArgument<C, ?>> head = node;
int index = 0;
outer: while (head != null) {
++index;
traversedNodes.add(head.getValue());
if (head.isLeaf()) {
return new VerboseHelpTopic<>(head.getValue().getOwningCommand());
} else if (head.getChildren().size() == 1) {
head = head.getChildren().get(0);
} else {
if (index < queryFragments.length) {
/* We might still be able to match an argument */
for (final CommandTree.Node<CommandArgument<C, ?>> child : head.getChildren()) {
final StaticArgument<C> childArgument = (StaticArgument<C>) child.getValue();
for (final String childAlias : childArgument.getAliases()) {
if (childAlias.equalsIgnoreCase(queryFragments[index])) {
head = child;
continue outer;
}
}
}
}
final String currentDescription = this.commandManager.getCommandSyntaxFormatter().apply(traversedNodes, null);
/* Attempt to parse the longest possible description for the children */
final List<String> childSuggestions = new LinkedList<>();
for (final CommandTree.Node<CommandArgument<C, ?>> child : head.getChildren()) {
childSuggestions.add(this.commandManager.getCommandSyntaxFormatter().apply(traversedNodes, child));
}
return new MultiHelpTopic<>(currentDescription, childSuggestions);
}
}
return new IndexHelpTopic<>(Collections.emptyList());
}
/**
* Something that can be returned as the result of a help query
* <p>
* Implementations:
* <ul>
* <li>{@link IndexHelpTopic}</li>
* <li>{@link VerboseHelpTopic}</li>
* <li>{@link MultiHelpTopic}</li>
* </ul>
*
* @param <C> Command sender type
*/
public interface HelpTopic<C> {
}
/**
* Index of available commands
*
* @param <C> Command sender type
*/
public static final class IndexHelpTopic<C> implements HelpTopic<C> {
private final List<VerboseHelpEntry<C>> entries;
private IndexHelpTopic(@Nonnull final List<VerboseHelpEntry<C>> entries) {
this.entries = entries;
}
/**
* Get help entries
*
* @return Entries
*/
@Nonnull
public List<VerboseHelpEntry<C>> getEntries() {
return this.entries;
}
/**
* Check if the help topic is entry
*
* @return {@code true} if the topic is entry, else {@code false}
*/
@Nonnull
public boolean isEmpty() {
return this.getEntries().isEmpty();
}
}
/**
* Verbose information about a specific {@link Command}
*
* @param <C> Command sender type
*/
public static final class VerboseHelpTopic<C> implements HelpTopic<C> {
private final Command<C> command;
private final String description;
private VerboseHelpTopic(@Nonnull final Command<C> command) {
this.command = command;
final String shortDescription = command.getCommandMeta().getOrDefault("description", "No description");
this.description = command.getCommandMeta().getOrDefault("long-description", shortDescription);
}
/**
* Get the command
*
* @return Command
*/
@Nonnull
public Command<C> getCommand() {
return this.command;
}
/**
* Get the command description
*
* @return Command description
*/
@Nonnull
public String getDescription() {
return this.description;
}
}
/**
* Help topic with multiple semi-verbose command descriptions
*
* @param <C> Command sender type
*/
public static final class MultiHelpTopic<C> implements HelpTopic<C> {
private final String longestPath;
private final List<String> childSuggestions;
private MultiHelpTopic(@Nonnull final String longestPath, @Nonnull final List<String> childSuggestions) {
this.longestPath = longestPath;
this.childSuggestions = childSuggestions;
}
/**
* Get the longest shared path
*
* @return Longest path
*/
@Nonnull
public String getLongestPath() {
return this.longestPath;
}
/**
* Get syntax hints for the node's children
*
* @return Child suggestions
*/
@Nonnull
public List<String> getChildSuggestions() {
return this.childSuggestions;
}
}
}

View file

@ -23,12 +23,17 @@
//
package com.intellectualsites.commands;
import com.intellectualsites.commands.arguments.CommandArgument;
import com.intellectualsites.commands.arguments.standard.IntegerArgument;
import com.intellectualsites.commands.meta.SimpleCommandMeta;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import javax.annotation.Nonnull;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
class CommandHelpHandlerTest {
@ -38,9 +43,11 @@ class CommandHelpHandlerTest {
@BeforeAll
static void setup() {
manager = new TestCommandManager();
manager.command(manager.commandBuilder("test").literal("this").literal("thing").build());
manager.command(manager.commandBuilder("test").literal("int").
argument(IntegerArgument.required("int")).build());
final SimpleCommandMeta meta1 = SimpleCommandMeta.builder().with("description", "Command with only literals").build();
manager.command(manager.commandBuilder("test", meta1).literal("this").literal("thing").build());
final SimpleCommandMeta meta2 = SimpleCommandMeta.builder().with("description", "Command with variables").build();
manager.command(manager.commandBuilder("test", meta2).literal("int").
argument(IntegerArgument.required("int"), "A number").build());
}
@Test
@ -59,4 +66,81 @@ class CommandHelpHandlerTest {
Assertions.assertEquals(Arrays.asList("test int|this"), longestChains);
}
@Test
void testHelpQuery() {
final CommandHelpHandler.HelpTopic<TestCommandSender> query1 = manager.getCommandHelpHandler().queryHelp("");
Assertions.assertTrue(query1 instanceof CommandHelpHandler.IndexHelpTopic);
this.printTopic("", query1);
final CommandHelpHandler.HelpTopic<TestCommandSender> query2 = manager.getCommandHelpHandler().queryHelp("test");
Assertions.assertTrue(query2 instanceof CommandHelpHandler.MultiHelpTopic);
this.printTopic("test", query2);
final CommandHelpHandler.HelpTopic<TestCommandSender> query3 = manager.getCommandHelpHandler().queryHelp("test int");
Assertions.assertTrue(query3 instanceof CommandHelpHandler.VerboseHelpTopic);
this.printTopic("test int", query3);
}
private void printTopic(@Nonnull final String query,
@Nonnull final CommandHelpHandler.HelpTopic<TestCommandSender> helpTopic) {
System.out.printf("Showing results for query: \"/%s\"\n", query);
if (helpTopic instanceof CommandHelpHandler.IndexHelpTopic) {
this.printIndexHelpTopic((CommandHelpHandler.IndexHelpTopic<TestCommandSender>) helpTopic);
} else if (helpTopic instanceof CommandHelpHandler.MultiHelpTopic) {
this.printMultiHelpTopic((CommandHelpHandler.MultiHelpTopic<TestCommandSender>) helpTopic);
} else if (helpTopic instanceof CommandHelpHandler.VerboseHelpTopic) {
this.printVerboseHelpTopic((CommandHelpHandler.VerboseHelpTopic<TestCommandSender>) helpTopic);
} else {
throw new IllegalArgumentException("Unknown help topic type");
}
System.out.println();
}
private void printIndexHelpTopic(@Nonnull final CommandHelpHandler.IndexHelpTopic<TestCommandSender> helpTopic) {
System.out.println("└── Available Commands: ");
final Iterator<CommandHelpHandler.VerboseHelpEntry<TestCommandSender>> iterator = helpTopic.getEntries().iterator();
while (iterator.hasNext()) {
final CommandHelpHandler.VerboseHelpEntry<TestCommandSender> entry = iterator.next();
final String prefix = iterator.hasNext() ? "├──" : "└──";
System.out.printf(" %s %s: %s\n", prefix, entry.getSyntaxString(), entry.getDescription());
}
}
private void printMultiHelpTopic(@Nonnull final CommandHelpHandler.MultiHelpTopic<TestCommandSender> helpTopic) {
System.out.printf("└── /%s\n", helpTopic.getLongestPath());
final int headerIndentation = helpTopic.getLongestPath().length();
final Iterator<String> iterator = helpTopic.getChildSuggestions().iterator();
while (iterator.hasNext()) {
final String suggestion = iterator.next();
final StringBuilder printBuilder = new StringBuilder();
for (int i = 0; i < headerIndentation; i++) {
printBuilder.append(' ');
}
if (iterator.hasNext()) {
printBuilder.append("├── ");
} else {
printBuilder.append("└── ");
}
printBuilder.append(suggestion);
System.out.println(printBuilder.toString());
}
}
private void printVerboseHelpTopic(@Nonnull final CommandHelpHandler.VerboseHelpTopic<TestCommandSender> helpTopic) {
System.out.printf("└── Command: /%s\n", manager.getCommandSyntaxFormatter()
.apply(helpTopic.getCommand().getArguments(), null));
System.out.printf(" ├── Description: %s\n", helpTopic.getDescription());
System.out.println(" └── Args: ");
final Iterator<CommandArgument<TestCommandSender, ?>> iterator = helpTopic.getCommand().getArguments().iterator();
while (iterator.hasNext()) {
final CommandArgument<TestCommandSender, ?> argument = iterator.next();
String description = helpTopic.getCommand().getArgumentDescription(argument);
if (!description.isEmpty()) {
description = ": " + description;
}
System.out.printf(" %s %s%s\n", iterator.hasNext() ? "├──" : "└──", manager.getCommandSyntaxFormatter().apply(
Collections.singletonList(argument), null), description);
}
}
}