This page explains how we can create static, dynamic and context-aware auto-completions
Auto-completions are a vital part of commands. They help users input valid values and save them a lot of typing.
Lamp provides a lot of helpful APIs for creating auto-completions. We will go through them in this page.
The SuggestionProvider interface
This is the foundational building block that is responsible for handling suggestions and auto-completion. It is a basic functional interface that has access to the command actor and provided parameters (parsed) and returns a list of suggestions.
You will see SuggestionProvider in the following places:
SuggestionProviders: An immutable registry that contains all registrations for SuggestionProviders. It is maintained through Lamp#suggestionProviders() and can be constructed inside Lamp.Builder#suggestionProviders().
SuggestionProvider.Factory: An interface that can generate SuggestionProviders dynamically for parameters. This factory can access the parameter type (and generics), its annotations, and other SuggestionProviders. It is a powerful interface as it can generate custom suggestions based on the type generics, a common interface, a specific data type (such as enums), or suggestions bound to a particular annotation.
ParameterType#defaultSuggestions(): This is a method that can be overridden in subclasses of ParameterType. It allows parameter types to define a default SuggestionProvider that is used when no other suggestion provider is available.
Let's move on to the creation of custom suggestions
Static completions
This is the simplest form of auto-completion: we have a static set of values that we would like to recommend to the user.
By static completions, we mean hard-coded, compile-time-defined constant completions. For that, we use the @Suggest annotation.
@Command("coins give")publicvoidgive(BukkitCommandActor actor,Player target, @Suggest({"100","200","300","500","1000"}) int coins) {/* ... */}
This will automatically supply the users with all the suggestions in @Suggest. A nicety of the @Suggest annotation is that suggestions are allowed to contain spaces. We can define suggestions that contain spaces easily and Lamp will handle the rest.
As you can see, there isn't much you can do with @Suggest. This is why we will need to introduce more complicated ways for creating suggestions.
Type-specific completions
It's common to have a certain set of completions bound to a particular Java class. Let's imagine we have a World parameter. We can create a SuggestionProvider that will retrieve the names of available worlds and give them back to us.
We can register this in our Lamp.Builder:
var lamp =Lamp.builder().suggestionProviders(providers -> {providers.addProvider(World.class, context -> {returnBukkit.getWorlds().stream().map(World::getName).toList(); }); }).build();
Lamp also provides a way to create auto-completions that are bound to certain annotations. This is a very flexible approach as it combines the benefits of dynamic parameters as well as annotations that allow you to tweak suggestions as needed.
Let's create an annotation named @WithPermission("some.permission.node"). This annotation will automatically give us all players that have a certain annotation node.
Let's create our suggestion provider. We can register one with SuggestionProviders.Builder#addProviderForAnnotation, which constructs a SuggestionProvider.Factory under the hood.
var lamp =Lamp.builder().suggestionProviders(providers -> {providers.addProviderForAnnotation(WithPermission.class, withPermission -> {String permission =withPermission.value(); // <-- the value inside @WithPermissionreturn context -> { // <-- here we return a SuggestionProviderreturnBukkit.getOnlinePlayers().stream().filter(player ->player.hasPermission(permission)).map(Player::getName).toList(); }; }); }).build();
var lamp =BukkitLamp.builder(this).suggestionProviders(suggestions -> {suggestions.addProviderFactory(WithPermissionSuggestionFactory.INSTANCE); }).build();
A few extra lines, sure. But this will protect us from messy lambdas, and keep everything in its own separate place. Better organization and maintenance.
Suggestion provider factories
So far, our suggestion providers have been very specific, either to a particular class or to a particular annotation. But, what if we want to generate SuggestionProviders that work even beyond such scopes? This is where SuggestionProvider.Factory comes in. Here are some use-cases where it would come useful:
Create annotation-bound SuggestionProviders that act differently depending on the parameter type or the annotations present on it
Create SuggestionProviders for a certain class or its subclasses
Create SuggestionProviders that respect generics (e.g. List<String> vs List<Integer>)
Create SuggestionProviders made of other SuggestionProviders (e.g. T[] which uses the completions of T)
Create SuggestionProviders that act on a certain type of classes, e.g. automatically generate suggestions for all enum types without having to explicitly register them.
...
Let's create a basic factory that will generate suggestions for all enum types. This will save us from creating individual SuggestionProviders.
publicenumEnumSuggestionProviderFactoryimplementsSuggestionProvider.Factory<CommandActor> { INSTANCE; @Override public @Nullable SuggestionProvider<CommandActor> create(@NotNull Type type, @NotNull AnnotationList annotations, @NotNull Lamp<CommandActor> lamp) {
returnnull; }}
That's it! We can simply register this to our SuggestionProviders.Builder, and automatically get suggestions for all enums generated. Effortless, efficient and easy!
var lamp =Lamp.builder().suggestionProviders(suggestions -> {suggestions.addProviderFactory(EnumSuggestionProviderFactory.INSTANCE); }).build()
Finally, Lamp provides a utility annotation: @SuggestWith that allows you to specify either a SuggestionProvider or a SuggestionProvider.Factory for individual parameters. This is useful if we want a quick, dirty way of modifying suggestions for a particular parameter without creating individual annotations or wrapper types.
Let's imagine that we have this suggestion provider that automatically suggests all worlds that start with world.
In the future, Lamp will try different things (instance, getInstance, INSTANCE, singleton methods, etc.) for providing instances, but in the meantime, this is what you have to do.
It's important to ensure that your annotation has runtime retention. This means that it should have @Retention(RetentionPolicy.RUNTIME) on it. Kotlin annotations have it by default.
Note: While Lamp allows you to do everything in lambdas, it is actually discouraged as it can lead to terribly-looking code! For small lambdas, it's okay. But when things get big and messy, it's better to put them in a separate class.
A SuggestionProvider.Factory should almost always work with well-defined constraints (in our case, enum types only). Because a SuggestionProvider.Factory has the potential to create suggestion providers for anything, we should be careful with what we return. When a factory receives types/annotations that it does not care about (in our case, a non-enum type), it should return null!
Suggestion providers (or factories) supplied with @SuggestWith must provide a no-arg constructor, as Lamp will have to construct them reflectively.