# Source code
source: package: co.aikar.commands, class: CommandCompletions, interface: CommandCompletionHandler

    001/*
    002 * Copyright (c) 2016-2017 Daniel Ennis (Aikar) - MIT License
    003 *
    004 *  Permission is hereby granted, free of charge, to any person obtaining
    005 *  a copy of this software and associated documentation files (the
    006 *  "Software"), to deal in the Software without restriction, including
    007 *  without limitation the rights to use, copy, modify, merge, publish,
    008 *  distribute, sublicense, and/or sell copies of the Software, and to
    009 *  permit persons to whom the Software is furnished to do so, subject to
    010 *  the following conditions:
    011 *
    012 *  The above copyright notice and this permission notice shall be
    013 *  included in all copies or substantial portions of the Software.
    014 *
    015 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    016 *  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    017 *  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    018 *  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
    019 *  LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
    020 *  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
    021 *  WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    022 */
    023
    024package co.aikar.commands;
    025
    026import co.aikar.commands.apachecommonslang.ApacheCommonsLangUtil;
    027import org.jetbrains.annotations.NotNull;
    028
    029import java.util.ArrayList;
    030import java.util.Arrays;
    031import java.util.Collection;
    032import java.util.Collections;
    033import java.util.HashMap;
    034import java.util.List;
    035import java.util.Locale;
    036import java.util.Map;
    037import java.util.function.Supplier;
    038import java.util.stream.Collectors;
    039import java.util.stream.IntStream;
    040
    041
    042@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"})
    043public class CommandCompletions<C extends CommandCompletionContext> {
    044    private static final String DEFAULT_ENUM_ID = "@__defaultenum__";
    045    private final CommandManager manager;
    046    // TODO: use a CompletionProvider that can return a delegated Id or provide values such as enum support
    047    private Map<String, CommandCompletionHandler> completionMap = new HashMap<>();
    048    private Map<Class, String> defaultCompletions = new HashMap<>();
    049
    050    public CommandCompletions(CommandManager manager) {
    051        this.manager = manager;
    052        registerStaticCompletion("empty", Collections.emptyList());
    053        registerStaticCompletion("nothing", Collections.emptyList());
    054        registerStaticCompletion("timeunits", Arrays.asList("minutes", "hours", "days", "weeks", "months", "years"));
    055        registerAsyncCompletion("range", (c) -> {
    056            String config = c.getConfig();
    057            if (config == null) {
    058                return Collections.emptyList();
    059            }
    060            final String[] ranges = ACFPatterns.DASH.split(config);
    061            int start;
    062            int end;
    063            if (ranges.length != 2) {
    064                start = 0;
    065                end = ACFUtil.parseInt(ranges[0], 0);
    066            } else {
    067                start = ACFUtil.parseInt(ranges[0], 0);
    068                end = ACFUtil.parseInt(ranges[1], 0);
    069            }
    070            return IntStream.rangeClosed(start, end).mapToObj(Integer::toString).collect(Collectors.toList());
    071        });
    072    }
    073
    074    /**
    075     * Registr a completion handler to provide command completions based on the user input.
    076     *
    077     * @param id
    078     * @param handler
    079     * @return
    080     */
    081    public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler<C> handler) {
    082        return this.completionMap.put(prepareCompletionId(id), handler);
    083    }
    084
    085    /**
    086     * Unregister a completion handler.
    087     * @param id
    088     * @return
    089     * @throws IllegalStateException If the completion couldn't be found
    090     */
    091    public CommandCompletionHandler unregisterCompletion(String id) {
    092        if (!this.completionMap.containsKey(id)) {
    093            throw new IllegalStateException("The supplied key " + id + " does not exist in any completions");
    094        }
    095
    096        return this.completionMap.remove(id);
    097    }
    098
    099    /**
    100     * Registr a completion handler to provide command completions based on the user input.
    101     * This handler is declared to be safe to be executed asynchronously.
    102     * <p>
    103     * Not all platforms support this, so if the platform does not support asynchronous execution,
    104     * your handler will be executed on the main thread.
    105     * <p>
    106     * Use this anytime your handler does not need to access state that is not considered thread safe.
    107     * <p>
    108     * Use context.isAsync() to determine if you are async or not.
    109     *
    110     * @param id
    111     * @param handler
    112     * @return
    113     */
    114    public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler<C> handler) {
    115        return this.completionMap.put(prepareCompletionId(id), handler);
    116    }
    117
    118    /**
    119     * Register a static list of command completions that will never change.
    120     * Like @CommandCompletion, values are | (PIPE) separated.
    121     * <p>
    122     * Example: foo|bar|baz
    123     *
    124     * @param id
    125     * @param list
    126     * @return
    127     */
    128    public CommandCompletionHandler registerStaticCompletion(String id, String list) {
    129        return registerStaticCompletion(id, ACFPatterns.PIPE.split(list));
    130    }
    131
    132    /**
    133     * Register a static list of command completions that will never change
    134     *
    135     * @param id
    136     * @param completions
    137     * @return
    138     */
    139    public CommandCompletionHandler registerStaticCompletion(String id, String[] completions) {
    140        return registerStaticCompletion(id, Arrays.asList(completions));
    141    }
    142
    143    /**
    144     * Register a static list of command completions that will never change. The list is obtained from the supplier
    145     * immediately as part of this method call.
    146     *
    147     * @param id
    148     * @param supplier
    149     * @return
    150     */
    151    public CommandCompletionHandler registerStaticCompletion(String id, Supplier<Collection<String>> supplier) {
    152        return registerStaticCompletion(id, supplier.get());
    153    }
    154
    155    /**
    156     * Register a static list of command completions that will never change
    157     *
    158     * @param id
    159     * @param completions
    160     * @return
    161     */
    162    public CommandCompletionHandler registerStaticCompletion(String id, Collection<String> completions) {
    163        return registerAsyncCompletion(id, x -> completions);
    164    }
    165
    166    /**
    167     * Registers a completion handler such as @players to default apply to all command parameters of the specified types
    168     * <p>
    169     * This enables automatic completion support for parameters without manually defining it for custom objects
    170     *
    171     * @param id
    172     * @param classes
    173     */
    174    public void setDefaultCompletion(String id, Class... classes) {
    175        // get completion with specified id
    176        id = prepareCompletionId(id);
    177        CommandCompletionHandler completion = completionMap.get(id);
    178
    179        if (completion == null) {
    180            // Throw something because no completion with specified id
    181            throw new IllegalStateException("Completion not registered for " + id);
    182        }
    183
    184        for (Class clazz : classes) {
    185            defaultCompletions.put(clazz, id);
    186        }
    187    }
    188
    189    @NotNull
    190    private static String prepareCompletionId(String id) {
    191        return (id.startsWith("@") ? "" : "@") + id.toLowerCase(Locale.ENGLISH);
    192    }
    193
    194    @NotNull
    195    List<String> of(RegisteredCommand cmd, CommandIssuer sender, String[] args, boolean isAsync) {
    196        String[] completions = ACFPatterns.SPACE.split(cmd.complete);
    197        final int argIndex = args.length - 1;
    198
    199        String input = args[argIndex];
    200
    201        String completion = argIndex < completions.length ? completions[argIndex] : null;
    202        if (completion == null || completion.isEmpty() || "*".equals(completion)) {
    203            completion = findDefaultCompletion(cmd, args);
    204        }
    205
    206        if (completion == null && completions.length > 0) {
    207            String last = completions[completions.length - 1];
    208            if (last.startsWith("repeat@")) {
    209                completion = last;
    210            } else if (argIndex >= completions.length && cmd.parameters[cmd.parameters.length - 1].consumesRest) {
    211                completion = last;
    212            }
    213        }
    214
    215        if (completion == null) {
    216            return Collections.singletonList(input);
    217        }
    218
    219        return getCompletionValues(cmd, sender, completion, args, isAsync);
    220    }
    221
    222    String findDefaultCompletion(RegisteredCommand cmd, String[] args) {
    223        int i = 0;
    224        for (CommandParameter param : cmd.parameters) {
    225            if (param.canConsumeInput() && ++i == args.length) {
    226                Class type = param.getType();
    227                while (type != null) {
    228                    String completion = this.defaultCompletions.get(type);
    229                    if (completion != null) {
    230                        return completion;
    231                    }
    232                    type = type.getSuperclass();
    233                }
    234                if (param.getType().isEnum()) {
    235                    CommandOperationContext ctx = CommandManager.getCurrentCommandOperationContext();
    236                    //noinspection unchecked
    237                    ctx.enumCompletionValues = ACFUtil.enumNames((Class<? extends Enum<?>>) param.getType());
    238                    return DEFAULT_ENUM_ID;
    239                }
    240                break;
    241            }
    242        }
    243        return null;
    244    }
    245
    246    List<String> getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args, boolean isAsync) {
    247        if (DEFAULT_ENUM_ID.equals(completion)) {
    248            CommandOperationContext<?> ctx = CommandManager.getCurrentCommandOperationContext();
    249            return ctx.enumCompletionValues;
    250        }
    251        boolean repeat = completion.startsWith("repeat@");
    252        if (repeat) {
    253            completion = completion.substring(6);
    254        }
    255        completion = manager.getCommandReplacements().replace(completion);
    256
    257        List<String> allCompletions = new ArrayList<>();
    258        String input = args.length > 0 ? args[args.length - 1] : "";
    259
    260        for (String value : ACFPatterns.PIPE.split(completion)) {
    261            String[] complete = ACFPatterns.COLONEQUALS.split(value, 2);
    262            CommandCompletionHandler handler = this.completionMap.get(complete[0].toLowerCase(Locale.ENGLISH));
    263            if (handler != null) {
    264                if (isAsync && !(handler instanceof AsyncCommandCompletionHandler)) {
    265                    ACFUtil.sneaky(new SyncCompletionRequired());
    266                    return null;
    267                }
    268                String config = complete.length == 1 ? null : complete[1];
    269                CommandCompletionContext context = manager.createCompletionContext(command, sender, input, config, args);
    270
    271                try {
    272                    //noinspection unchecked
    273                    Collection<String> completions = handler.getCompletions(context);
    274
    275                    //Handle completions with more than one word:
    276                    if (!repeat && completions != null
    277                            && command.parameters[command.parameters.length - 1].consumesRest
    278                            && args.length > ACFPatterns.SPACE.split(command.complete).length) {
    279                        String start = String.join(" ", args);
    280                        completions = completions.stream()
    281                                .map(s -> {
    282                                    if (s != null && s.split(" ").length >= args.length && ApacheCommonsLangUtil.startsWithIgnoreCase(s, start)) {
    283                                        String[] completionArgs = s.split(" ");
    284                                        return String.join(" ", Arrays.copyOfRange(completionArgs, args.length - 1, completionArgs.length));
    285                                    } else {
    286                                        return s;
    287                                    }
    288                                }).collect(Collectors.toList());
    289                    }
    290
    291                    if (completions != null) {
    292                        allCompletions.addAll(completions);
    293                        continue;
    294                    }
    295                    //noinspection ConstantIfStatement,ConstantConditions
    296                    if (false) { // Hack to fool compiler. since its sneakily thrown.
    297                        throw new CommandCompletionTextLookupException();
    298                    }
    299                } catch (CommandCompletionTextLookupException ignored) {
    300                    // This should only happen if some other feedback error occured.
    301                } catch (Exception e) {
    302                    command.handleException(sender, Arrays.asList(args), e);
    303                }
    304                // Something went wrong in lookup, fall back to input
    305                return Collections.singletonList(input);
    306            } else {
    307                // Plaintext value
    308                allCompletions.add(value);
    309            }
    310        }
    311        return allCompletions;
    312    }
    313
    314    public interface CommandCompletionHandler<C extends CommandCompletionContext> {
    315        Collection<String> getCompletions(C context) throws InvalidCommandArgument;
    316    }
    317
    318    public interface AsyncCommandCompletionHandler<C extends CommandCompletionContext> extends CommandCompletionHandler<C> {
    319    }
    320
    321    public static class SyncCompletionRequired extends RuntimeException {
    322    }
    323
    324}