Brigadier Suggestions

As described in The ArgumentSuggestions interface, the ArgumentSuggestions interface has the following default method:

@FunctionalInterface
public interface ArgumentSuggestions<CommandSender> {

    /**
     * Create a {@link CompletableFuture} resolving onto a brigadier {@link Suggestions} object.
     * @param info The suggestions info
     * @param builder The Brigadier {@link SuggestionsBuilder} object
     * @return a {@link CompletableFuture} resolving onto a brigadier {@link Suggestions} object.
     *
     * @throws CommandSyntaxException if there is an error making suggestions
     */
    CompletableFuture<Suggestions> suggest(SuggestionInfo<CommandSender> info, SuggestionsBuilder builder)
        throws CommandSyntaxException;

}

This allows you to use Brigadier's SuggestionsBuilder and Suggestions classes to create more powerful suggestions beyond the basic capabilities of the CommandAPI.

In order to use this, you will need the Brigadier dependency, which you can find under the Brigadier installation instructions.

Example - Making an emoji broadcasting message

Say we want to let users broadcast a message, but also allow them to enter emojis into the message they're typing:

A gif showcasing a command where emojis are suggested when typing a message

For this command, we'll use a GreedyStringArgument as if we were making a generic broadcasted message. We create a map of emojis to their descriptions to use as tooltips and then we use Brigadier to display the suggestions at the end of the message where the cursor is.

Map<String, String> emojis = new HashMap<>();
emojis.put("☻", "smile");
emojis.put("❤", "heart");
emojis.put("🔥", "fire");
emojis.put("★", "star");
emojis.put("☠", "death");
emojis.put("⚠", "warning");
emojis.put("☀", "sun");
emojis.put("☺", "smile");
emojis.put("☹", "frown");
emojis.put("✉", "mail");
emojis.put("☂", "umbrella");
emojis.put("✘", "cross");
emojis.put("♪", "music note (eighth)");
emojis.put("♬", "music note (beamed sixteenth)");
emojis.put("♩", "music note (quarter)");
emojis.put("♫", "music note (beamed eighth)");
emojis.put("☄", "comet");
emojis.put("✦", "star");
emojis.put("🗡", "sword");
emojis.put("🪓", "axe");
emojis.put("🔱", "trident");
emojis.put("🎣", "fishing rod");
emojis.put("🏹", "bow");
emojis.put("⛏", "pickaxe");
emojis.put("🍖", "food");

Argument<String> messageArgument = new GreedyStringArgument("message")
    .replaceSuggestions((info, builder) -> {
        // Only display suggestions at the very end character
        builder = builder.createOffset(builder.getStart() + info.currentArg().length());

        // Suggest all the emojis!
        for (Entry<String, String> str : emojis.entrySet()) {
            builder.suggest(str.getKey(), new LiteralMessage(str.getValue()));
        }

        return builder.buildFuture();
    });

new CommandAPICommand("emoji")
    .withArguments(messageArgument)
    .executes((sender, args) -> {
        Bukkit.broadcastMessage((String) args.get("message"));
    })
    .register();
val emojis = mapOf(
    "☻" to "smile",
    "❤" to "heart",
    "🔥" to "fire",
    "★" to "star",
    "☠" to "death",
    "⚠" to "warning",
    "☀" to "sun",
    "☺" to "smile",
    "☹" to "frown",
    "✉" to "mail",
    "☂" to "umbrella",
    "✘" to "cross",
    "♪" to "music note (eighth)",
    "♬" to "music note (beamed sixteenth)",
    "♩" to "music note (quarter)",
    "♫" to "music note (beamed eighth)",
    "☄" to "comet",
    "✦" to "star",
    "🗡" to "sword",
    "🪓" to "axe",
    "🔱" to "trident",
    "🎣" to "fishing rod",
    "🏹" to "bow",
    "⛏" to "pickaxe",
    "🍖" to "food"
)

val messageArgument = GreedyStringArgument("message")
    .replaceSuggestions { info, builder ->
        // Only display suggestions at the very end character
        val newBuilder = builder.createOffset(builder.start + info.currentArg().length)

        // Suggest all the emojis!
        emojis.forEach { (emoji, description) ->
            newBuilder.suggest(emoji, LiteralMessage(description))
        }

        newBuilder.buildFuture()
    }

CommandAPICommand("emoji")
    .withArguments(messageArgument)
    .executes(CommandExecutor { _, args ->
        Bukkit.broadcastMessage(args["message"] as String)
    })
    .register()

In this example, we simply create the GreedyStringArgument and use replaceSuggestions() to specify our suggestion rules. We create an offset using the current builder to make suggestions start at the last character (the current builder start builder.getStart() and the current length of what the user has already typed info.currentArg().length()). Finally, we build the suggestions with builder.buildFuture() and then register our command as normal.

Example - Using a Minecraft command as an argument

Developer's Note:

This example has been superseded by the Command argument. This example is still present as it gives an example of much more complicated brigadier suggestions which may be useful for readers!

Courtesy of 469512345, the following example shows how using Brigadier's suggestions and parser can be combined with the CommandAPI to create an argument which suggests valid Minecraft commands. This could be used for example as a sudo command, to run a command as another player.

A gif showcasing a command suggestion for the /give command

For this command, we'll use a GreedyStringArgument because that allows users to enter any combination of characters (which therefore, allows users to enter any command). First, we start by defining the suggestions that we'll use for the GreedyStringArgument. We'll use the ArgumentSuggestions functional interface described above:

ArgumentSuggestions<CommandSender> commandSuggestions = (info, builder) -> {
    // The current argument, which is a full command
    String arg = info.currentArg();

    // Identify the position of the current argument
    int start;
    if (arg.contains(" ")) {
        // Current argument contains spaces - it starts after the last space and after the start of this argument.
        start = builder.getStart() + arg.lastIndexOf(' ') + 1;
    } else {
        // Input starts at the start of this argument
        start = builder.getStart();
    }
    
    // Parse command using brigadier
    ParseResults<?> parseResults = Brigadier.getCommandDispatcher()
        .parse(info.currentArg(), Brigadier.getBrigadierSourceFromCommandSender(info.sender()));
    
    // Intercept any parsing errors indicating an invalid command
    if(!parseResults.getExceptions().isEmpty()) {
        CommandSyntaxException exception = parseResults.getExceptions().values().iterator().next();
        // Raise the error, with the cursor offset to line up with the argument
        throw new CommandSyntaxException(exception.getType(), exception.getRawMessage(), exception.getInput(), exception.getCursor() + start);
    }

    return Brigadier
        .getCommandDispatcher()
        .getCompletionSuggestions(parseResults)
        .thenApply(suggestionsObject -> {
            // Brigadier's suggestions
            Suggestions suggestions = (Suggestions) suggestionsObject;

            return new Suggestions(
                // Offset the index range of the suggestions by the start of the current argument
                new StringRange(start, start + suggestions.getRange().getLength()),
                // Copy the suggestions
                suggestions.getList()
            );
        });
};
val commandSuggestions: ArgumentSuggestions<CommandSender> = ArgumentSuggestions { info, builder ->
    // The current argument, which is a full command
    val arg: String = info.currentArg()

    // Identify the position of the current argument
    var start = if (arg.contains(" ")) {
        // Current argument contains spaces - it starts after the last space and after the start of this argument.
        builder.start + arg.lastIndexOf(' ') + 1
    } else {
        // Input starts at the start of this argument
        builder.start
    }

    // Parse command using brigadier
    val parseResults: ParseResults<*> = Brigadier.getCommandDispatcher()
        .parse(info.currentArg(), Brigadier.getBrigadierSourceFromCommandSender(info.sender))

    // Intercept any parsing errors indicating an invalid command
    for ((_, exception) in parseResults.exceptions) {
        // Raise the error, with the cursor offset to line up with the argument
        throw CommandSyntaxException(exception.type, exception.rawMessage, exception.input, exception.cursor + start)
    }

    val completableFutureSuggestions: CompletableFuture<Suggestions> =
        Brigadier.getCommandDispatcher().getCompletionSuggestions(parseResults) as CompletableFuture<Suggestions>

    completableFutureSuggestions.thenApply { suggestions: Suggestions ->
        Suggestions(
            // Offset the index range of the suggestions by the start of the current argument
            StringRange(start, start + suggestions.range.length),
            // Copy the suggestions
            suggestions.list
        )
    }
}

There's a lot to unpack there, but it's generally split up into 4 key sections:

  • Finding the start of the argument. We find the start of the argument so we know where the beginning of our command suggestion is. This is done easily using builder.getStart(), but we also have to take into account any spaces if our command argument contains spaces.

  • Parsing the command argument. We make use of Brigadier's parse() method to parse the argument and generate some ParseResults.

  • Reporting parsing errors. This is actually an optional step, but in general it's good practice to handle exceptions stored in ParseResults. While Brigadier doesn't actually handle suggestion exceptions, this has been included in this example to showcase exception handling.

  • Generating suggestions from parse results. We use our parse results with Brigadier's getCompletionSuggestions() method to generate some suggestions based on the parse results and the suggestion string range.

Now that we've declared our arguments suggestions, we can then create our simple command with the following syntax:

/commandargument <command>

We use the command suggestions declared above by using the replaceSuggestions method in our GreedyStringArgument, and write a simple executor which runs the command that the user provided:

new CommandAPICommand("commandargument")
    .withArguments(new GreedyStringArgument("command").replaceSuggestions(commandSuggestions))
    .executes((sender, args) -> {
        // Run the command using Bukkit.dispatchCommand()
        Bukkit.dispatchCommand(sender, (String) args.get("command"));
    }).register();
CommandAPICommand("commandargument")
    .withArguments(GreedyStringArgument("command").replaceSuggestions(commandSuggestions))
    .executes(CommandExecutor { sender, args ->
        // Run the command using Bukkit.dispatchCommand()
        Bukkit.dispatchCommand(sender, args["command"] as String)
    })
    .register()