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 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 CaptionRegistry<C> captionRegistry;
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
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;
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 <C> Command sender type
*/
@API(status = API.Status.STABLE)
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
public @NonNull List<@NonNull String> apply(
final @NonNull CommandPreprocessingContext<C> context,
@ -46,14 +73,217 @@ public final class FilteringCommandSuggestionProcessor<C> implements CommandSugg
if (context.getInputQueue().isEmpty()) {
input = "";
} 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) {
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 <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(suggestions4).containsExactly("--flag", "--flag2");
assertThat(suggestions5).containsExactly("-f");
assertThat(suggestions6).containsExactly("hello");
assertThat(suggestions6).isEmpty();
}
@Test