-
Notifications
You must be signed in to change notification settings - Fork 0
Command completion
KWCommands provides an implementation of Command completion, lets take the example of Custom ArgumentType and use AutoCompleter on it.
Completer depends on CommandParser because it uses data from failures of incomplete parsing to construct command and argument completion.
public class KwDocsCustomArgTypeCompleter {
public static void main(String[] args) {
CommandProcessor processor = Processors.createCommonProcessor();
Completion completion = new CompletionImpl(processor.getParser());
Command command = Command.builder()
.name("register")
.addAlias("reg")
.description(Text.of("Register user"))
.staticArguments()
.addArgument(Argument.builder()
.name("user")
.argumentType(
new UserArgumentType(null)
)
.build()
)
.build()
.handler((commandContainer, informationProviders, resultHandler) -> {
User2 user = commandContainer.<User2>getArgumentValue("user");
System.out.printf("Registered user '%s' (email: '%s') age: '%s'%n", user.getName(), user.getEmail(), user.getAge());
return Unit.INSTANCE;
})
.build();
processor.getCommandManager().registerCommand(command, new KwDocsCustomArgTypeCompleter());
List<String> complete = completion.complete("r", null, InformationProvidersVoid.INSTANCE);
System.out.println(complete);
}
}
This prints: [register, reg]
.
Now lets try to complete arguments:
> List<String> complete = completion.complete("register ", null, InformationProvidersVoid.INSTANCE);
> System.out.println(complete);
[--user, {]
> List<String> complete = completion.complete("register {", null, InformationProvidersVoid.INSTANCE);
> System.out.println(complete);
[}]
Oh no, AutoCompletion is not working? Calm down, it is because we haven't provided the correct information for parsing system when we declared our custom ArgumentType, so the AutoCompleter does not know how to complete the keys of the Map, so we go back to UserArgumentType and change getMapKeyType
to include possible values for keys for each map position:
@NotNull
@Override
// Gets the type of map key at position i
public ArgumentType<?, ?> getMapKeyType(int i) {
switch (i) {
case 0:return CommonArgTypesKt.stringArgumentType("name");
case 1:return CommonArgTypesKt.stringArgumentType("email");
case 2:return CommonArgTypesKt.stringArgumentType("age");
default:return CommonArgTypesKt.getStringArgumentType();
}
}
Now if we run the complete again:
> List<String> complete = completion.complete("register {", null, InformationProvidersVoid.INSTANCE);
> System.out.println(complete);
[}, name]
You could also improve your ArgumentType by allowing any of these combinations to be placed in any order, lets change the ArgumentType a bit:
public class UserArgumentType extends ArgumentType<MapInput, User2> {
private final UserArgumentParser userArgumentParser = new UserArgumentParser(this);
public UserArgumentType(@Nullable User2 defaultValue) {
super(defaultValue, MapInputType.INSTANCE, TypeInfo.of(User2.class));
}
@NotNull
@Override
public ArgumentParser<MapInput, User2> getParser() {
return this.userArgumentParser;
}
@NotNull
@Override
// Possibilities of completion for this Argument
public Possibilities getPossibilities() {
List<Pair<? extends Input, ? extends Input>> inputs = Arrays.asList(
new Pair<>(new SingleInput("name"), new SingleInput("")),
new Pair<>(new SingleInput("email"), new SingleInput("")),
new Pair<>(new SingleInput("age"), new SingleInput(""))
);
MapInput mapInput = new MapInput(inputs);
return () -> Collections.singletonList(mapInput);
}
@NotNull
@Override
// Gets the type of list element at position i
public ArgumentType<?, ?> getListType(@NotNull List<? extends Input> parsedInputs, int i) {
return this.getMapKeyType(Collections.emptyList(), i);
}
@NotNull
@Override
// Gets the type of map key at position i
public ArgumentType<?, ?> getMapKeyType(@NotNull List<? extends Pair<? extends Input, ? extends Input>> parsedPairs, int i) {
return new SingleArgumentType<>(
StringParser.INSTANCE,
() -> Arrays.asList(new SingleInput("name"), new SingleInput("email"), new SingleInput("age")),
null,
TypeInfo.of(String.class)
);
}
@NotNull
@Override
// Gets the type of map value at position i
public ArgumentType<?, ?> getMapValueType(@NotNull Input key, int i) {
if (key.toInputString().equals("age")) {
return CommonArgTypesKt.getIntArgumentType();
} else {
return CommonArgTypesKt.getStringArgumentType();
}
}
@Override
// Whether there is a type at position i
public boolean hasType(int i) {
return i > 0 && i < 3;
}
}
Now try again...
> List<String> complete = completion.complete("register {", null, InformationProvidersVoid.INSTANCE);
> System.out.println(complete);
[}, name, email, age]
> completion.complete("register {name", null, InformationProvidersVoid.INSTANCE);
[:, =, name, email, age]
> completion.complete("register {name=", null, InformationProvidersVoid.INSTANCE);
[]
Oh no, we got to a point where AutoComplete can't complete, this happens because name
accepts a String, but we can't get suggestions of String to show to user because it will generate an infinite combination of characters, so AutoCompleter does not try to complete that. There is various workarounds we could work to resolve this, such as defining a subset of random names to be used as an example, so lets do it:
public class UserArgumentType extends ArgumentType<MapInput, User2> {
private final UserArgumentParser userArgumentParser = new UserArgumentParser(this);
public UserArgumentType(@Nullable User2 defaultValue) {
super(defaultValue, MapInputType.INSTANCE, TypeInfo.of(User2.class));
}
@NotNull
@Override
public ArgumentParser<MapInput, User2> getParser() {
return this.userArgumentParser;
}
@NotNull
@Override
// Possibilities of completion for this Argument
public Possibilities getPossibilities() {
List<Pair<? extends Input, ? extends Input>> inputs = Arrays.asList(
new Pair<>(new SingleInput("name"), new SingleInput("")),
new Pair<>(new SingleInput("email"), new SingleInput("")),
new Pair<>(new SingleInput("age"), new SingleInput(""))
);
MapInput mapInput = new MapInput(inputs);
return () -> Collections.singletonList(mapInput);
}
@NotNull
@Override
// Gets the type of list element at position i
public ArgumentType<?, ?> getListType(@NotNull List<? extends Input> parsedInputs, int i) {
return this.getMapKeyType(Collections.emptyList(), i);
}
@NotNull
@Override
// Gets the type of map key at position i
public ArgumentType<?, ?> getMapKeyType(@NotNull List<? extends Pair<? extends Input, ? extends Input>> parsedPairs, int i) {
Set<String> exclude = parsedPairs.stream()
.map(it -> it.getFirst().toInputString())
.collect(Collectors.toSet());
return new SingleArgumentType<>(
StringParser.INSTANCE,
() -> Stream.of("name", "email", "age")
.filter(it -> !exclude.contains(it)).map(SingleInput::new)
.collect(Collectors.toList()),
null,
TypeInfo.of(String.class)
);
}
@NotNull
@Override
// Gets the type of map value at position i
public ArgumentType<?, ?> getMapValueType(@NotNull Input key, int i) {
String keyStr = key.toInputString();
switch (keyStr) {
case "name": return this.choices("Some", "Random", "Name");
case "email": return this.choices("some@random.email", "some@random.email2");
case "age": return this.choices(1, 18, 21, 26, 30, 45, 60, 70, 80, 90);
default: return CommonArgTypesKt.getAnyArgumentType();
}
}
private SingleArgumentType<String> choices(String... choices) {
return new SingleArgumentType<>(
StringParser.INSTANCE,
() -> Arrays.stream(choices).map(SingleInput::new).collect(Collectors.toList()),
null,
TypeInfo.of(String.class)
);
}
private SingleArgumentType<Integer> choices(int... choices) {
return new SingleArgumentType<>(
IntParser.INSTANCE,
() -> Arrays.stream(choices).mapToObj(String::valueOf).map(SingleInput::new).collect(Collectors.toList()),
null,
TypeInfo.of(Integer.class)
);
}
@Override
// Whether there is a type at position i
public boolean hasType(int i) {
return i >= 0 && i < 3;
}
}
Now it is possible to complete map values:
> completion.complete("register {name=", null, InformationProvidersVoid.INSTANCE);
[Some, Random, Name]
> completion.complete("register {name=qwe,", null, InformationProvidersVoid.INSTANCE);
[email, age]
> completion.complete("register {name=qwe,email=", null, InformationProvidersVoid.INSTANCE);
[some@random.email, some@random.email2]
> completion.complete("register {name=qwe,email=email@email.email,age=", null, InformationProvidersVoid.INSTANCE);
[1, 18, 21, 26, 30, 45, 60, 70, 80, 90]
> completion.complete("register {name=qwe,email=email@email.email,age=22", null, InformationProvidersVoid.INSTANCE);
[,, }] // Suggests ',' because there could be more entries in the Map.
> completion.complete("register {name=qwe,email=email@email.email,age=22}", null, InformationProvidersVoid.INSTANCE);
[]
Note that AutoCompleter will always suggests all possibilities provided to them, so make sure to limit the number of sugestions you provide to Completer.
As we will talk in the future, custom ArgumentsProviders also benefit from AutoComplete, as long as they respect the API constraints.