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}