Skip to content

Auto completions and suggestions

Revxrsal edited this page Jul 10, 2022 · 2 revisions

Auto-completions represent a fundamental feature when building commands, as they greatly help the end-user know what parameter to supply and give appropriate suggestions.

Luckily, Lamp provides multiple ways to create powerful suggestions and auto-completions.

Completions by parameter type

This is the most common and convenient way of creating suggestions. We bind a specific suggestion provider to a parameter type.

Let's imagine we have this class, where we store the names of kits:

public final class Kits {

    private static final Map<String, Kit> KITS = new HashMap<>();

    public static void register(Kit kit) {
        KITS.put(kit.getName(), kit);
    }

    public static Kit getKit(String name) {
        return KITS.get(name);
    }

    public static Set<String> getKitsNames() {
        return Collections.unmodifiableSet(KITS.keySet());
    }
}

Then, we want every Kit in commands to be automatically completed by the names of the kits registered. For this, we will use registerParameterSuggestions in CommandHandler.getAutoCompleter()

CommandHandler handler = ...;
handler.getAutoCompleter().registerParameterSuggestions(Kit.class, (args, sender, command) -> Kits.getKitsNames());

That's it! Now, every Kit parameter will automatically receive the completions in getKitsNames().

@Command("kit give")
public void giveKit(Kit kit, @Default("me") Player player) {
    kit.give(player);
    ...
}

Completions by @AutoComplete

Sometimes, we may want more restrictions on completions, depending on the command being executed. Using the @AutoComplete annotation, it is possible to bind IDs to suggestion providers.

handler.getAutoCompleter().registerSuggestion("vipKits", (args, sender, command) -> {  
    return KITS.values().stream().filter(Kit::isVIP).map(Kit::getName).collect(Collectors.toList());  
});

handler.getAutoCompleter().registerSuggestion("adminKits", (args, sender, command) -> {  
    return KITS.values().stream().filter(Kit::isAdmin).map(Kit::getName).collect(Collectors.toList());  
});

Then, we can reference our completions using our annotation, by preceding the ID with @:

@Command("kit give vip")
@AutoComplete("@vipKits *")
public void giveVipKit(Kit kit, @Default("self") Player player) {
    kit.give(player);
    ...
}
@Command("kit give admin")
@AutoComplete("@adminKits *")
public void giveAdminKit(Kit kit, @Default("self") Player player) {
    kit.give(player);
    ...
}

There are a few things to note here:

  • Notice the * that follows vipKits and adminKits: Since @AutoComplete overwrites the entire command auto-completion, you are required to provide auto-completions for all the parameters. By using * for any remaining parameter, we are telling Lamp to use the default suggestion provider for the given parameter.
  • Notice that IDs must begin with @. Otherwise, they will be considered a string literal and be passed directly as the completion. For example, doing @AutoComplete("1|2|3") will return 1, 2, and 3 as suggestions for the command.

Completions using SuggestionProviderFactory

Although the above 2 methods cover 80% of the cases, it is possible that you may want to extend your auto-completion functionality beyond what is provided in the other two methods. Such examples are:

  • Custom annotations with fields that control how the completion occurs
  • Registering the same provider for multiple classes (e.g. a common interface or superclass)
  • Auto-completions based on parameter generics (e.g. List<String> vs List<Integer>)

In this example, we will go by the first case, which is the most common. Let's say we want to return a completion for players who only have a specific permission.

Let's create our annotation:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface WithPermission {
    
    String value();
    
}

Then, we register a SuggestionProviderFactory (as a lambda) using registerSuggestionFactory:

handler.getAutoCompleter().registerSuggestionFactory(parameter -> {
     if (parameter.hasAnnotation(WithPermission.class)) {
       String permission = parameter.getAnnotation(WithPermission.class).value();
       return (args, sender, command) -> { // Create a SuggestionProvider here
         List<String> players = new ArrayList<>();
         for (Player player : Bukkit.getOnlinePlayers()) {
          if (player.hasPermission(permission))
            players.add(player.getName());
         }
         return players;
       };
     }
     return null; // Parameter does not have @WithPermission, ignore it.
});

Then, we can use our annotation to provide completions for players who only have the required permission.

@Command("send")  
public void send(CommandActor sender, @WithPermission("hello.lamp") Player player) {  
    ...          
}

This gives us the utmost flexibility in providing completions and makes it possible to abstract away all the completions logic behind a specific annotation, therefore reducing repetitive code and encouraging readability and declarativity. Combined with resolvers that respect custom annotations, this allows you to put all the command-related logic behind a single annotation, requiring you only to implement it on the parameter.

Clone this wiki locally