From f5406a8c0e3e1c8cf029574951253d2b7b357255 Mon Sep 17 00:00:00 2001 From: NorbiPeti Date: Wed, 18 Mar 2020 01:34:32 +0100 Subject: [PATCH] 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 --- .../buttondevteam/core/ChromaCommand.java | 2 +- .../buttondevteam/core/ComponentCommand.java | 21 +++++-- .../core/component/members/MemberCommand.java | 4 +- .../towny/RemoveResidentsCommand.java | 3 +- .../java/buttondevteam/lib/chat/Command2.java | 30 +++++----- .../buttondevteam/lib/chat/Command2MC.java | 55 ++++++++++++++----- .../lib/chat/CustomTabComplete.java | 15 +++++ .../lib/chat/CustomTabCompleteMethod.java | 23 ++++++++ .../commandargs/BetterStringArgumentType.java | 42 -------------- 9 files changed, 119 insertions(+), 76 deletions(-) create mode 100644 Chroma-Core/src/main/java/buttondevteam/lib/chat/CustomTabComplete.java create mode 100644 Chroma-Core/src/main/java/buttondevteam/lib/chat/CustomTabCompleteMethod.java delete mode 100644 Chroma-Core/src/main/java/buttondevteam/lib/chat/commandargs/BetterStringArgumentType.java diff --git a/Chroma-Core/src/main/java/buttondevteam/core/ChromaCommand.java b/Chroma-Core/src/main/java/buttondevteam/core/ChromaCommand.java index 6328ebc..06fe7f4 100644 --- a/Chroma-Core/src/main/java/buttondevteam/core/ChromaCommand.java +++ b/Chroma-Core/src/main/java/buttondevteam/core/ChromaCommand.java @@ -26,7 +26,7 @@ public class ChromaCommand extends ICommand2MC { sender.sendMessage(ButtonPlugin.getCommand2MC().getCommandsText()); } - @Command2.Subcommand + @Command2.Subcommand //TODO: Remove public void test(CommandSender sender, char test) { sender.sendMessage(test + ""); } diff --git a/Chroma-Core/src/main/java/buttondevteam/core/ComponentCommand.java b/Chroma-Core/src/main/java/buttondevteam/core/ComponentCommand.java index 6800c88..1611276 100644 --- a/Chroma-Core/src/main/java/buttondevteam/core/ComponentCommand.java +++ b/Chroma-Core/src/main/java/buttondevteam/core/ComponentCommand.java @@ -6,13 +6,17 @@ import buttondevteam.lib.architecture.Component; import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.Command2.Subcommand; import buttondevteam.lib.chat.CommandClass; +import buttondevteam.lib.chat.CustomTabCompleteMethod; import buttondevteam.lib.chat.ICommand2MC; import lombok.val; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import java.util.Arrays; import java.util.Optional; +import java.util.stream.Stream; @CommandClass(modOnly = true, helpText = { "Component command", @@ -20,7 +24,8 @@ import java.util.Optional; }) public class ComponentCommand extends ICommand2MC { 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 = { @@ -57,6 +62,11 @@ public class ComponentCommand extends ICommand2MC { return true; } + @CustomTabCompleteMethod(param = "component", subcommand = {"enable", "disable"}) + public Iterable 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) { try { val oc = getComponentOrError(plugin, component, sender); @@ -72,10 +82,13 @@ public class ComponentCommand extends ICommand2MC { return true; } + private Stream> getPluginComponents(Plugin plugin) { + return Component.getComponents().values().stream() + .filter(c -> plugin.getName().equals(c.getPlugin().getName())); + } + private Optional> getComponentOrError(Plugin plugin, String arg, CommandSender sender) { - val oc = Component.getComponents().values().stream() - .filter(c -> plugin.getName().equals(c.getPlugin().getName())) - .filter(c -> c.getClass().getSimpleName().equalsIgnoreCase(arg)).findAny(); + val oc = getPluginComponents(plugin).filter(c -> c.getClass().getSimpleName().equalsIgnoreCase(arg)).findAny(); if (!oc.isPresent()) sender.sendMessage("§cComponent not found!"); //^ Much simpler to solve in the new command system return oc; diff --git a/Chroma-Core/src/main/java/buttondevteam/core/component/members/MemberCommand.java b/Chroma-Core/src/main/java/buttondevteam/core/component/members/MemberCommand.java index 207e182..1cc3459 100644 --- a/Chroma-Core/src/main/java/buttondevteam/core/component/members/MemberCommand.java +++ b/Chroma-Core/src/main/java/buttondevteam/core/component/members/MemberCommand.java @@ -7,6 +7,7 @@ import buttondevteam.lib.chat.ICommand2MC; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.bukkit.command.CommandSender; +import org.bukkit.entity.HumanEntity; import org.bukkit.entity.Player; import java.util.concurrent.TimeUnit; @@ -19,7 +20,8 @@ public class MemberCommand extends ICommand2MC { private final 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; } diff --git a/Chroma-Core/src/main/java/buttondevteam/core/component/towny/RemoveResidentsCommand.java b/Chroma-Core/src/main/java/buttondevteam/core/component/towny/RemoveResidentsCommand.java index 03228f9..a740b42 100644 --- a/Chroma-Core/src/main/java/buttondevteam/core/component/towny/RemoveResidentsCommand.java +++ b/Chroma-Core/src/main/java/buttondevteam/core/component/towny/RemoveResidentsCommand.java @@ -2,6 +2,7 @@ package buttondevteam.core.component.towny; import buttondevteam.lib.chat.Command2; import buttondevteam.lib.chat.CommandClass; +import buttondevteam.lib.chat.CustomTabComplete; import buttondevteam.lib.chat.ICommand2MC; import com.palmergames.bukkit.towny.TownySettings; import com.palmergames.bukkit.towny.TownyUniverse; @@ -20,7 +21,7 @@ import java.util.stream.Stream; }) public class RemoveResidentsCommand extends ICommand2MC { @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(), () -> { sender.sendMessage("Starting..."); var ds = TownyUniverse.getInstance().getDataSource(); diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java index d2d1a6b..704e92b 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2.java @@ -27,6 +27,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.function.Function; +import java.util.function.Supplier; /** * The method name is the subcommand, use underlines (_) to add further subcommands. @@ -110,10 +111,11 @@ public abstract class Command2, TP extends Command2Send protected static class ParamConverter { public final Function converter; public final String errormsg; + public final Supplier> allSupplier; } - protected HashMap> subcommands = new HashMap<>(); - private HashMap, ParamConverter> paramConverters = new HashMap<>(); + protected final HashMap> subcommands = new HashMap<>(); + protected final HashMap, ParamConverter> paramConverters = new HashMap<>(); private ArrayList commandHelp = new ArrayList<>(); //Mainly needed by Discord @@ -123,12 +125,14 @@ public abstract class Command2, TP extends Command2Send * Adds a param converter that obtains a specific object from a string parameter. * The converter may return null. * - * @param cl The class of the result object - * @param converter The converter to use - * @param The type of the result + * @param The type of the result + * @param cl The class of the result object + * @param converter The converter to use + * @param allSupplier The supplier of all possible values (ideally) */ - public void addParamConverter(Class cl, Function converter, String errormsg) { - paramConverters.put(cl, new ParamConverter<>(converter, errormsg)); + public void addParamConverter(Class cl, Function converter, String errormsg, + Supplier> allSupplier) { + paramConverters.put(cl, new ParamConverter<>(converter, errormsg, allSupplier)); } public boolean handleCommand(TP sender, String commandline) { @@ -180,12 +184,12 @@ public abstract class Command2, TP extends Command2Send if (sendertype.isAssignableFrom(sender.getClass())) params.add(sender); //The command either expects a CommandSender or it is a Player, or some other expected type else if (sender instanceof Command2MCSender - && sendertype.isAssignableFrom(((Command2MCSender) sender).getSender().getClass())) + && sendertype.isAssignableFrom(((Command2MCSender) sender).getSender().getClass())) params.add(((Command2MCSender) sender).getSender()); else if (ChromaGamerBase.class.isAssignableFrom(sendertype) - && sender instanceof Command2MCSender - && (cg = ChromaGamerBase.getFromSender(((Command2MCSender) sender).getSender())) != null - && cg.getClass() == sendertype) //The command expects a user of our system + && sender instanceof Command2MCSender + && (cg = ChromaGamerBase.getFromSender(((Command2MCSender) sender).getSender())) != null + && cg.getClass() == sendertype) //The command expects a user of our system params.add(cg); else { sender.sendMessage("§cYou need to be a " + sendertype.getSimpleName() + " to use this command."); @@ -201,7 +205,7 @@ public abstract class Command2, TP extends Command2Send if (cl.isPrimitive()) params.add(Defaults.defaultValue(cl)); else if (Number.class.isAssignableFrom(cl) - || Number.class.isAssignableFrom(cl)) + || Number.class.isAssignableFrom(cl)) params.add(Defaults.defaultValue(Primitives.unwrap(cl))); else params.add(null); @@ -309,7 +313,7 @@ public abstract class Command2, TP extends Command2Send var ht = command.getHelpText(method, ann); if (ht != null) { //The method is a subcommand 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]; ht = getParameterHelp(method, ht, subcommand, params); var sd = new SubcommandData<>(method, command, params, ht); diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java index 89d42ce..9d9d4ba 100644 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/Command2MC.java @@ -4,11 +4,12 @@ import buttondevteam.core.MainPlugin; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.architecture.ButtonPlugin; import buttondevteam.lib.architecture.Component; -import buttondevteam.lib.chat.commandargs.BetterStringArgumentType; import com.mojang.brigadier.arguments.*; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.suggestion.SuggestionProvider; import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; import lombok.val; import me.lucko.commodore.Commodore; import me.lucko.commodore.CommodoreProvider; @@ -20,7 +21,9 @@ import org.bukkit.command.CommandSender; import org.bukkit.command.ConsoleCommandSender; import org.bukkit.command.SimpleCommandMap; import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; +import org.bukkit.event.server.TabCompleteEvent; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; @@ -31,6 +34,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.function.Function; +import java.util.function.Supplier; public class Command2MC extends Command2 implements Listener { /** @@ -139,8 +143,8 @@ public class Command2MC extends Command2 implemen * {@see super#addParamConverter} */ @Override - public void addParamConverter(Class cl, Function converter, String errormsg) { - super.addParamConverter(cl, converter, "§c" + errormsg); + public void addParamConverter(Class cl, Function converter, String errormsg, Supplier> allSupplier) { + super.addParamConverter(cl, converter, "§c" + errormsg, allSupplier); } public void unregisterCommands(ButtonPlugin plugin) { @@ -158,6 +162,13 @@ public class Command2MC extends Command2 implemen .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 void registerOfficially(ICommand2MC command, List> subcmds) { @@ -202,6 +213,19 @@ public class Command2MC extends Command2 implemen private static class TabcompleteHelper { private static Commodore commodore; + private static LiteralCommandNode appendSubcommand(String path, CommandNode parent, + SubcommandData 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> subcmds, Command bukkitCommand) { if (commodore == null) commodore = CommodoreProvider.getCommodore(MainPlugin.Instance); //Register all to the Core, it's easier @@ -209,18 +233,15 @@ public class Command2MC extends Command2 implemen var maincmd = LiteralArgumentBuilder.literal(path[0]).build(); var cmd = maincmd; for (int i = 1; i < path.length; i++) { - var subcmd = LiteralArgumentBuilder.literal(path[i]).build(); - cmd.addChild(subcmd); - cmd = subcmd; //Add each part of the path as a child of the previous one + var scmd = subcmds.stream().filter(sd -> sd.method.getName().equals("def")).findAny().orElse(null); + cmd = appendSubcommand(path[i], cmd, scmd); //Add each part of the path as a child of the previous one } for (SubcommandData subcmd : subcmds) { String[] subpath = ButtonPlugin.getCommand2MC().getCommandPath(subcmd.method.getName(), ' ').trim().split(" "); CommandNode scmd = cmd; if (subpath[0].length() > 0) { //If the method is def, it will contain one empty string for (String s : subpath) { - var subsubcmd = LiteralArgumentBuilder.literal(s).build(); - scmd.addChild(subsubcmd); - scmd = subsubcmd; //Add method name part of the path (could_be_multiple()) + scmd = appendSubcommand(s, scmd, subcmd); //Add method name part of the path (could_be_multiple()) } } Parameter[] parameters = subcmd.method.getParameters(); @@ -232,7 +253,7 @@ public class Command2MC extends Command2 implemen if (parameter.isAnnotationPresent(TextArg.class)) type = StringArgumentType.greedyString(); else - type = BetterStringArgumentType.word(); + type = StringArgumentType.word(); else if (ptype == int.class || ptype == Integer.class || ptype == byte.class || ptype == Byte.class || ptype == short.class || ptype == Short.class) @@ -244,12 +265,18 @@ public class Command2MC extends Command2 implemen else if (ptype == double.class || ptype == Double.class) type = DoubleArgumentType.doubleArg(); else if (ptype == char.class || ptype == Character.class) - type = BetterStringArgumentType.word(1); + type = StringArgumentType.word(); else if (ptype == boolean.class || ptype == Boolean.class) type = BoolArgumentType.bool(); else //TODO: Custom parameter types - type = BetterStringArgumentType.word(); - var arg = RequiredArgumentBuilder.argument(subcmd.parameters[i - 1], type).build(); + type = StringArgumentType.word(); + val param = subcmd.parameters[i - 1]; + var argb = RequiredArgumentBuilder.argument(param, type) + .suggests((SuggestionProvider) (context, builder) -> { + //TODO + return builder.suggest(param).buildFuture(); + }); + var arg = argb.build(); scmd.addChild(arg); scmd = arg; } @@ -263,7 +290,7 @@ public class Command2MC extends Command2 implemen } catch (Exception e) { - Client log: Could not deserialize chroma:string e.printStackTrace(); }*/ - commodore.register(bukkitCommand, maincmd); + commodore.register(maincmd); } } } diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CustomTabComplete.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CustomTabComplete.java new file mode 100644 index 0000000..d1b3448 --- /dev/null +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CustomTabComplete.java @@ -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(); +} diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/CustomTabCompleteMethod.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CustomTabCompleteMethod.java new file mode 100644 index 0000000..00a7a16 --- /dev/null +++ b/Chroma-Core/src/main/java/buttondevteam/lib/chat/CustomTabCompleteMethod.java @@ -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}<{@link String}> 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 ""; +} diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commandargs/BetterStringArgumentType.java b/Chroma-Core/src/main/java/buttondevteam/lib/chat/commandargs/BetterStringArgumentType.java deleted file mode 100644 index b77a3da..0000000 --- a/Chroma-Core/src/main/java/buttondevteam/lib/chat/commandargs/BetterStringArgumentType.java +++ /dev/null @@ -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 { - 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 getExamples() { - return StringArgumentType.StringType.SINGLE_WORD.getExamples(); - } -}