Skip to content

Commit ea560c6

Browse files
Simple tab completion system
1 parent 813cc99 commit ea560c6

8 files changed

Lines changed: 307 additions & 26 deletions

File tree

src/main/java/org/skriptlang/skript/bukkit/command/CommandModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ protected void loadSelf(SkriptAddon addon) {
3737
ExprCommandExecutor::register,
3838
ExprCommandInfo::register,
3939
ExprCommandSender::register,
40+
ExprCommandSuggestions::register,
4041
syntaxRegistry -> StructCommand.register(addon, syntaxRegistry)
4142
);
4243
}

src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptArgumentType.java

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.jetbrains.annotations.ApiStatus;
3434
import org.jetbrains.annotations.NotNull;
3535
import org.jetbrains.annotations.Nullable;
36+
import org.skriptlang.skript.bukkit.command.elements.structures.util.ScriptSuggestionProvider;
3637

3738
import java.util.Iterator;
3839
import java.util.Locale;
@@ -193,26 +194,11 @@ public ScriptArgumentType(@NotNull ArgumentData<T> argument, @NotNull StringArgu
193194
if (supplier == null) {
194195
return Suggestions.empty();
195196
}
196-
197-
// treat <foo b> as a valid match for <foo_bar>
198-
// treat <"foo b> as a valid match for <foo_bar>
199-
supplier.get().forEachRemaining(value -> {
200-
if (value == null) {
201-
return;
202-
}
203-
String name = Classes.toString(value).toLowerCase(Locale.ENGLISH)
204-
.replace(' ', '_');
205-
String remaining = builder.getRemainingLowerCase();
206-
if (!remaining.isEmpty() && remaining.charAt(0) == '"') {
207-
remaining = remaining.substring(1);
208-
}
209-
if (remaining.contains(" ")) {
210-
remaining = remaining.replace(' ', '_');
211-
}
212-
if (name.startsWith(remaining)) {
213-
builder.suggest(name);
214-
}
215-
});
197+
Iterator<T> iterator = supplier.get();
198+
while (iterator.hasNext()) {
199+
ScriptSuggestionProvider.suggest(builder, Classes.toString(iterator.next()).toLowerCase(Locale.ENGLISH)
200+
.replace(' ', '_'));
201+
}
216202

217203
return builder.buildFuture();
218204
}

src/main/java/org/skriptlang/skript/bukkit/command/custom/ScriptCommandRegistrar.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.bukkit.help.HelpTopicComparator;
1414
import org.bukkit.help.IndexHelpTopic;
1515
import org.bukkit.plugin.java.JavaPlugin;
16+
import org.jetbrains.annotations.ApiStatus;
1617
import org.jetbrains.annotations.Nullable;
1718

1819
import java.lang.invoke.MethodHandle;
@@ -50,6 +51,7 @@ public final class ScriptCommandRegistrar {
5051

5152
private static final SkriptIndexHelpTopic indexHelpTopic = new SkriptIndexHelpTopic();
5253

54+
@ApiStatus.Internal
5355
public static void init(JavaPlugin plugin) {
5456
plugin.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> {
5557
commandRegistrar = commands.registrar();

src/main/java/org/skriptlang/skript/bukkit/command/elements/expressions/ExprArgument.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.bukkit.event.server.ServerCommandEvent;
2323
import org.jetbrains.annotations.Nullable;
2424
import org.skriptlang.skript.bukkit.command.custom.ArgumentData;
25+
import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandSuggestionEvent;
2526
import org.skriptlang.skript.bukkit.command.custom.ScriptCommandEvent;
2627
import org.skriptlang.skript.bukkit.command.custom.CommandParsingData;
2728
import org.skriptlang.skript.registration.SyntaxInfo;
@@ -69,14 +70,14 @@ private enum ArgumentType {
6970
private ArgumentType type;
7071
private int ordinal = -1; // Available in ORDINAL and sometimes CLASSINFO
7172

72-
private @Nullable ArgumentData<?> argument;
73+
@Nullable ArgumentData<?> argument;
7374

7475
private boolean couldCauseArithmeticConfusion = false;
7576

7677
@Override
7778
public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
7879
ParserInstance parser = getParser();
79-
boolean scriptCommand = parser.isCurrentEvent(ScriptCommandEvent.class);
80+
boolean scriptCommand = parser.isCurrentEvent(ScriptCommandEvent.class, CommandSuggestionEvent.class);
8081

8182
type = switch (matchedPattern) {
8283
case 0 -> ArgumentType.LAST;
@@ -192,11 +193,19 @@ public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelaye
192193

193194
@Override
194195
public Class<? extends Event>[] supportedEvents() {
195-
return CollectionUtils.array(ScriptCommandEvent.class, PlayerCommandPreprocessEvent.class, ServerCommandEvent.class);
196+
// important note: this expression is not actually evaluated for CommandSuggestionEvent
197+
return CollectionUtils.array(ScriptCommandEvent.class, PlayerCommandPreprocessEvent.class, ServerCommandEvent.class,
198+
CommandSuggestionEvent.class);
196199
}
197200

198201
@Override
199202
protected Object @Nullable [] get(Event event) {
203+
if (event instanceof CommandSuggestionEvent) {
204+
error("Arguments cannot be obtained before the command is executed!");
205+
assert argument != null;
206+
return (Object[]) Array.newInstance(argument.type().getC(), 0);
207+
}
208+
200209
if (argument != null) {
201210
Object value = ((ScriptCommandEvent) event).getArgument(argument.name());
202211
if (argument.isSingle()) {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package org.skriptlang.skript.bukkit.command.elements.expressions;
2+
3+
import ch.njol.skript.Skript;
4+
import ch.njol.skript.classes.Changer.ChangeMode;
5+
import ch.njol.skript.doc.Description;
6+
import ch.njol.skript.doc.Example;
7+
import ch.njol.skript.doc.Name;
8+
import ch.njol.skript.doc.Since;
9+
import ch.njol.skript.expressions.base.PropertyExpression;
10+
import ch.njol.skript.lang.EventRestrictedSyntax;
11+
import ch.njol.skript.lang.Expression;
12+
import ch.njol.skript.lang.SkriptParser.ParseResult;
13+
import ch.njol.skript.lang.util.SimpleExpression;
14+
import ch.njol.skript.registrations.Classes;
15+
import ch.njol.util.Kleenean;
16+
import org.bukkit.event.Event;
17+
import org.jetbrains.annotations.Nullable;
18+
import org.skriptlang.skript.bukkit.command.elements.structures.util.CommandSuggestionEvent;
19+
import org.skriptlang.skript.registration.SyntaxRegistry;
20+
21+
import java.util.ArrayList;
22+
import java.util.List;
23+
24+
@Name("Script Command Suggestions")
25+
@Description("""
26+
Usable in a script command's "suggestions" entry.
27+
Allows modifying the suggestions displayed while a command is being typed.
28+
""")
29+
@Example("""
30+
command /menu <text>:
31+
suggestions:
32+
set the text argument's suggestions to "Appetizers", "Entrees", and "Desserts"
33+
trigger:
34+
send "Yum!"
35+
""")
36+
@Since("INSERT VERSION")
37+
public class ExprCommandSuggestions extends SimpleExpression<String> implements EventRestrictedSyntax {
38+
39+
public static void register(SyntaxRegistry syntaxRegistry) {
40+
syntaxRegistry.register(SyntaxRegistry.EXPRESSION, PropertyExpression.infoBuilder(ExprCommandSuggestions.class, String.class,
41+
"[command] (suggestions|tab completions)", "objects", false)
42+
.supplier(ExprCommandSuggestions::new)
43+
.build());
44+
}
45+
46+
private ExprArgument argument;
47+
48+
@Override
49+
public boolean init(Expression<?>[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
50+
if (!(expressions[0] instanceof ExprArgument exprArgument) || exprArgument.argument == null) {
51+
Skript.error("It is only possible to obtain or change command suggestions of an argument!");
52+
return false;
53+
}
54+
argument = exprArgument;
55+
return true;
56+
}
57+
58+
@Override
59+
public Class<? extends Event>[] supportedEvents() {
60+
//noinspection unchecked
61+
return new Class[]{CommandSuggestionEvent.class};
62+
}
63+
64+
@Override
65+
protected String @Nullable [] get(Event event) {
66+
assert argument.argument != null;
67+
List<String> suggestions = ((CommandSuggestionEvent) event).suggestions.get(argument.argument.name());
68+
return suggestions == null ? new String[0] : suggestions.toArray(new String[0]);
69+
}
70+
71+
@Override
72+
public Class<?> @Nullable [] acceptChange(ChangeMode mode) {
73+
return switch (mode) {
74+
case ADD, SET, REMOVE, DELETE, RESET -> new Class[]{String[].class, argument.getReturnType().arrayType()};
75+
default -> null;
76+
};
77+
}
78+
79+
@Override
80+
public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
81+
assert argument.argument != null;
82+
String argumentName = argument.argument.name();
83+
84+
if (mode == ChangeMode.RESET) {
85+
((CommandSuggestionEvent) event).suggestions.remove(argumentName);
86+
return;
87+
}
88+
89+
List<String> currentSuggestions = ((CommandSuggestionEvent) event).suggestions
90+
.computeIfAbsent(argumentName, ignored -> new ArrayList<>());
91+
92+
switch (mode) {
93+
case SET:
94+
currentSuggestions.clear();
95+
//$FALL-THROUGH$
96+
case ADD:
97+
assert delta != null;
98+
for (Object object : delta) {
99+
if (object instanceof String string) {
100+
currentSuggestions.add(string);
101+
} else {
102+
currentSuggestions.add(Classes.toString(object));
103+
}
104+
}
105+
break;
106+
case REMOVE:
107+
assert delta != null;
108+
for (Object object : delta) {
109+
if (object instanceof String string) {
110+
currentSuggestions.remove(string);
111+
} else {
112+
currentSuggestions.remove(Classes.toString(object));
113+
}
114+
}
115+
break;
116+
case DELETE:
117+
currentSuggestions.clear();
118+
break;
119+
}
120+
}
121+
122+
@Override
123+
public boolean isSingle() {
124+
return false;
125+
}
126+
127+
@Override
128+
public Class<? extends String> getReturnType() {
129+
return String.class;
130+
}
131+
132+
@Override
133+
public String toString(@Nullable Event event, boolean debug) {
134+
return "the command suggestions of " + argument.toString(event, debug);
135+
}
136+
137+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package org.skriptlang.skript.bukkit.command.elements.structures.util;
2+
3+
import org.bukkit.event.Event;
4+
import org.bukkit.event.HandlerList;
5+
import org.jetbrains.annotations.ApiStatus;
6+
import org.jetbrains.annotations.Contract;
7+
import org.jetbrains.annotations.NotNull;
8+
9+
import java.util.HashMap;
10+
import java.util.List;
11+
import java.util.Map;
12+
13+
/**
14+
* Internal event for managing context during "suggestions" entry evaluation in {@link SubCommandEntryData}.
15+
*/
16+
@ApiStatus.Internal
17+
public class CommandSuggestionEvent extends Event {
18+
19+
/**
20+
* Map keyed by argument name.
21+
*/
22+
public Map<String, List<String>> suggestions = new HashMap<>();
23+
24+
@Override
25+
@Contract("-> fail")
26+
public @NotNull HandlerList getHandlers() {
27+
throw new UnsupportedOperationException();
28+
}
29+
30+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package org.skriptlang.skript.bukkit.command.elements.structures.util;
2+
3+
import ch.njol.skript.lang.Trigger;
4+
import com.mojang.brigadier.arguments.ArgumentType;
5+
import com.mojang.brigadier.context.CommandContext;
6+
import com.mojang.brigadier.suggestion.Suggestions;
7+
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
8+
import io.papermc.paper.command.brigadier.CommandSourceStack;
9+
import org.jetbrains.annotations.ApiStatus;
10+
import org.jetbrains.annotations.NotNull;
11+
12+
import java.util.List;
13+
import java.util.Locale;
14+
import java.util.concurrent.CompletableFuture;
15+
16+
/**
17+
* Utility class for managing {@link org.skriptlang.skript.bukkit.command.custom.ScriptBrigadierCommand} suggestions.
18+
* Similar to {@link com.mojang.brigadier.suggestion.SuggestionProvider}.
19+
*/
20+
@ApiStatus.Internal
21+
public class ScriptSuggestionProvider {
22+
23+
private final Trigger suggestionsProvider;
24+
25+
/**
26+
* @param suggestionsProvider A trigger to execute using {@link CommandSuggestionEvent}.
27+
* It is expected (though not required) that this trigger holds code to modify suggestions.
28+
*/
29+
public ScriptSuggestionProvider(Trigger suggestionsProvider) {
30+
this.suggestionsProvider = suggestionsProvider;
31+
}
32+
33+
/**
34+
* Obtains suggestions for a specific argument using this provider.
35+
* @param argumentName The name of the argument suggestions are being obtained for.
36+
* @param context Context around the command execution.
37+
* @param builder The builder to add suggestions to
38+
* @param argument The underlying argument suggestions are being obtained for.
39+
* @return Future to obtain suggestions.
40+
*/
41+
public CompletableFuture<Suggestions> getSuggestions(String argumentName, CommandContext<CommandSourceStack> context,
42+
SuggestionsBuilder builder, ArgumentType<?> argument) {
43+
CommandSuggestionEvent suggestionEvent = new CommandSuggestionEvent();
44+
suggestionsProvider.execute(suggestionEvent);
45+
List<String> suggestions = suggestionEvent.suggestions.get(argumentName);
46+
if (suggestions == null) { // nothing explicitly set, rely on argument's default suggestions
47+
return argument.listSuggestions(context, builder);
48+
}
49+
for (String suggestion : suggestions) {
50+
suggest(builder, suggestion);
51+
}
52+
return builder.buildFuture();
53+
}
54+
55+
/**
56+
* Attempts to add {@code suggestion} to {@code builder}.
57+
* It expects that {@code suggestion} starts with (ignoring case) the currently entered input for the argument.
58+
* @param builder The builder to add the suggestion to.
59+
* @param suggestion The suggestion.
60+
*/
61+
public static void suggest(@NotNull SuggestionsBuilder builder, String suggestion) {
62+
if (suggestion == null) {
63+
return;
64+
}
65+
// treat <foo b> as a valid match for <foo_bar>
66+
// treat <"foo b> as a valid match for <foo_bar>
67+
String remaining = builder.getRemainingLowerCase();
68+
if (!remaining.isEmpty() && remaining.charAt(0) == '"') {
69+
remaining = remaining.substring(1);
70+
}
71+
if (remaining.contains(" ")) {
72+
remaining = remaining.replace(' ', '_');
73+
}
74+
if (suggestion.toLowerCase(Locale.ENGLISH).startsWith(remaining)) {
75+
builder.suggest(suggestion);
76+
}
77+
}
78+
79+
}

0 commit comments

Comments
 (0)