From a50b36e41f6ffa419fccd32d20d1a84b1d6f21aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20S=C3=B6derberg?= Date: Mon, 21 Sep 2020 15:21:56 +0200 Subject: [PATCH] Initial progress towards on a more advanced help system --- .../intellectualsites/commands/Command.java | 76 +++--- .../commands/CommandHelpHandler.java | 236 +++++++++++++++++- .../commands/CommandHelpHandlerTest.java | 90 ++++++- 3 files changed, 363 insertions(+), 39 deletions(-) diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/Command.java b/cloud-core/src/main/java/com/intellectualsites/commands/Command.java index bb7d16e2..8d1f0b58 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/Command.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/Command.java @@ -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 { - @Nonnull private final List> arguments; + @Nonnull private final Map, String> arguments; @Nonnull private final CommandExecutionHandler commandExecutionHandler; @Nullable private final Class senderType; @Nonnull private final String commandPermission; @@ -54,13 +56,13 @@ public class Command { /** * 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> commandArguments, + public Command(@Nonnull final Map, String> commandArguments, @Nonnull final CommandExecutionHandler commandExecutionHandler, @Nullable final Class senderType, @Nonnull final String commandPermission, @@ -71,7 +73,7 @@ public class Command { } // Enforce ordering of command arguments boolean foundOptional = false; - for (final CommandArgument argument : this.arguments) { + for (final CommandArgument argument : this.arguments.keySet()) { if (argument.getName().isEmpty()) { throw new IllegalArgumentException("Argument names may not be empty"); } @@ -97,7 +99,7 @@ public class Command { * @param senderType Required sender type. May be {@code null} * @param commandMeta Command meta instance */ - public Command(@Nonnull final List> commandArguments, + public Command(@Nonnull final Map, String> commandArguments, @Nonnull final CommandExecutionHandler commandExecutionHandler, @Nullable final Class senderType, @Nonnull final CommandMeta commandMeta) { @@ -112,7 +114,7 @@ public class Command { * @param commandPermission Command permission * @param commandMeta Command meta instance */ - public Command(@Nonnull final List> commandArguments, + public Command(@Nonnull final Map, String> commandArguments, @Nonnull final CommandExecutionHandler commandExecutionHandler, @Nonnull final String commandPermission, @Nonnull final CommandMeta commandMeta) { @@ -133,8 +135,9 @@ public class Command { public static Builder 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, 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 { */ @Nonnull public List> getArguments() { - return Collections.unmodifiableList(this.arguments); + return new ArrayList<>(this.arguments.keySet()); } /** @@ -189,22 +192,14 @@ public class Command { } /** - * 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> getSharedArgumentChain(@Nonnull final Command other) { - final List> 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 argument) { + return this.arguments.get(argument); } @@ -217,7 +212,7 @@ public class Command { public static final class Builder { @Nonnull private final CommandMeta commandMeta; - @Nonnull private final List> commandArguments; + @Nonnull private final Map, String> commandArguments; @Nonnull private final CommandExecutionHandler commandExecutionHandler; @Nullable private final Class senderType; @Nonnull private final String commandPermission; @@ -226,7 +221,7 @@ public class Command { private Builder(@Nullable final CommandManager commandManager, @Nonnull final CommandMeta commandMeta, @Nullable final Class senderType, - @Nonnull final List> commandArguments, + @Nonnull final Map, String> commandArguments, @Nonnull final CommandExecutionHandler commandExecutionHandler, @Nonnull final String commandPermission) { this.commandManager = commandManager; @@ -264,7 +259,7 @@ public class Command { } /** - * 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 Argument type @@ -272,12 +267,26 @@ public class Command { */ @Nonnull public Builder argument(@Nonnull final CommandArgument argument) { - final List> 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 Argument type + * @return New builder instance with the command argument inserted into the argument list + */ + @Nonnull + public Builder argument(@Nonnull final CommandArgument argument, @Nonnull final String description) { + final Map, 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 { */ @Nonnull public Command 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); } } diff --git a/cloud-core/src/main/java/com/intellectualsites/commands/CommandHelpHandler.java b/cloud-core/src/main/java/com/intellectualsites/commands/CommandHelpHandler.java index 177367fa..3b2bdfb6 100644 --- a/cloud-core/src/main/java/com/intellectualsites/commands/CommandHelpHandler.java +++ b/cloud-core/src/main/java/com/intellectualsites/commands/CommandHelpHandler.java @@ -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 { @@ -49,8 +51,11 @@ public final class CommandHelpHandler { final List> syntaxHints = new ArrayList<>(); for (final Command command : this.commandManager.getCommands()) { final List> 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 { public List getLongestSharedChains() { final List 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 { private final Command command; private final String syntaxString; + private final String description; - private VerboseHelpEntry(@Nonnull final Command command, @Nonnull final String syntaxString) { + private VerboseHelpEntry(@Nonnull final Command 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 { 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 queryHelp(@Nonnull final String query) { + if (query.replace(" ", "").isEmpty()) { + return new IndexHelpTopic<>(this.getAllCommands()); + } + + final String[] queryFragments = query.split(" "); + final List> verboseEntries = this.getAllCommands(); + final String rootFragment = queryFragments[0]; + + /* Determine which command we are querying for */ + Command queryCommand = null; + String queryCommandName = ""; + + outer: + for (final VerboseHelpEntry entry : verboseEntries) { + final Command command = entry.getCommand(); + @SuppressWarnings("unchecked") final StaticArgument staticArgument = (StaticArgument) 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> node = this.commandManager.getCommandTree().getNamedNode(queryCommandName); + + final List> traversedNodes = new LinkedList<>(); + CommandTree.Node> 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> child : head.getChildren()) { + final StaticArgument childArgument = (StaticArgument) 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 childSuggestions = new LinkedList<>(); + for (final CommandTree.Node> 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 + *

+ * Implementations: + *

    + *
  • {@link IndexHelpTopic}
  • + *
  • {@link VerboseHelpTopic}
  • + *
  • {@link MultiHelpTopic}
  • + *
+ * + * @param Command sender type + */ + public interface HelpTopic { + } + + + /** + * Index of available commands + * + * @param Command sender type + */ + public static final class IndexHelpTopic implements HelpTopic { + + private final List> entries; + + private IndexHelpTopic(@Nonnull final List> entries) { + this.entries = entries; + } + + /** + * Get help entries + * + * @return Entries + */ + @Nonnull + public List> 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 Command sender type + */ + public static final class VerboseHelpTopic implements HelpTopic { + + private final Command command; + private final String description; + + private VerboseHelpTopic(@Nonnull final Command 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 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 Command sender type + */ + public static final class MultiHelpTopic implements HelpTopic { + + private final String longestPath; + private final List childSuggestions; + + private MultiHelpTopic(@Nonnull final String longestPath, @Nonnull final List 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 getChildSuggestions() { + return this.childSuggestions; + } + } } diff --git a/cloud-core/src/test/java/com/intellectualsites/commands/CommandHelpHandlerTest.java b/cloud-core/src/test/java/com/intellectualsites/commands/CommandHelpHandlerTest.java index 8fbca513..57a5db1b 100644 --- a/cloud-core/src/test/java/com/intellectualsites/commands/CommandHelpHandlerTest.java +++ b/cloud-core/src/test/java/com/intellectualsites/commands/CommandHelpHandlerTest.java @@ -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 query1 = manager.getCommandHelpHandler().queryHelp(""); + Assertions.assertTrue(query1 instanceof CommandHelpHandler.IndexHelpTopic); + this.printTopic("", query1); + final CommandHelpHandler.HelpTopic query2 = manager.getCommandHelpHandler().queryHelp("test"); + Assertions.assertTrue(query2 instanceof CommandHelpHandler.MultiHelpTopic); + this.printTopic("test", query2); + final CommandHelpHandler.HelpTopic 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 helpTopic) { + System.out.printf("Showing results for query: \"/%s\"\n", query); + if (helpTopic instanceof CommandHelpHandler.IndexHelpTopic) { + this.printIndexHelpTopic((CommandHelpHandler.IndexHelpTopic) helpTopic); + } else if (helpTopic instanceof CommandHelpHandler.MultiHelpTopic) { + this.printMultiHelpTopic((CommandHelpHandler.MultiHelpTopic) helpTopic); + } else if (helpTopic instanceof CommandHelpHandler.VerboseHelpTopic) { + this.printVerboseHelpTopic((CommandHelpHandler.VerboseHelpTopic) helpTopic); + } else { + throw new IllegalArgumentException("Unknown help topic type"); + } + System.out.println(); + } + + private void printIndexHelpTopic(@Nonnull final CommandHelpHandler.IndexHelpTopic helpTopic) { + System.out.println("└── Available Commands: "); + final Iterator> iterator = helpTopic.getEntries().iterator(); + while (iterator.hasNext()) { + final CommandHelpHandler.VerboseHelpEntry 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 helpTopic) { + System.out.printf("└── /%s\n", helpTopic.getLongestPath()); + final int headerIndentation = helpTopic.getLongestPath().length(); + final Iterator 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 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> iterator = helpTopic.getCommand().getArguments().iterator(); + while (iterator.hasNext()) { + final CommandArgument 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); + } + } + }