Improve FilteringCommandSuggestionProcessor and adjust default filters (#410)

This commit is contained in:
Jason 2022-11-28 13:23:11 -07:00
parent 6c026f994b
commit eca81f7372
10 changed files with 278 additions and 8 deletions

View file

@ -108,7 +108,8 @@ public abstract class CommandManager<C> {
private CaptionVariableReplacementHandler captionVariableReplacementHandler = new SimpleCaptionVariableReplacementHandler(); private CaptionVariableReplacementHandler captionVariableReplacementHandler = new SimpleCaptionVariableReplacementHandler();
private CommandSyntaxFormatter<C> commandSyntaxFormatter = new StandardCommandSyntaxFormatter<>(); private CommandSyntaxFormatter<C> commandSyntaxFormatter = new StandardCommandSyntaxFormatter<>();
private CommandSuggestionProcessor<C> commandSuggestionProcessor = new FilteringCommandSuggestionProcessor<>(); private CommandSuggestionProcessor<C> commandSuggestionProcessor =
new FilteringCommandSuggestionProcessor<>(FilteringCommandSuggestionProcessor.Filter.startsWith(true));
private CommandRegistrationHandler commandRegistrationHandler; private CommandRegistrationHandler commandRegistrationHandler;
private CaptionRegistry<C> captionRegistry; private CaptionRegistry<C> captionRegistry;
private final AtomicReference<RegistrationState> state = new AtomicReference<>(RegistrationState.BEFORE_REGISTRATION); private final AtomicReference<RegistrationState> state = new AtomicReference<>(RegistrationState.BEFORE_REGISTRATION);

View file

@ -38,4 +38,16 @@ import org.checkerframework.checker.nullness.qual.NonNull;
public interface CommandSuggestionProcessor<C> extends public interface CommandSuggestionProcessor<C> extends
BiFunction<@NonNull CommandPreprocessingContext<C>, @NonNull List<String>, @NonNull List<String>> { BiFunction<@NonNull CommandPreprocessingContext<C>, @NonNull List<String>, @NonNull List<String>> {
/**
* Create a pass through {@link CommandSuggestionProcessor} that simply returns
* the input.
*
* @param <C> sender type
* @return new processor
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
static <C> @NonNull CommandSuggestionProcessor<C> passThrough() {
return (ctx, suggestions) -> suggestions;
}
} }

View file

@ -24,19 +24,46 @@
package cloud.commandframework.execution; package cloud.commandframework.execution;
import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext; import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext;
import java.util.LinkedList; import java.util.ArrayList;
import java.util.List; 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.apiguardian.api.API;
import org.checkerframework.checker.nullness.qual.NonNull; 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 <C> Command sender type * @param <C> Command sender type
*/ */
@API(status = API.Status.STABLE) @API(status = API.Status.STABLE)
public final class FilteringCommandSuggestionProcessor<C> implements CommandSuggestionProcessor<C> { public final class FilteringCommandSuggestionProcessor<C> implements CommandSuggestionProcessor<C> {
private final @NonNull Filter<C> 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<C> filter) {
this.filter = filter;
}
@Override @Override
public @NonNull List<@NonNull String> apply( public @NonNull List<@NonNull String> apply(
final @NonNull CommandPreprocessingContext<C> context, final @NonNull CommandPreprocessingContext<C> context,
@ -46,14 +73,217 @@ public final class FilteringCommandSuggestionProcessor<C> implements CommandSugg
if (context.getInputQueue().isEmpty()) { if (context.getInputQueue().isEmpty()) {
input = ""; input = "";
} else { } else {
input = context.getInputQueue().peek(); input = String.join(" ", context.getInputQueue());
} }
final List<String> suggestions = new LinkedList<>(); final List<String> suggestions = new ArrayList<>(strings.size());
for (final String suggestion : strings) { for (final String suggestion : strings) {
if (suggestion.startsWith(input)) { final @Nullable String filtered = this.filter.filter(context, suggestion, input);
suggestions.add(suggestion); if (filtered != null) {
suggestions.add(filtered);
} }
} }
return suggestions; return suggestions;
} }
/**
* Filter function that tests (and potentially changes) each suggestion against the input and context.
*
* @param <C> sender type
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
@FunctionalInterface
public interface Filter<C> {
/**
* 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<C> 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<C> and(final @NonNull Filter<C> 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<C> 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 <C> sender type
* @return new filter
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
static <C> @NonNull Simple<C> startsWith(final boolean ignoreCase) {
final BiPredicate<String, String> 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 <C> sender type
* @return new filter
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
static <C> @NonNull Simple<C> contains(final boolean ignoreCase) {
final BiPredicate<String, String> 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.
*
* <p>Will return the portion of the suggestion which is after the last space in
* the input.</p>
*
* @param <C> sender type
* @return new filter
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
static <C> @NonNull Filter<C> 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 <C> sender type
* @return filter
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
static <C> @NonNull Filter<C> contextFree(final @NonNull BiFunction<String, String, @Nullable String> 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 <C> sender type
* @return new simple filter
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
static <C> @NonNull Simple<C> simple(final Simple<C> filter) {
return filter;
}
/**
* Simple version of {@link Filter} which doesn't modify suggestions.
*
* <p>Returns boolean instead of nullable String.</p>
*
* @param <C> sender type
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
@FunctionalInterface
interface Simple<C> extends Filter<C> {
/**
* 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<C> context,
@NonNull String suggestion,
@NonNull String input
);
@Override
@SuppressWarnings("FunctionalInterfaceMethodChanged")
default @Nullable String filter(
@NonNull CommandPreprocessingContext<C> 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 <C> sender type
* @return simple filter
* @since 1.8.0
*/
@API(status = API.Status.STABLE, since = "1.8.0")
static <C> @NonNull Simple<C> contextFree(final @NonNull BiPredicate<String, String> test) {
return (ctx, suggestion, input) -> test.test(suggestion, input);
}
}
}
} }

View file

@ -475,7 +475,7 @@ public class CommandSuggestionsTest {
assertThat(suggestions3).containsExactly("--flag", "--flag2"); assertThat(suggestions3).containsExactly("--flag", "--flag2");
assertThat(suggestions4).containsExactly("--flag", "--flag2"); assertThat(suggestions4).containsExactly("--flag", "--flag2");
assertThat(suggestions5).containsExactly("-f"); assertThat(suggestions5).containsExactly("-f");
assertThat(suggestions6).containsExactly("hello"); assertThat(suggestions6).isEmpty();
} }
@Test @Test

View file

@ -55,6 +55,7 @@ import cloud.commandframework.bukkit.parsers.selector.MultiplePlayerSelectorArgu
import cloud.commandframework.bukkit.parsers.selector.SingleEntitySelectorArgument; import cloud.commandframework.bukkit.parsers.selector.SingleEntitySelectorArgument;
import cloud.commandframework.bukkit.parsers.selector.SinglePlayerSelectorArgument; import cloud.commandframework.bukkit.parsers.selector.SinglePlayerSelectorArgument;
import cloud.commandframework.execution.CommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.execution.FilteringCommandSuggestionProcessor;
import cloud.commandframework.tasks.TaskFactory; import cloud.commandframework.tasks.TaskFactory;
import cloud.commandframework.tasks.TaskRecipe; import cloud.commandframework.tasks.TaskRecipe;
import io.leangen.geantyref.TypeToken; import io.leangen.geantyref.TypeToken;
@ -134,6 +135,10 @@ public class BukkitCommandManager<C> extends CommandManager<C> implements Brigad
final BukkitSynchronizer bukkitSynchronizer = new BukkitSynchronizer(owningPlugin); final BukkitSynchronizer bukkitSynchronizer = new BukkitSynchronizer(owningPlugin);
this.taskFactory = new TaskFactory(bukkitSynchronizer); this.taskFactory = new TaskFactory(bukkitSynchronizer);
this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>(
FilteringCommandSuggestionProcessor.Filter.<C>startsWith(true).andTrimBeforeLastSpace()
));
/* Register capabilities */ /* Register capabilities */
CloudBukkitCapabilities.CAPABLE.forEach(this::registerCapability); CloudBukkitCapabilities.CAPABLE.forEach(this::registerCapability);
this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION); this.registerCapability(CloudCapability.StandardCapabilities.ROOT_COMMAND_DELETION);

View file

@ -29,6 +29,7 @@ import cloud.commandframework.bungee.arguments.PlayerArgument;
import cloud.commandframework.bungee.arguments.ServerArgument; import cloud.commandframework.bungee.arguments.ServerArgument;
import cloud.commandframework.captions.FactoryDelegatingCaptionRegistry; import cloud.commandframework.captions.FactoryDelegatingCaptionRegistry;
import cloud.commandframework.execution.CommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.execution.FilteringCommandSuggestionProcessor;
import cloud.commandframework.meta.SimpleCommandMeta; import cloud.commandframework.meta.SimpleCommandMeta;
import io.leangen.geantyref.TypeToken; import io.leangen.geantyref.TypeToken;
import java.util.function.Function; import java.util.function.Function;
@ -76,6 +77,10 @@ public class BungeeCommandManager<C> extends CommandManager<C> {
this.commandSenderMapper = commandSenderMapper; this.commandSenderMapper = commandSenderMapper;
this.backwardsCommandSenderMapper = backwardsCommandSenderMapper; this.backwardsCommandSenderMapper = backwardsCommandSenderMapper;
this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>(
FilteringCommandSuggestionProcessor.Filter.<C>startsWith(true).andTrimBeforeLastSpace()
));
/* Register Bungee Preprocessor */ /* Register Bungee Preprocessor */
this.registerCommandPreProcessor(new BungeeCommandPreprocessor<>(this)); this.registerCommandPreProcessor(new BungeeCommandPreprocessor<>(this));

View file

@ -26,6 +26,7 @@ package cloud.commandframework.cloudburst;
import cloud.commandframework.CommandManager; import cloud.commandframework.CommandManager;
import cloud.commandframework.CommandTree; import cloud.commandframework.CommandTree;
import cloud.commandframework.execution.CommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.execution.FilteringCommandSuggestionProcessor;
import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.CommandMeta;
import cloud.commandframework.meta.SimpleCommandMeta; import cloud.commandframework.meta.SimpleCommandMeta;
import java.util.function.Function; import java.util.function.Function;
@ -69,6 +70,9 @@ public class CloudburstCommandManager<C> extends CommandManager<C> {
this.commandSenderMapper = commandSenderMapper; this.commandSenderMapper = commandSenderMapper;
this.backwardsCommandSenderMapper = backwardsCommandSenderMapper; this.backwardsCommandSenderMapper = backwardsCommandSenderMapper;
this.owningPlugin = owningPlugin; this.owningPlugin = owningPlugin;
this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>(
FilteringCommandSuggestionProcessor.Filter.<C>startsWith(true).andTrimBeforeLastSpace()
));
// Prevent commands from being registered when the server would reject them anyways // Prevent commands from being registered when the server would reject them anyways
this.owningPlugin.getServer().getPluginManager().registerEvent( this.owningPlugin.getServer().getPluginManager().registerEvent(

View file

@ -32,6 +32,7 @@ import cloud.commandframework.brigadier.argument.WrappedBrigadierParser;
import cloud.commandframework.context.CommandContext; import cloud.commandframework.context.CommandContext;
import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator; import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator;
import cloud.commandframework.execution.CommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.execution.FilteringCommandSuggestionProcessor;
import cloud.commandframework.fabric.argument.FabricArgumentParsers; import cloud.commandframework.fabric.argument.FabricArgumentParsers;
import cloud.commandframework.fabric.argument.RegistryEntryArgument; import cloud.commandframework.fabric.argument.RegistryEntryArgument;
import cloud.commandframework.fabric.argument.TeamArgument; import cloud.commandframework.fabric.argument.TeamArgument;
@ -154,6 +155,9 @@ public abstract class FabricCommandManager<C, S extends SharedSuggestionProvider
this.registerNativeBrigadierMappings(this.brigadierManager); this.registerNativeBrigadierMappings(this.brigadierManager);
this.captionRegistry(new FabricCaptionRegistry<>()); this.captionRegistry(new FabricCaptionRegistry<>());
this.registerCommandPreProcessor(new FabricCommandPreprocessor<>(this)); this.registerCommandPreProcessor(new FabricCommandPreprocessor<>(this));
this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>(
FilteringCommandSuggestionProcessor.Filter.<C>startsWith(true).andTrimBeforeLastSpace()
));
((FabricCommandRegistrationHandler<C, S>) this.commandRegistrationHandler()).initialize(this); ((FabricCommandRegistrationHandler<C, S>) this.commandRegistrationHandler()).initialize(this);
} }

View file

@ -27,6 +27,7 @@ import cloud.commandframework.CommandManager;
import cloud.commandframework.CommandTree; import cloud.commandframework.CommandTree;
import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator; import cloud.commandframework.execution.AsynchronousCommandExecutionCoordinator;
import cloud.commandframework.execution.CommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.execution.FilteringCommandSuggestionProcessor;
import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.CommandMeta;
import cloud.commandframework.meta.SimpleCommandMeta; import cloud.commandframework.meta.SimpleCommandMeta;
import java.util.function.Function; import java.util.function.Function;
@ -78,6 +79,9 @@ public class SpongeCommandManager<C> extends CommandManager<C> {
this.owningPlugin = requireNonNull(container, "container"); this.owningPlugin = requireNonNull(container, "container");
this.forwardMapper = requireNonNull(forwardMapper, "forwardMapper"); this.forwardMapper = requireNonNull(forwardMapper, "forwardMapper");
this.reverseMapper = requireNonNull(reverseMapper, "reverseMapper"); this.reverseMapper = requireNonNull(reverseMapper, "reverseMapper");
this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>(
FilteringCommandSuggestionProcessor.Filter.<C>startsWith(true).andTrimBeforeLastSpace()
));
((SpongePluginRegistrationHandler<C>) this.commandRegistrationHandler()).initialize(this); ((SpongePluginRegistrationHandler<C>) this.commandRegistrationHandler()).initialize(this);
} }

View file

@ -29,6 +29,7 @@ import cloud.commandframework.brigadier.BrigadierManagerHolder;
import cloud.commandframework.brigadier.CloudBrigadierManager; import cloud.commandframework.brigadier.CloudBrigadierManager;
import cloud.commandframework.captions.FactoryDelegatingCaptionRegistry; import cloud.commandframework.captions.FactoryDelegatingCaptionRegistry;
import cloud.commandframework.execution.CommandExecutionCoordinator; import cloud.commandframework.execution.CommandExecutionCoordinator;
import cloud.commandframework.execution.FilteringCommandSuggestionProcessor;
import cloud.commandframework.meta.CommandMeta; import cloud.commandframework.meta.CommandMeta;
import cloud.commandframework.meta.SimpleCommandMeta; import cloud.commandframework.meta.SimpleCommandMeta;
import cloud.commandframework.velocity.arguments.PlayerArgument; import cloud.commandframework.velocity.arguments.PlayerArgument;
@ -114,6 +115,10 @@ public class VelocityCommandManager<C> extends CommandManager<C> implements Brig
this.commandSenderMapper = commandSenderMapper; this.commandSenderMapper = commandSenderMapper;
this.backwardsCommandSenderMapper = backwardsCommandSenderMapper; this.backwardsCommandSenderMapper = backwardsCommandSenderMapper;
this.commandSuggestionProcessor(new FilteringCommandSuggestionProcessor<>(
FilteringCommandSuggestionProcessor.Filter.<C>startsWith(true).andTrimBeforeLastSpace()
));
/* Register Velocity Preprocessor */ /* Register Velocity Preprocessor */
this.registerCommandPreProcessor(new VelocityCommandPreprocessor<>(this)); this.registerCommandPreProcessor(new VelocityCommandPreprocessor<>(this));