diff --git a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java index a5bbf982..05483a90 100644 --- a/cloud-core/src/main/java/cloud/commandframework/CommandManager.java +++ b/cloud-core/src/main/java/cloud/commandframework/CommandManager.java @@ -108,7 +108,8 @@ public abstract class CommandManager { private CaptionVariableReplacementHandler captionVariableReplacementHandler = new SimpleCaptionVariableReplacementHandler(); private CommandSyntaxFormatter commandSyntaxFormatter = new StandardCommandSyntaxFormatter<>(); - private CommandSuggestionProcessor commandSuggestionProcessor = new FilteringCommandSuggestionProcessor<>(); + private CommandSuggestionProcessor commandSuggestionProcessor = + new FilteringCommandSuggestionProcessor<>(FilteringCommandSuggestionProcessor.Filter.startsWith(true)); private CommandRegistrationHandler commandRegistrationHandler; private CaptionRegistry captionRegistry; private final AtomicReference state = new AtomicReference<>(RegistrationState.BEFORE_REGISTRATION); diff --git a/cloud-core/src/main/java/cloud/commandframework/execution/CommandSuggestionProcessor.java b/cloud-core/src/main/java/cloud/commandframework/execution/CommandSuggestionProcessor.java index 235036f1..173d258f 100644 --- a/cloud-core/src/main/java/cloud/commandframework/execution/CommandSuggestionProcessor.java +++ b/cloud-core/src/main/java/cloud/commandframework/execution/CommandSuggestionProcessor.java @@ -38,4 +38,16 @@ import org.checkerframework.checker.nullness.qual.NonNull; public interface CommandSuggestionProcessor extends BiFunction<@NonNull CommandPreprocessingContext, @NonNull List, @NonNull List> { + /** + * Create a pass through {@link CommandSuggestionProcessor} that simply returns + * the input. + * + * @param sender type + * @return new processor + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + static @NonNull CommandSuggestionProcessor passThrough() { + return (ctx, suggestions) -> suggestions; + } } diff --git a/cloud-core/src/main/java/cloud/commandframework/execution/FilteringCommandSuggestionProcessor.java b/cloud-core/src/main/java/cloud/commandframework/execution/FilteringCommandSuggestionProcessor.java index 1b8e0cff..99a5c4b4 100644 --- a/cloud-core/src/main/java/cloud/commandframework/execution/FilteringCommandSuggestionProcessor.java +++ b/cloud-core/src/main/java/cloud/commandframework/execution/FilteringCommandSuggestionProcessor.java @@ -24,19 +24,46 @@ package cloud.commandframework.execution; import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; +import java.util.Locale; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; import org.apiguardian.api.API; import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; /** - * Command suggestion processor that checks the input queue head and filters based on that + * Command suggestion processor filters suggestions based on the remaining unconsumed input in the + * queue. * * @param Command sender type */ @API(status = API.Status.STABLE) public final class FilteringCommandSuggestionProcessor implements CommandSuggestionProcessor { + private final @NonNull Filter filter; + + /** + * Create a new {@link FilteringCommandSuggestionProcessor} filtering with {@link String#startsWith(String)} that does + * not ignore case. + */ + @API(status = API.Status.STABLE) + public FilteringCommandSuggestionProcessor() { + this(Filter.startsWith(false)); + } + + /** + * Create a new {@link FilteringCommandSuggestionProcessor}. + * + * @param filter mode + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + public FilteringCommandSuggestionProcessor(final @NonNull Filter filter) { + this.filter = filter; + } + @Override public @NonNull List<@NonNull String> apply( final @NonNull CommandPreprocessingContext context, @@ -46,14 +73,217 @@ public final class FilteringCommandSuggestionProcessor implements CommandSugg if (context.getInputQueue().isEmpty()) { input = ""; } else { - input = context.getInputQueue().peek(); + input = String.join(" ", context.getInputQueue()); } - final List suggestions = new LinkedList<>(); + final List suggestions = new ArrayList<>(strings.size()); for (final String suggestion : strings) { - if (suggestion.startsWith(input)) { - suggestions.add(suggestion); + final @Nullable String filtered = this.filter.filter(context, suggestion, input); + if (filtered != null) { + suggestions.add(filtered); } } return suggestions; } + + /** + * Filter function that tests (and potentially changes) each suggestion against the input and context. + * + * @param sender type + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + @FunctionalInterface + public interface Filter { + + /** + * Filters a potential suggestion against the input and context. + * + * @param context context + * @param suggestion potential suggestion + * @param input remaining unconsumed input + * @return possibly modified suggestion or null to deny + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + @Nullable String filter( + @NonNull CommandPreprocessingContext context, + @NonNull String suggestion, + @NonNull String input + ); + + /** + * Returns a new {@link Filter} which tests this filter, and if the result + * is non-null, then filters with {@code and}. + * + * @param and next filter + * @return combined filter + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + default @NonNull Filter and(final @NonNull Filter and) { + return (ctx, suggestion, input) -> { + final @Nullable String filtered = this.filter(ctx, suggestion, input); + if (filtered == null) { + return null; + } + return and.filter(ctx, filtered, input); + }; + } + + /** + * Returns a new {@link Filter} that tests this filter, and then + * uses {@link #trimBeforeLastSpace()} if the result is non-null. + * + * @return combined filter + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + default Filter andTrimBeforeLastSpace() { + return this.and(trimBeforeLastSpace()); + } + + /** + * Create a filter using {@link String#startsWith(String)} that can optionally ignore case. + * + * @param ignoreCase whether to ignore case + * @param sender type + * @return new filter + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + static @NonNull Simple startsWith(final boolean ignoreCase) { + final BiPredicate test = ignoreCase + ? (suggestion, input) -> suggestion.toLowerCase(Locale.ROOT).startsWith(input.toLowerCase(Locale.ROOT)) + : String::startsWith; + return Simple.contextFree(test); + } + + /** + * Create a filter using {@link String#contains(CharSequence)} that can optionally ignore case. + * + * @param ignoreCase whether to ignore case + * @param sender type + * @return new filter + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + static @NonNull Simple contains(final boolean ignoreCase) { + final BiPredicate test = ignoreCase + ? (suggestion, input) -> suggestion.toLowerCase(Locale.ROOT).contains(input.toLowerCase(Locale.ROOT)) + : String::contains; + return Simple.contextFree(test); + } + + /** + * Create a filter which does extra processing when the input contains spaces. + * + *

Will return the portion of the suggestion which is after the last space in + * the input.

+ * + * @param sender type + * @return new filter + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + static @NonNull Filter trimBeforeLastSpace() { + return (context, suggestion, input) -> { + final int lastSpace = input.lastIndexOf(' '); + // No spaces in input, don't do anything + if (lastSpace == -1) { + return suggestion; + } + + // Always use case-insensitive here. If case-sensitive filtering is desired it should + // be done in another filter which this is appended to using #and/#andTrimBeforeLastSpace. + if (suggestion.toLowerCase(Locale.ROOT).startsWith(input.toLowerCase(Locale.ROOT).substring(0, lastSpace))) { + return suggestion.substring(lastSpace + 1); + } + + return null; + }; + } + + /** + * Create a new context-free {@link Filter}. + * + * @param function function + * @param sender type + * @return filter + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + static @NonNull Filter contextFree(final @NonNull BiFunction function) { + return (ctx, suggestion, input) -> function.apply(suggestion, input); + } + + /** + * Create a new {@link Simple}. This is a convenience method to allow + * for more easily implementing {@link Simple} using a lambda without + * casting, for methods which accept {@link Filter}. + * + * @param filter filter lambda + * @param sender type + * @return new simple filter + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + static @NonNull Simple simple(final Simple filter) { + return filter; + } + + /** + * Simple version of {@link Filter} which doesn't modify suggestions. + * + *

Returns boolean instead of nullable String.

+ * + * @param sender type + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + @FunctionalInterface + interface Simple extends Filter { + + /** + * Tests a suggestion against the context and input. + * + * @param context context + * @param suggestion potential suggestion + * @param input remaining unconsumed input + * @return whether to accept the suggestion + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + boolean test( + @NonNull CommandPreprocessingContext context, + @NonNull String suggestion, + @NonNull String input + ); + + @Override + @SuppressWarnings("FunctionalInterfaceMethodChanged") + default @Nullable String filter( + @NonNull CommandPreprocessingContext context, + @NonNull String suggestion, + @NonNull String input + ) { + if (this.test(context, suggestion, input)) { + return suggestion; + } + return null; + } + + /** + * Create a new context-free {@link Simple}. + * + * @param test predicate + * @param sender type + * @return simple filter + * @since 1.8.0 + */ + @API(status = API.Status.STABLE, since = "1.8.0") + static @NonNull Simple contextFree(final @NonNull BiPredicate test) { + return (ctx, suggestion, input) -> test.test(suggestion, input); + } + } + } } diff --git a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java index a307fc31..818b99cc 100644 --- a/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java +++ b/cloud-core/src/test/java/cloud/commandframework/CommandSuggestionsTest.java @@ -475,7 +475,7 @@ public class CommandSuggestionsTest { assertThat(suggestions3).containsExactly("--flag", "--flag2"); assertThat(suggestions4).containsExactly("--flag", "--flag2"); assertThat(suggestions5).containsExactly("-f"); - assertThat(suggestions6).containsExactly("hello"); + assertThat(suggestions6).isEmpty(); } @Test diff --git a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java index a6990eab..9177898b 100644 --- a/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java +++ b/cloud-minecraft/cloud-bukkit/src/main/java/cloud/commandframework/bukkit/BukkitCommandManager.java @@ -55,6 +55,7 @@ import cloud.commandframework.bukkit.parsers.selector.MultiplePlayerSelectorArgu import cloud.commandframework.bukkit.parsers.selector.SingleEntitySelectorArgument; import cloud.commandframework.bukkit.parsers.selector.SinglePlayerSelectorArgument; import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.execution.FilteringCommandSuggestionProcessor; import cloud.commandframework.tasks.TaskFactory; import cloud.commandframework.tasks.TaskRecipe; import io.leangen.geantyref.TypeToken; @@ -134,6 +135,10 @@ public class BukkitCommandManager extends CommandManager implements Brigad final BukkitSynchronizer bukkitSynchronizer = new BukkitSynchronizer(owningPlugin); this.taskFactory = new TaskFactory(bukkitSynchronizer); + this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>( + FilteringCommandSuggestionProcessor.Filter.startsWith(true).andTrimBeforeLastSpace() + )); + /* Register capabilities */ CloudBukkitCapabilities.CAPABLE.forEach(this::registerCapability); this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION); diff --git a/cloud-minecraft/cloud-bungee/src/main/java/cloud/commandframework/bungee/BungeeCommandManager.java b/cloud-minecraft/cloud-bungee/src/main/java/cloud/commandframework/bungee/BungeeCommandManager.java index 4fbf2882..5c1189e1 100644 --- a/cloud-minecraft/cloud-bungee/src/main/java/cloud/commandframework/bungee/BungeeCommandManager.java +++ b/cloud-minecraft/cloud-bungee/src/main/java/cloud/commandframework/bungee/BungeeCommandManager.java @@ -29,6 +29,7 @@ import cloud.commandframework.bungee.arguments.PlayerArgument; import cloud.commandframework.bungee.arguments.ServerArgument; import cloud.commandframework.captions.FactoryDelegatingCaptionRegistry; import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.execution.FilteringCommandSuggestionProcessor; import cloud.commandframework.meta.SimpleCommandMeta; import io.leangen.geantyref.TypeToken; import java.util.function.Function; @@ -76,6 +77,10 @@ public class BungeeCommandManager extends CommandManager { this.commandSenderMapper = commandSenderMapper; this.backwardsCommandSenderMapper = backwardsCommandSenderMapper; + this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>( + FilteringCommandSuggestionProcessor.Filter.startsWith(true).andTrimBeforeLastSpace() + )); + /* Register Bungee Preprocessor */ this.registerCommandPreProcessor(new BungeeCommandPreprocessor<>(this)); diff --git a/cloud-minecraft/cloud-cloudburst/src/main/java/cloud/commandframework/cloudburst/CloudburstCommandManager.java b/cloud-minecraft/cloud-cloudburst/src/main/java/cloud/commandframework/cloudburst/CloudburstCommandManager.java index c6d7469c..e16845c0 100644 --- a/cloud-minecraft/cloud-cloudburst/src/main/java/cloud/commandframework/cloudburst/CloudburstCommandManager.java +++ b/cloud-minecraft/cloud-cloudburst/src/main/java/cloud/commandframework/cloudburst/CloudburstCommandManager.java @@ -26,6 +26,7 @@ package cloud.commandframework.cloudburst; import cloud.commandframework.CommandManager; import cloud.commandframework.CommandTree; import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.execution.FilteringCommandSuggestionProcessor; import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; import java.util.function.Function; @@ -69,6 +70,9 @@ public class CloudburstCommandManager extends CommandManager { this.commandSenderMapper = commandSenderMapper; this.backwardsCommandSenderMapper = backwardsCommandSenderMapper; this.owningPlugin = owningPlugin; + this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>( + FilteringCommandSuggestionProcessor.Filter.startsWith(true).andTrimBeforeLastSpace() + )); // Prevent commands from being registered when the server would reject them anyways this.owningPlugin.getServer().getPluginManager().registerEvent( diff --git a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCommandManager.java b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCommandManager.java index 55f6e593..09830d35 100644 --- a/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCommandManager.java +++ b/cloud-minecraft/cloud-fabric/src/main/java/cloud/commandframework/fabric/FabricCommandManager.java @@ -32,6 +32,7 @@ import cloud.commandframework.brigadier.argument.WrappedBrigadierParser; import cloud.commandframework.context.CommandContext; import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.execution.FilteringCommandSuggestionProcessor; import cloud.commandframework.fabric.argument.FabricArgumentParsers; import cloud.commandframework.fabric.argument.RegistryEntryArgument; import cloud.commandframework.fabric.argument.TeamArgument; @@ -154,6 +155,9 @@ public abstract class FabricCommandManager()); this.registerCommandPreProcessor(new FabricCommandPreprocessor<>(this)); + this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>( + FilteringCommandSuggestionProcessor.Filter.startsWith(true).andTrimBeforeLastSpace() + )); ((FabricCommandRegistrationHandler) this.commandRegistrationHandler()).initialize(this); } diff --git a/cloud-minecraft/cloud-sponge7/src/main/java/cloud/commandframework/sponge7/SpongeCommandManager.java b/cloud-minecraft/cloud-sponge7/src/main/java/cloud/commandframework/sponge7/SpongeCommandManager.java index c954deb7..f8d2b980 100644 --- a/cloud-minecraft/cloud-sponge7/src/main/java/cloud/commandframework/sponge7/SpongeCommandManager.java +++ b/cloud-minecraft/cloud-sponge7/src/main/java/cloud/commandframework/sponge7/SpongeCommandManager.java @@ -27,6 +27,7 @@ import cloud.commandframework.CommandManager; import cloud.commandframework.CommandTree; import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.execution.FilteringCommandSuggestionProcessor; import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; import java.util.function.Function; @@ -78,6 +79,9 @@ public class SpongeCommandManager extends CommandManager { this.owningPlugin = requireNonNull(container, "container"); this.forwardMapper = requireNonNull(forwardMapper, "forwardMapper"); this.reverseMapper = requireNonNull(reverseMapper, "reverseMapper"); + this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>( + FilteringCommandSuggestionProcessor.Filter.startsWith(true).andTrimBeforeLastSpace() + )); ((SpongePluginRegistrationHandler) this.commandRegistrationHandler()).initialize(this); } diff --git a/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityCommandManager.java b/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityCommandManager.java index 88850b03..6bbb05e2 100644 --- a/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityCommandManager.java +++ b/cloud-minecraft/cloud-velocity/src/main/java/cloud/commandframework/velocity/VelocityCommandManager.java @@ -29,6 +29,7 @@ import cloud.commandframework.brigadier.BrigadierManagerHolder; import cloud.commandframework.brigadier.CloudBrigadierManager; import cloud.commandframework.captions.FactoryDelegatingCaptionRegistry; import cloud.commandframework.execution.CommandExecutionCoordinator; +import cloud.commandframework.execution.FilteringCommandSuggestionProcessor; import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.SimpleCommandMeta; import cloud.commandframework.velocity.arguments.PlayerArgument; @@ -114,6 +115,10 @@ public class VelocityCommandManager extends CommandManager implements Brig this.commandSenderMapper = commandSenderMapper; this.backwardsCommandSenderMapper = backwardsCommandSenderMapper; + this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>( + FilteringCommandSuggestionProcessor.Filter.startsWith(true).andTrimBeforeLastSpace() + )); + /* Register Velocity Preprocessor */ this.registerCommandPreProcessor(new VelocityCommandPreprocessor<>(this));