Fix tabcompletion: use custom suggestion provider

It looks like we can't have custom argument types
Checking for permission for each offered option
Suggesting the argument's name as well
#82

TODO:
Handle annotations
Remove test command
This commit is contained in:
Norbi Peti 2020-03-18 01:34:32 +01:00
parent 95a8e92b51
commit f5406a8c0e
No known key found for this signature in database
GPG key ID: DBA4C4549A927E56
9 changed files with 119 additions and 76 deletions

View file

@ -26,7 +26,7 @@ public class ChromaCommand extends ICommand2MC {
sender.sendMessage(ButtonPlugin.getCommand2MC().getCommandsText()); sender.sendMessage(ButtonPlugin.getCommand2MC().getCommandsText());
} }
@Command2.Subcommand @Command2.Subcommand //TODO: Remove
public void test(CommandSender sender, char test) { public void test(CommandSender sender, char test) {
sender.sendMessage(test + ""); sender.sendMessage(test + "");
} }

View file

@ -6,13 +6,17 @@ import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.Command2.Subcommand; import buttondevteam.lib.chat.Command2.Subcommand;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.CustomTabCompleteMethod;
import buttondevteam.lib.chat.ICommand2MC; import buttondevteam.lib.chat.ICommand2MC;
import lombok.val; import lombok.val;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.plugin.Plugin; import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream;
@CommandClass(modOnly = true, helpText = { @CommandClass(modOnly = true, helpText = {
"Component command", "Component command",
@ -20,7 +24,8 @@ import java.util.Optional;
}) })
public class ComponentCommand extends ICommand2MC { public class ComponentCommand extends ICommand2MC {
public ComponentCommand() { public ComponentCommand() {
getManager().addParamConverter(Plugin.class, arg -> Bukkit.getPluginManager().getPlugin(arg), "Plugin not found!"); getManager().addParamConverter(Plugin.class, arg -> Bukkit.getPluginManager().getPlugin(arg), "Plugin not found!",
() -> Arrays.stream(Bukkit.getPluginManager().getPlugins()).map(Plugin::getName)::iterator);
} }
@Subcommand(helpText = { @Subcommand(helpText = {
@ -57,6 +62,11 @@ public class ComponentCommand extends ICommand2MC {
return true; return true;
} }
@CustomTabCompleteMethod(param = "component", subcommand = {"enable", "disable"})
public Iterable<String> componentTabcomplete(Plugin plugin) {
return getPluginComponents(plugin).map(c -> c.getClass().getSimpleName())::iterator;
}
private boolean enable_disable(CommandSender sender, Plugin plugin, String component, boolean enable, boolean permanent) { private boolean enable_disable(CommandSender sender, Plugin plugin, String component, boolean enable, boolean permanent) {
try { try {
val oc = getComponentOrError(plugin, component, sender); val oc = getComponentOrError(plugin, component, sender);
@ -72,10 +82,13 @@ public class ComponentCommand extends ICommand2MC {
return true; return true;
} }
private Stream<Component<? extends JavaPlugin>> getPluginComponents(Plugin plugin) {
return Component.getComponents().values().stream()
.filter(c -> plugin.getName().equals(c.getPlugin().getName()));
}
private Optional<Component<?>> getComponentOrError(Plugin plugin, String arg, CommandSender sender) { private Optional<Component<?>> getComponentOrError(Plugin plugin, String arg, CommandSender sender) {
val oc = Component.getComponents().values().stream() val oc = getPluginComponents(plugin).filter(c -> c.getClass().getSimpleName().equalsIgnoreCase(arg)).findAny();
.filter(c -> plugin.getName().equals(c.getPlugin().getName()))
.filter(c -> c.getClass().getSimpleName().equalsIgnoreCase(arg)).findAny();
if (!oc.isPresent()) if (!oc.isPresent())
sender.sendMessage("§cComponent not found!"); //^ Much simpler to solve in the new command system sender.sendMessage("§cComponent not found!"); //^ Much simpler to solve in the new command system
return oc; return oc;

View file

@ -7,6 +7,7 @@ import buttondevteam.lib.chat.ICommand2MC;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer; import org.bukkit.OfflinePlayer;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -19,7 +20,8 @@ public class MemberCommand extends ICommand2MC {
private final MemberComponent component; private final MemberComponent component;
public MemberCommand(MemberComponent component) { public MemberCommand(MemberComponent component) {
getManager().addParamConverter(OfflinePlayer.class, Bukkit::getOfflinePlayer, "Player not found!"); getManager().addParamConverter(OfflinePlayer.class, Bukkit::getOfflinePlayer, "Player not found!",
() -> Bukkit.getOnlinePlayers().stream().map(HumanEntity::getName)::iterator);
this.component = component; this.component = component;
} }

View file

@ -2,6 +2,7 @@ package buttondevteam.core.component.towny;
import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2;
import buttondevteam.lib.chat.CommandClass; import buttondevteam.lib.chat.CommandClass;
import buttondevteam.lib.chat.CustomTabComplete;
import buttondevteam.lib.chat.ICommand2MC; import buttondevteam.lib.chat.ICommand2MC;
import com.palmergames.bukkit.towny.TownySettings; import com.palmergames.bukkit.towny.TownySettings;
import com.palmergames.bukkit.towny.TownyUniverse; import com.palmergames.bukkit.towny.TownyUniverse;
@ -20,7 +21,7 @@ import java.util.stream.Stream;
}) })
public class RemoveResidentsCommand extends ICommand2MC { public class RemoveResidentsCommand extends ICommand2MC {
@Command2.Subcommand @Command2.Subcommand
public void def(CommandSender sender, @Command2.OptionalArg String remove) { public void def(CommandSender sender, @Command2.OptionalArg @CustomTabComplete("remove") String remove) {
Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), () -> { Bukkit.getScheduler().runTaskAsynchronously(getPlugin(), () -> {
sender.sendMessage("Starting..."); sender.sendMessage("Starting...");
var ds = TownyUniverse.getInstance().getDataSource(); var ds = TownyUniverse.getInstance().getDataSource();

View file

@ -27,6 +27,7 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
/** /**
* The method name is the subcommand, use underlines (_) to add further subcommands. * The method name is the subcommand, use underlines (_) to add further subcommands.
@ -110,10 +111,11 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
protected static class ParamConverter<T> { protected static class ParamConverter<T> {
public final Function<String, T> converter; public final Function<String, T> converter;
public final String errormsg; public final String errormsg;
public final Supplier<Iterable<String>> allSupplier;
} }
protected HashMap<String, SubcommandData<TC>> subcommands = new HashMap<>(); protected final HashMap<String, SubcommandData<TC>> subcommands = new HashMap<>();
private HashMap<Class<?>, ParamConverter<?>> paramConverters = new HashMap<>(); protected final HashMap<Class<?>, ParamConverter<?>> paramConverters = new HashMap<>();
private ArrayList<String> commandHelp = new ArrayList<>(); //Mainly needed by Discord private ArrayList<String> commandHelp = new ArrayList<>(); //Mainly needed by Discord
@ -123,12 +125,14 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
* Adds a param converter that obtains a specific object from a string parameter. * Adds a param converter that obtains a specific object from a string parameter.
* The converter may return null. * The converter may return null.
* *
* @param cl The class of the result object * @param <T> The type of the result
* @param converter The converter to use * @param cl The class of the result object
* @param <T> The type of the result * @param converter The converter to use
* @param allSupplier The supplier of all possible values (ideally)
*/ */
public <T> void addParamConverter(Class<T> cl, Function<String, T> converter, String errormsg) { public <T> void addParamConverter(Class<T> cl, Function<String, T> converter, String errormsg,
paramConverters.put(cl, new ParamConverter<>(converter, errormsg)); Supplier<Iterable<String>> allSupplier) {
paramConverters.put(cl, new ParamConverter<>(converter, errormsg, allSupplier));
} }
public boolean handleCommand(TP sender, String commandline) { public boolean handleCommand(TP sender, String commandline) {
@ -180,12 +184,12 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
if (sendertype.isAssignableFrom(sender.getClass())) if (sendertype.isAssignableFrom(sender.getClass()))
params.add(sender); //The command either expects a CommandSender or it is a Player, or some other expected type params.add(sender); //The command either expects a CommandSender or it is a Player, or some other expected type
else if (sender instanceof Command2MCSender else if (sender instanceof Command2MCSender
&& sendertype.isAssignableFrom(((Command2MCSender) sender).getSender().getClass())) && sendertype.isAssignableFrom(((Command2MCSender) sender).getSender().getClass()))
params.add(((Command2MCSender) sender).getSender()); params.add(((Command2MCSender) sender).getSender());
else if (ChromaGamerBase.class.isAssignableFrom(sendertype) else if (ChromaGamerBase.class.isAssignableFrom(sendertype)
&& sender instanceof Command2MCSender && sender instanceof Command2MCSender
&& (cg = ChromaGamerBase.getFromSender(((Command2MCSender) sender).getSender())) != null && (cg = ChromaGamerBase.getFromSender(((Command2MCSender) sender).getSender())) != null
&& cg.getClass() == sendertype) //The command expects a user of our system && cg.getClass() == sendertype) //The command expects a user of our system
params.add(cg); params.add(cg);
else { else {
sender.sendMessage("§cYou need to be a " + sendertype.getSimpleName() + " to use this command."); sender.sendMessage("§cYou need to be a " + sendertype.getSimpleName() + " to use this command.");
@ -201,7 +205,7 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
if (cl.isPrimitive()) if (cl.isPrimitive())
params.add(Defaults.defaultValue(cl)); params.add(Defaults.defaultValue(cl));
else if (Number.class.isAssignableFrom(cl) else if (Number.class.isAssignableFrom(cl)
|| Number.class.isAssignableFrom(cl)) || Number.class.isAssignableFrom(cl))
params.add(Defaults.defaultValue(Primitives.unwrap(cl))); params.add(Defaults.defaultValue(Primitives.unwrap(cl)));
else else
params.add(null); params.add(null);
@ -309,7 +313,7 @@ public abstract class Command2<TC extends ICommand2<TP>, TP extends Command2Send
var ht = command.getHelpText(method, ann); var ht = command.getHelpText(method, ann);
if (ht != null) { //The method is a subcommand if (ht != null) { //The method is a subcommand
val subcommand = commandChar + path + //Add command path (class name by default) val subcommand = commandChar + path + //Add command path (class name by default)
getCommandPath(method.getName(), ' '); //Add method name, unless it's 'def' getCommandPath(method.getName(), ' '); //Add method name, unless it's 'def'
var params = new String[method.getParameterCount() - 1]; var params = new String[method.getParameterCount() - 1];
ht = getParameterHelp(method, ht, subcommand, params); ht = getParameterHelp(method, ht, subcommand, params);
var sd = new SubcommandData<>(method, command, params, ht); var sd = new SubcommandData<>(method, command, params, ht);

View file

@ -4,11 +4,12 @@ import buttondevteam.core.MainPlugin;
import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.TBMCCoreAPI;
import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.lib.architecture.ButtonPlugin;
import buttondevteam.lib.architecture.Component; import buttondevteam.lib.architecture.Component;
import buttondevteam.lib.chat.commandargs.BetterStringArgumentType;
import com.mojang.brigadier.arguments.*; import com.mojang.brigadier.arguments.*;
import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;
import lombok.val; import lombok.val;
import me.lucko.commodore.Commodore; import me.lucko.commodore.Commodore;
import me.lucko.commodore.CommodoreProvider; import me.lucko.commodore.CommodoreProvider;
@ -20,7 +21,9 @@ import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender; import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.command.SimpleCommandMap; import org.bukkit.command.SimpleCommandMap;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.server.TabCompleteEvent;
import org.bukkit.permissions.Permission; import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault; import org.bukkit.permissions.PermissionDefault;
@ -31,6 +34,7 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implements Listener { public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implements Listener {
/** /**
@ -139,8 +143,8 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
* {@see super#addParamConverter} * {@see super#addParamConverter}
*/ */
@Override @Override
public <T> void addParamConverter(Class<T> cl, Function<String, T> converter, String errormsg) { public <T> void addParamConverter(Class<T> cl, Function<String, T> converter, String errormsg, Supplier<Iterable<String>> allSupplier) {
super.addParamConverter(cl, converter, "§c" + errormsg); super.addParamConverter(cl, converter, "§c" + errormsg, allSupplier);
} }
public void unregisterCommands(ButtonPlugin plugin) { public void unregisterCommands(ButtonPlugin plugin) {
@ -158,6 +162,13 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
.map(comp -> component.getClass().getSimpleName().equals(comp.getClass().getSimpleName())).orElse(false)); .map(comp -> component.getClass().getSimpleName().equals(comp.getClass().getSimpleName())).orElse(false));
} }
@EventHandler
public void onTabComplete(TabCompleteEvent event) {
//System.out.println("Tabcomplete: " + event.getBuffer());
//System.out.println("First completion: " + event.getCompletions().stream().findFirst().orElse("no completions"));
event.getCompletions().clear();
}
private boolean shouldRegisterOfficially = true; private boolean shouldRegisterOfficially = true;
private void registerOfficially(ICommand2MC command, List<SubcommandData<ICommand2MC>> subcmds) { private void registerOfficially(ICommand2MC command, List<SubcommandData<ICommand2MC>> subcmds) {
@ -202,6 +213,19 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
private static class TabcompleteHelper { private static class TabcompleteHelper {
private static Commodore commodore; private static Commodore commodore;
private static LiteralCommandNode<Object> appendSubcommand(String path, CommandNode<Object> parent,
SubcommandData<ICommand2MC> subcommand) {
var scmdBuilder = LiteralArgumentBuilder.literal(path);
if (subcommand != null)
scmdBuilder.requires(o -> {
var sender = commodore.getBukkitSender(o);
return ButtonPlugin.getCommand2MC().hasPermission(sender, subcommand.command, subcommand.method);
});
var scmd = scmdBuilder.build();
parent.addChild(scmd);
return scmd;
}
private static void registerTabcomplete(ICommand2MC command2MC, List<SubcommandData<ICommand2MC>> subcmds, Command bukkitCommand) { private static void registerTabcomplete(ICommand2MC command2MC, List<SubcommandData<ICommand2MC>> subcmds, Command bukkitCommand) {
if (commodore == null) if (commodore == null)
commodore = CommodoreProvider.getCommodore(MainPlugin.Instance); //Register all to the Core, it's easier commodore = CommodoreProvider.getCommodore(MainPlugin.Instance); //Register all to the Core, it's easier
@ -209,18 +233,15 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
var maincmd = LiteralArgumentBuilder.literal(path[0]).build(); var maincmd = LiteralArgumentBuilder.literal(path[0]).build();
var cmd = maincmd; var cmd = maincmd;
for (int i = 1; i < path.length; i++) { for (int i = 1; i < path.length; i++) {
var subcmd = LiteralArgumentBuilder.literal(path[i]).build(); var scmd = subcmds.stream().filter(sd -> sd.method.getName().equals("def")).findAny().orElse(null);
cmd.addChild(subcmd); cmd = appendSubcommand(path[i], cmd, scmd); //Add each part of the path as a child of the previous one
cmd = subcmd; //Add each part of the path as a child of the previous one
} }
for (SubcommandData<ICommand2MC> subcmd : subcmds) { for (SubcommandData<ICommand2MC> subcmd : subcmds) {
String[] subpath = ButtonPlugin.getCommand2MC().getCommandPath(subcmd.method.getName(), ' ').trim().split(" "); String[] subpath = ButtonPlugin.getCommand2MC().getCommandPath(subcmd.method.getName(), ' ').trim().split(" ");
CommandNode<Object> scmd = cmd; CommandNode<Object> scmd = cmd;
if (subpath[0].length() > 0) { //If the method is def, it will contain one empty string if (subpath[0].length() > 0) { //If the method is def, it will contain one empty string
for (String s : subpath) { for (String s : subpath) {
var subsubcmd = LiteralArgumentBuilder.literal(s).build(); scmd = appendSubcommand(s, scmd, subcmd); //Add method name part of the path (could_be_multiple())
scmd.addChild(subsubcmd);
scmd = subsubcmd; //Add method name part of the path (could_be_multiple())
} }
} }
Parameter[] parameters = subcmd.method.getParameters(); Parameter[] parameters = subcmd.method.getParameters();
@ -232,7 +253,7 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
if (parameter.isAnnotationPresent(TextArg.class)) if (parameter.isAnnotationPresent(TextArg.class))
type = StringArgumentType.greedyString(); type = StringArgumentType.greedyString();
else else
type = BetterStringArgumentType.word(); type = StringArgumentType.word();
else if (ptype == int.class || ptype == Integer.class else if (ptype == int.class || ptype == Integer.class
|| ptype == byte.class || ptype == Byte.class || ptype == byte.class || ptype == Byte.class
|| ptype == short.class || ptype == Short.class) || ptype == short.class || ptype == Short.class)
@ -244,12 +265,18 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
else if (ptype == double.class || ptype == Double.class) else if (ptype == double.class || ptype == Double.class)
type = DoubleArgumentType.doubleArg(); type = DoubleArgumentType.doubleArg();
else if (ptype == char.class || ptype == Character.class) else if (ptype == char.class || ptype == Character.class)
type = BetterStringArgumentType.word(1); type = StringArgumentType.word();
else if (ptype == boolean.class || ptype == Boolean.class) else if (ptype == boolean.class || ptype == Boolean.class)
type = BoolArgumentType.bool(); type = BoolArgumentType.bool();
else //TODO: Custom parameter types else //TODO: Custom parameter types
type = BetterStringArgumentType.word(); type = StringArgumentType.word();
var arg = RequiredArgumentBuilder.argument(subcmd.parameters[i - 1], type).build(); val param = subcmd.parameters[i - 1];
var argb = RequiredArgumentBuilder.argument(param, type)
.suggests((SuggestionProvider<Object>) (context, builder) -> {
//TODO
return builder.suggest(param).buildFuture();
});
var arg = argb.build();
scmd.addChild(arg); scmd.addChild(arg);
scmd = arg; scmd = arg;
} }
@ -263,7 +290,7 @@ public class Command2MC extends Command2<ICommand2MC, Command2MCSender> implemen
} catch (Exception e) { - Client log: Could not deserialize chroma:string } catch (Exception e) { - Client log: Could not deserialize chroma:string
e.printStackTrace(); e.printStackTrace();
}*/ }*/
commodore.register(bukkitCommand, maincmd); commodore.register(maincmd);
} }
} }
} }

View file

@ -0,0 +1,15 @@
package buttondevteam.lib.chat;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Can be used if an argument should be completed with predefined strings.
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomTabComplete {
String[] value();
}

View file

@ -0,0 +1,23 @@
package buttondevteam.lib.chat;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* The method must return with {@link String}[] or {@link Iterable}&lt;{@link String}&gt; and may have the sender and preceding arguments as parameters.
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomTabCompleteMethod {
/**
* The parameter's name where we want to give completion
*/
String param();
/**
* The subcommand(s) which have the parameter, by default the method's name
*/
String[] subcommand() default "";
}

View file

@ -1,42 +0,0 @@
package buttondevteam.lib.chat.commandargs;
import com.mojang.brigadier.LiteralMessage;
import com.mojang.brigadier.StringReader;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
@RequiredArgsConstructor
public class BetterStringArgumentType implements ArgumentType<String> {
private final int len;
public static BetterStringArgumentType word() {
return new BetterStringArgumentType(-1);
}
public static BetterStringArgumentType word(int maxlen) {
return new BetterStringArgumentType(maxlen);
}
@Override
public String parse(StringReader reader) throws CommandSyntaxException {
if (len < 1)
return reader.readStringUntil(' ');
final int start = reader.getCursor();
if (reader.canRead(len + 1) && reader.peek(len) != ' ')
throw new SimpleCommandExceptionType(new LiteralMessage("String too long")).createWithContext(reader);
for (int i = 0; i < len; i++)
reader.skip();
return reader.getString().substring(start, reader.getCursor());
}
@Override
public Collection<String> getExamples() {
return StringArgumentType.StringType.SINGLE_WORD.getExamples();
}
}