From f6e766d45319b731a58627fc3550602da9c70ccb Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Tue, 16 Sep 2025 21:17:20 +0200 Subject: [PATCH] Adds more tab-completion help methods --- .../knarlib/util/TabCompletionHelper.java | 253 +++++++++++++++--- 1 file changed, 213 insertions(+), 40 deletions(-) diff --git a/src/main/java/net/knarcraft/knarlib/util/TabCompletionHelper.java b/src/main/java/net/knarcraft/knarlib/util/TabCompletionHelper.java index eb2da84..ac55144 100644 --- a/src/main/java/net/knarcraft/knarlib/util/TabCompletionHelper.java +++ b/src/main/java/net/knarcraft/knarlib/util/TabCompletionHelper.java @@ -1,10 +1,16 @@ package net.knarcraft.knarlib.util; +import org.bukkit.Bukkit; +import org.bukkit.permissions.Permission; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.StringJoiner; import java.util.function.BiFunction; /** @@ -13,6 +19,9 @@ import java.util.function.BiFunction; @SuppressWarnings("unused") public final class TabCompletionHelper { + private static List plugins; + private static Map> permissions; + private TabCompletionHelper() { } @@ -27,45 +36,40 @@ public final class TabCompletionHelper { * @param filterer

The filterer (filterMatching) to use to filter by typed text

* @return

Available tab-completions

*/ - public static @NotNull List getStringList(@NotNull List values, @NotNull String typedText, - @Nullable BiFunction, String, List> filterer) { + @NotNull + public static List getStringList(@NotNull List values, @NotNull String typedText, + @Nullable BiFunction, String, List> filterer) { + // If the typed text is blank, only filter, if necessary if (typedText.isBlank() || !typedText.contains(",")) { if (filterer != null) { return filterer.apply(values, typedText); } else { return values; } - } else { - // The argument base is everything the player has typed, until the last comma - List arguments = List.of(typedText.split(",")); - String base; - List baseArguments; - if (typedText.endsWith(",")) { - baseArguments = arguments; - } else { - baseArguments = arguments.subList(0, arguments.size() - 1); - } - base = String.join(",", baseArguments) + ","; - String lastArgument = arguments.get(arguments.size() - 1); - - // Filter available values by the text after the last comma only - List filtered; - if (filterer != null && !typedText.endsWith(",")) { - filtered = filterer.apply(new ArrayList<>(values), lastArgument); - } else { - filtered = values; - } - - // Remove any already used values - List unused = new ArrayList<>(); - for (String string : filtered) { - if (!baseArguments.contains(string)) { - unused.add(base + string); - } - } - - return unused; } + + List items = List.of(typedText.split(",")); + + // Filter available values by the text after the last comma only + List filtered; + if (filterer != null && !typedText.endsWith(",")) { + filtered = filterer.apply(new ArrayList<>(values), items.get(items.size() - 1)); + } else { + filtered = values; + } + + // Add the prefix to all filtered values + List baseItems = typedText.endsWith(",") ? items : items.subList(0, items.size() - 1); + String base = String.join(",", baseItems) + ","; + List unused = new ArrayList<>(); + for (String string : filtered) { + // Remove any already used values + if (!baseItems.contains(string)) { + unused.add(base + string); + } + } + + return unused; } /** @@ -75,7 +79,8 @@ public final class TabCompletionHelper { * @param typedText

The text the player has started typing

* @return

The given string values that contain the player's typed text

*/ - public static @NotNull List filterMatchingContains(@NotNull List values, @NotNull String typedText) { + @NotNull + public static List filterMatchingContains(@NotNull List values, @NotNull String typedText) { List configValues = new ArrayList<>(); for (String value : values) { if (value.toLowerCase().contains(typedText.toLowerCase())) { @@ -92,7 +97,8 @@ public final class TabCompletionHelper { * @param typedText

The text the player has started typing

* @return

The given string values that start with the player's typed text

*/ - public static @NotNull List filterMatchingStartsWith(@NotNull List values, @NotNull String typedText) { + @NotNull + public static List filterMatchingStartsWith(@NotNull List values, @NotNull String typedText) { List configValues = new ArrayList<>(); for (String value : values) { if (value.toLowerCase().startsWith(typedText.toLowerCase())) { @@ -103,14 +109,181 @@ public final class TabCompletionHelper { } /** - * Gets the "base" string from the given arguments, not including the incomplete last argument + * Gets a list of enum names from the given enum type * - * @param arguments

The string arguments to get the base of

- * @return

The base string

+ * @param enumType

The type of enum to list

+ * @param

The type of enum to list values for

+ * @return

The names of the enums in the type

*/ - private static @NotNull String getBase(@NotNull List arguments) { - List baseArray = arguments.subList(0, arguments.size() - 1); - return String.join(",", baseArray) + ","; + @NotNull + public static > List getEnumList(Class enumType) { + return Arrays.stream(enumType.getEnumConstants()).map((item) -> item.name().toLowerCase()).toList(); + } + + /** + * Gets tab-completions with only remaining text, from a list of full strings + * + *

This is specifically useful when tab-completing for values that contain spaces. This allows tab-completions to + * only list remaining text missing for tab-completing each value, instead of listing the entire strings.

+ * + * @param arguments

The arguments given by the user

+ * @param filtered

Tab-completions filtered by user input

+ * @param offset

The offset to use, if other arguments are preceding the relevant arguments

+ * @param prefix

The prefix to use before each tab-completion

+ * @return

The cleaned tab-completions

+ */ + @NotNull + public static List getCleanedTabCompletions(@NotNull String[] arguments, @NotNull List filtered, + int offset, @NotNull String prefix) { + List cleaned = new ArrayList<>(); + int offsetCount = arguments.length - offset; + for (String name : filtered) { + int startIndex = arguments.length == 0 ? 0 : (offsetCount > 1 ? offsetCount - 1 : 0); + String[] parts = name.split(" ", -1); + // Skip non-matching entries + if ((offsetCount > 1 && !parts[offsetCount - 2].equalsIgnoreCase(arguments[offsetCount - 2 + offset])) || + startIndex >= parts.length) { + continue; + } + + if (startIndex == 0) { + cleaned.add(prefix + name); + } else { + // Skip the required amount of items + StringBuilder builder = new StringBuilder(prefix).append(parts[startIndex]); + for (int i = startIndex + 1; i < parts.length; i++) { + builder.append(" ").append(parts[i]); + } + cleaned.add(builder.toString()); + } + } + return cleaned; + } + + /** + * Merges all arguments to a string with spaces + * + * @param arguments

The arguments to merge

+ * @param stripLastN

How many of the last arguments to ignore

+ * @return

The merged arguments

+ */ + @NotNull + public static String mergeArguments(@NotNull String[] arguments, int stripLastN) { + return mergeArguments(0, arguments, stripLastN); + } + + /** + * Merges all arguments to a string with spaces + * + * @param stripFirstN

How many of the first arguments to ignore

+ * @param arguments

The arguments to merge

+ * @return

The merged arguments

+ */ + @NotNull + public static String mergeArguments(int stripFirstN, @NotNull String[] arguments) { + return mergeArguments(stripFirstN, arguments, 0); + } + + /** + * Merges all arguments to a string with spaces + * + * @param stripFirstN

How many of the first arguments to ignore

+ * @param arguments

The arguments to merge

+ * @param stripLastN

How many of the last arguments to ignore

+ * @return

The merged arguments

+ */ + @NotNull + public static String mergeArguments(int stripFirstN, @NotNull String[] arguments, int stripLastN) { + StringBuilder builder = new StringBuilder(arguments[stripFirstN]); + for (int i = stripFirstN + 1; i < arguments.length - stripLastN; i++) { + builder.append(" ").append(arguments[i]); + } + return builder.toString(); + } + + /** + * Gets the tab complete value for the permission typed + * + * @param typedNode

The full permission node typed by the player

+ * @return

All known valid auto-complete options

+ */ + @NotNull + private static List tabCompletePermission(@NotNull String typedNode) { + if (plugins == null) { + loadAvailablePermissions(); + } + + String permissionPrefix = parsePermissionPrefix(typedNode); + typedNode = typedNode.replaceFirst(permissionPrefix, typedNode); + if (!typedNode.contains(".")) { + return TabCompletionHelper.filterMatchingStartsWith(plugins, typedNode); + } + + List matchingPermissions = permissions.get(typedNode.substring(0, typedNode.lastIndexOf("."))); + if (matchingPermissions != null) { + List output = TabCompletionHelper.filterMatchingStartsWith(matchingPermissions, typedNode); + return output.stream().map((item) -> permissionPrefix + item).toList(); + } else { + if (!permissionPrefix.isEmpty()) { + return List.of(permissionPrefix); + } else { + return List.of(); + } + } + } + + /** + * Parses comma-separated preceding permission values as a prefix + * + * @param typedNode

The permission node string the user typed

+ * @return

The prefix

+ */ + @NotNull + private static String parsePermissionPrefix(@NotNull String typedNode) { + if (!typedNode.contains(",")) { + return ""; + } + + StringBuilder permissionListString = new StringBuilder(); + String[] permissionList = typedNode.split(","); + for (int i = 0; i < permissionList.length - 1; i++) { + permissionListString.append(permissionList[i]).append(","); + } + return permissionListString.toString(); + } + + /** + * Loads all permissions available from bukkit plugins + */ + private static void loadAvailablePermissions() { + plugins = new ArrayList<>(); + permissions = new HashMap<>(); + + for (Permission permission : Bukkit.getPluginManager().getPermissions()) { + loadPermission(permission.getName()); + } + } + + /** + * Loads a given permission into the proper lists and maps + * + * @param permissionName

The permission to load

+ */ + private static void loadPermission(@NotNull String permissionName) { + String[] permissionParts = permissionName.split("\\."); + if (permissionParts.length == 1) { + plugins.add(permissionParts[0]); + } else if (permissionParts.length > 1) { + StringJoiner pathJoiner = new StringJoiner("."); + for (int j = 0; j < permissionParts.length - 1; j++) { + pathJoiner.add(permissionParts[j]); + } + String path = pathJoiner.toString(); + List permissionList = permissions.computeIfAbsent(path, k -> new ArrayList<>()); + permissionList.add(permissionName); + + loadPermission(path); + } } }