Skip to content
This repository was archived by the owner on Aug 31, 2019. It is now read-only.

Commit 4aa87c7

Browse files
committed
Expand completion support
1 parent 37da660 commit 4aa87c7

6 files changed

Lines changed: 336 additions & 30 deletions

File tree

core/src/main/java/com/sk89q/minecraft/util/commands/CommandContext.java

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.HashSet;
2626
import java.util.List;
2727
import java.util.Map;
28+
import java.util.Optional;
2829
import java.util.Set;
2930
import javax.annotation.Nullable;
3031

@@ -192,10 +193,82 @@ public CommandContext(String[] args, @Nullable Set<Character> valueFlagNames, bo
192193
return suggestionContext;
193194
}
194195

196+
/**
197+
* Is the command being executed, rather than completed?
198+
*/
199+
public boolean isExecuting() {
200+
return getSuggestionContext() == null;
201+
}
202+
203+
/**
204+
* Is the command being completed, rather than executed?
205+
*/
206+
public boolean isSuggesting() {
207+
return getSuggestionContext() != null;
208+
}
209+
210+
/**
211+
* Is the given argument being completed?
212+
*/
213+
public boolean isSuggestingArgument(int index) {
214+
return isSuggesting() && getSuggestionContext().isArgument(index);
215+
}
216+
217+
/**
218+
* Is the given flag being completed?
219+
*/
220+
public boolean isSuggestingFlag(char flag) {
221+
return isSuggesting() && getSuggestionContext().isFlag(flag);
222+
}
223+
224+
/**
225+
* If the given argument is being completed, generate suggestions based on the given choices.
226+
*/
227+
public void suggestArgument(int index, Iterable<String> choices) throws SuggestException {
228+
if(isSuggesting() && getSuggestionContext().isArgument()) {
229+
getSuggestionContext().suggestArgument(index, choices);
230+
}
231+
}
232+
233+
/**
234+
* If the given flag is being completed, generate suggestions based on the given choices.
235+
*/
236+
public void suggestFlag(char flag, Iterable<String> choices) throws SuggestException {
237+
if(isSuggesting()) {
238+
getSuggestionContext().suggestFlag(flag, choices);
239+
}
240+
}
241+
242+
/**
243+
* If the command is being completed from anywhere at or after the given argument index,
244+
* generate suggestions for the entire command from that index, based on the given choices.
245+
*/
246+
public void suggestJoinedArguments(int start, Iterable<String> choices) throws SuggestException {
247+
final SuggestionContext ctx = getSuggestionContext();
248+
if(ctx != null && ctx.isArgument()) {
249+
if(start == ctx.getIndex()) {
250+
ctx.suggestArgument(start, choices);
251+
} else if(start < ctx.getIndex()) {
252+
final String prefix = String.join(" ", parsedArgs.subList(start, ctx.getIndex())).toLowerCase() + " ";
253+
final List<String> filtered = new ArrayList<>();
254+
for(String choice : choices) {
255+
if(choice.toLowerCase().startsWith(prefix)) {
256+
filtered.add(choice.substring(prefix.length()));
257+
}
258+
}
259+
ctx.suggestArgument(ctx.getIndex(), filtered);
260+
}
261+
}
262+
}
263+
195264
public String getCommand() {
196265
return command;
197266
}
198267

268+
public String[] getOriginalArgs() {
269+
return originalArgs;
270+
}
271+
199272
public boolean matches(String command) {
200273
return this.command.equalsIgnoreCase(command);
201274
}
@@ -204,10 +277,46 @@ public String getString(int index) {
204277
return parsedArgs.get(index);
205278
}
206279

280+
/**
281+
* Return the argument at the given index as a String, if it is present.
282+
*/
283+
public Optional<String> tryString(int index) {
284+
return index < parsedArgs.size() ? Optional.of(parsedArgs.get(index))
285+
: Optional.empty();
286+
}
287+
207288
public String getString(int index, String def) {
208289
return index < parsedArgs.size() ? parsedArgs.get(index) : def;
209290
}
210291

292+
/**
293+
* Return the argument at the given index as a String.
294+
* @throws CommandException if the argument is missing
295+
*/
296+
public String string(int index) throws CommandException {
297+
if(index >= parsedArgs.size()) {
298+
throw new CommandUsageException("Missing argument");
299+
}
300+
return getString(index);
301+
}
302+
303+
/**
304+
* Return the argument at the given index as a String, or generate suggestions
305+
* if that argument is being completed.
306+
*
307+
* @throws CommandException if the argument is missing
308+
* @throws SuggestException if the argument is being completed
309+
*/
310+
public String string(int index, Iterable<String> choices) throws CommandException, SuggestException {
311+
suggestArgument(index, choices);
312+
return string(index);
313+
}
314+
315+
public Optional<String> tryString(int index, Iterable<String> choices) throws SuggestException {
316+
suggestArgument(index, choices);
317+
return tryString(index);
318+
}
319+
211320
public String getJoinedStrings(int initialIndex) {
212321
initialIndex = originalArgIndices.get(initialIndex);
213322
StringBuilder buffer = new StringBuilder(originalArgs[initialIndex]);
@@ -216,11 +325,70 @@ public String getJoinedStrings(int initialIndex) {
216325
}
217326
return buffer.toString();
218327
}
219-
328+
329+
public String getJoinedStrings(int initialIndex, String def) {
330+
return initialIndex < originalArgIndices.size() ? getJoinedStrings(initialIndex) : def;
331+
}
332+
333+
/**
334+
* Return the rest of the command line, starting at the given argument index.
335+
*
336+
* Any flags that appear after the given index are included in the result.
337+
*
338+
* @throws CommandException if the argument is missing
339+
*/
340+
public String joinedStrings(int initialIndex) throws CommandException {
341+
if(initialIndex >= originalArgIndices.size()) {
342+
throw new CommandUsageException("Missing argument");
343+
}
344+
return getJoinedStrings(initialIndex);
345+
}
346+
347+
public String joinedStrings(int initialIndex, Iterable<String> choices) throws CommandException, SuggestException {
348+
suggestJoinedArguments(initialIndex, choices);
349+
return joinedStrings(initialIndex);
350+
}
351+
352+
public Optional<String> tryJoinedStrings(int initialIndex) {
353+
return initialIndex < originalArgIndices.size() ? Optional.of(getJoinedStrings(initialIndex))
354+
: Optional.empty();
355+
}
356+
357+
public Optional<String> tryJoinedStrings(int initialIndex, Iterable<String> choices) throws SuggestException {
358+
suggestJoinedArguments(initialIndex, choices);
359+
return tryJoinedStrings(initialIndex);
360+
}
361+
220362
public String getRemainingString(int start) {
221363
return getString(start, parsedArgs.size() - 1);
222364
}
223365

366+
/**
367+
* Return the given argument and all arguments after it, joined with spaces.
368+
*
369+
* Flags are never included in the result, even if they appear between between
370+
* the arguments that are included.
371+
*
372+
* @throws CommandException if the argument is missing
373+
*/
374+
public String remainingString(int start) throws CommandException {
375+
return string(start, parsedArgs.size() - 1);
376+
}
377+
378+
public String remainingString(int start, Iterable<String> choices) throws CommandException, SuggestException {
379+
suggestJoinedArguments(start, choices);
380+
return remainingString(start);
381+
}
382+
383+
public Optional<String> tryRemainingString(int start) {
384+
return tryString(start, parsedArgs.size() - 1);
385+
}
386+
387+
public Optional<String> tryRemainingString(int start, Iterable<String> choices) throws SuggestException {
388+
suggestJoinedArguments(start, choices);
389+
return tryRemainingString(start);
390+
}
391+
224392
public String getString(int start, int end) {
225393
StringBuilder buffer = new StringBuilder(parsedArgs.get(start));
226394
for (int i = start + 1; i < end + 1; ++i) {
@@ -229,6 +397,18 @@ public String getString(int start, int end) {
229397
return buffer.toString();
230398
}
231399

400+
public String string(int start, int end) throws CommandException {
401+
if(start >= parsedArgs.size() || end >= parsedArgs.size()) {
402+
throw new CommandUsageException("Missing argument");
403+
}
404+
return getString(start, end);
405+
}
406+
407+
public Optional<String> tryString(int start, int end) {
408+
return start < parsedArgs.size() && end < parsedArgs.size() ? Optional.of(getString(start, end))
409+
: Optional.empty();
410+
}
411+
232412
public int getInteger(int index) throws CommandNumberFormatException {
233413
final String text = parsedArgs.get(index);
234414
try {
@@ -291,7 +471,7 @@ public Map<Character, String> getValueFlags() {
291471
return valueFlags;
292472
}
293473

294-
public String getFlag(char ch) {
474+
public @Nullable String getFlag(char ch) {
295475
return valueFlags.get(ch);
296476
}
297477

@@ -304,6 +484,20 @@ public String getFlag(char ch, String def) {
304484
return value;
305485
}
306486

487+
public @Nullable String flagOrNull(char ch, Iterable<String> choices) throws SuggestException {
488+
suggestFlag(ch, choices);
489+
return getFlag(ch);
490+
}
491+
492+
public Optional<String> tryFlag(char ch) {
493+
return Optional.ofNullable(getFlag(ch));
494+
}
495+
496+
public Optional<String> tryFlag(char ch, Iterable<String> choices) throws SuggestException {
497+
suggestFlag(ch, choices);
498+
return tryFlag(ch);
499+
}
500+
307501
public int getFlagInteger(char ch) throws CommandNumberFormatException {
308502
final String text = valueFlags.get(ch);
309503
try {

core/src/main/java/com/sk89q/minecraft/util/commands/CommandUsageException.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,28 @@
1919

2020
package com.sk89q.minecraft.util.commands;
2121

22+
import javax.annotation.Nullable;
23+
2224
public class CommandUsageException extends CommandException {
2325

24-
protected String usage;
26+
protected @Nullable String usage;
27+
28+
public CommandUsageException(String message) {
29+
this(message, null);
30+
}
2531

26-
public CommandUsageException(String message, String usage) {
32+
public CommandUsageException(String message, @Nullable String usage) {
2733
super(message);
2834
this.usage = usage;
2935
}
3036

3137
public String getUsage() {
32-
return usage;
38+
return usage != null ? usage : "";
39+
}
40+
41+
public void offerUsage(String usage) {
42+
if(this.usage == null) {
43+
this.usage = usage;
44+
}
3345
}
3446
}

core/src/main/java/com/sk89q/minecraft/util/commands/CommandsManager.java

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.Set;
3434
import java.util.logging.Level;
3535
import java.util.logging.Logger;
36+
import java.util.stream.Stream;
3637
import javax.annotation.Nullable;
3738
import javax.inject.Provider;
3839

@@ -415,6 +416,12 @@ protected String getNestedUsage(String[] args, int level, Method method, T playe
415416
return command.toString();
416417
}
417418

419+
private static boolean supportsCompletion(Method method) {
420+
return List.class.isAssignableFrom(method.getReturnType()) ||
421+
Stream.of(method.getExceptionTypes())
422+
.anyMatch(SuggestException.class::isAssignableFrom);
423+
}
424+
418425
/**
419426
* Attempt to execute a command. This version takes a separate command
420427
* name (for the root command) and then a list of following arguments.
@@ -529,7 +536,7 @@ private List<String> executeMethod(Method parent, boolean completing, String[] a
529536

530537
// If the command method doesn't do completions, return null to indicate that
531538
// the default completion (player name) should be used.
532-
if(completing && !List.class.isAssignableFrom(method.getReturnType())) return null;
539+
if(completing && !supportsCompletion(method)) return null;
533540

534541
String[] newArgs = new String[args.length - level];
535542
System.arraycopy(args, level, newArgs, 0, args.length - level);
@@ -571,30 +578,29 @@ private List<String> executeMethod(Method parent, boolean completing, String[] a
571578
Provider provider = providers.get(method);
572579
Object instance = provider == null ? null : provider.get();
573580

574-
// If we get here while completing, it means the method's return type is a List<String>,
575-
// and we never want to use the default completion. So if it returns null, convert it to
576-
// an empty list.
577-
final List<String> completions = invokeMethod(parent, args, player, method, instance, methodArgs, argsCount);
578-
return completions != null ? completions : Collections.<String>emptyList();
579-
}
580-
}
581-
582-
public List<String> invokeMethod(Method parent, String[] args, T player, Method method, Object instance, Object[] methodArgs, int level) throws CommandException {
583-
try {
584-
return (List<String>) method.invoke(instance, methodArgs);
585-
} catch (IllegalArgumentException | IllegalAccessException e) {
586-
logger.log(Level.SEVERE, "Failed to execute command", e);
587-
return null;
588-
} catch (InvocationTargetException e) {
589-
if (e.getCause() instanceof CommandException) {
590-
throw (CommandException) e.getCause();
591-
}
592-
593-
if(e.getCause() instanceof RuntimeException) {
594-
throw (RuntimeException) e.getCause();
581+
try {
582+
// If we get here while completing, it means the method's return type is a List<String>,
583+
// and we never want to use the default completion. So if it returns null, convert it to
584+
// an empty list.
585+
List<String> completions = (List<String>) method.invoke(instance, methodArgs);
586+
return completions != null ? completions : Collections.emptyList();
587+
} catch (IllegalArgumentException | IllegalAccessException e) {
588+
logger.log(Level.SEVERE, "Failed to execute command", e);
589+
return Collections.emptyList();
590+
} catch (InvocationTargetException e) {
591+
if (e.getCause() instanceof SuggestException && context.isSuggesting()) {
592+
return ((SuggestException) e.getCause()).suggestions();
593+
} else if (e.getCause() instanceof CommandException) {
594+
if(e.getCause() instanceof CommandUsageException) {
595+
((CommandUsageException) e.getCause()).offerUsage(getUsage(args, argsCount, method.getAnnotation(Command.class)));
596+
}
597+
throw (CommandException) e.getCause();
598+
} else if (e.getCause() instanceof RuntimeException) {
599+
throw (RuntimeException) e.getCause();
600+
} else {
601+
throw new WrappedCommandException(e.getCause());
602+
}
595603
}
596-
597-
throw new WrappedCommandException(e.getCause());
598604
}
599605
}
600606

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.sk89q.minecraft.util.commands;
2+
3+
import java.util.List;
4+
5+
import com.google.common.collect.ImmutableList;
6+
7+
/**
8+
* Throw this exception out of a command method to suggest completions for the command.
9+
*
10+
* This is only allowed when {@link CommandContext#isSuggesting()} is true. If it isn't,
11+
* then the exception is handled like any other uncaught exception.
12+
*/
13+
public class SuggestException extends Exception {
14+
15+
private final ImmutableList<String> suggestions;
16+
17+
public SuggestException(Iterable<String> suggestions) {
18+
this.suggestions = ImmutableList.copyOf(suggestions);
19+
}
20+
21+
public List<String> suggestions() {
22+
return suggestions;
23+
}
24+
}

0 commit comments

Comments
 (0)