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 org.jetbrains.annotations.NotNull; 027 028import java.util.ArrayList; 029import java.util.Arrays; 030import java.util.Collection; 031import java.util.Collections; 032import java.util.HashMap; 033import java.util.List; 034import java.util.Map; 035import java.util.function.Supplier; 036import java.util.stream.Collectors; 037import java.util.stream.IntStream; 038 039 040@SuppressWarnings({"WeakerAccess", "UnusedReturnValue"}) 041public class CommandCompletions<C extends CommandCompletionContext> { 042 private final CommandManager manager; 043 private Map<String, CommandCompletionHandler> completionMap = new HashMap<>(); 044 private Map<Class, String> defaultCompletions = new HashMap<>(); 045 046 public CommandCompletions(CommandManager manager) { 047 this.manager = manager; 048 registerAsyncCompletion("nothing", c -> Collections.emptyList()); 049 registerAsyncCompletion("range", (c) -> { 050 String config = c.getConfig(); 051 if (config == null) { 052 return Collections.emptyList(); 053 } 054 final String[] ranges = ACFPatterns.DASH.split(config); 055 int start; 056 int end; 057 if (ranges.length != 2) { 058 start = 0; 059 end = ACFUtil.parseInt(ranges[0], 0); 060 } else { 061 start = ACFUtil.parseInt(ranges[0], 0); 062 end = ACFUtil.parseInt(ranges[1], 0); 063 } 064 return IntStream.rangeClosed(start, end).mapToObj(Integer::toString).collect(Collectors.toList()); 065 }); 066 List<String> timeunits = Arrays.asList("minutes", "hours", "days", "weeks", "months", "years"); 067 registerAsyncCompletion("timeunits", (c) -> timeunits); 068 } 069 070 /** 071 * Registr a completion handler to provide command completions based on the user input. 072 * 073 * @param id 074 * @param handler 075 * @return 076 */ 077 public CommandCompletionHandler registerCompletion(String id, CommandCompletionHandler<C> handler) { 078 return this.completionMap.put("@" + id.toLowerCase(), handler); 079 } 080 081 /** 082 * Registr a completion handler to provide command completions based on the user input. 083 * This handler is declared to be safe to be executed asynchronously. 084 * <p> 085 * Not all platforms support this, so if the platform does not support asynchronous execution, 086 * your handler will be executed on the main thread. 087 * <p> 088 * Use this anytime your handler does not need to access state that is not considered thread safe. 089 * <p> 090 * Use context.isAsync() to determine if you are async or not. 091 * 092 * @param id 093 * @param handler 094 * @return 095 */ 096 public CommandCompletionHandler registerAsyncCompletion(String id, AsyncCommandCompletionHandler<C> handler) { 097 return this.completionMap.put("@" + id.toLowerCase(), handler); 098 } 099 100 /** 101 * Register a static list of command completions that will never change. 102 * Like @CommandCompletion, values are | (PIPE) separated. 103 * <p> 104 * Example: foo|bar|baz 105 * 106 * @param id 107 * @param list 108 * @return 109 */ 110 public CommandCompletionHandler registerStaticCompletion(String id, String list) { 111 return registerStaticCompletion(id, ACFPatterns.PIPE.split(list)); 112 } 113 114 /** 115 * Register a static list of command completions that will never change 116 * 117 * @param id 118 * @param completions 119 * @return 120 */ 121 public CommandCompletionHandler registerStaticCompletion(String id, String[] completions) { 122 return registerStaticCompletion(id, Arrays.asList(completions)); 123 } 124 125 /** 126 * Register a static list of command completions that will never change. The list is obtained from the supplier 127 * immediately as part of this method call. 128 * 129 * @param id 130 * @param supplier 131 * @return 132 */ 133 public CommandCompletionHandler registerStaticCompletion(String id, Supplier<Collection<String>> supplier) { 134 return registerStaticCompletion(id, supplier.get()); 135 } 136 137 /** 138 * Register a static list of command completions that will never change 139 * 140 * @param id 141 * @param completions 142 * @return 143 */ 144 public CommandCompletionHandler registerStaticCompletion(String id, Collection<String> completions) { 145 return registerAsyncCompletion(id, x -> completions); 146 } 147 148 /** 149 * @param id 150 * @param classes 151 * @return 152 * @deprecated Feature Not done yet 153 */ 154 CommandCompletionHandler setDefaultCompletion(String id, Class... classes) { 155 // get completion with specified id 156 id = id.toLowerCase(); 157 CommandCompletionHandler completion = completionMap.get(id); 158 159 if (completion == null) { 160 // Throw something because no completion with specified id 161 throw new IllegalStateException("Completion not registered for " + id); 162 } 163 164 for (Class clazz : classes) { 165 defaultCompletions.put(clazz, id); 166 } 167 168 return completion; 169 } 170 171 @NotNull 172 List<String> of(RegisteredCommand cmd, CommandIssuer sender, String[] args, boolean isAsync) { 173 String[] completions = ACFPatterns.SPACE.split(cmd.complete); 174 final int argIndex = args.length - 1; 175 176 String input = args[argIndex]; 177 178 String completion = argIndex < completions.length ? completions[argIndex] : null; 179 if (completion == null && completions.length > 0) { 180 completion = completions[completions.length - 1]; 181 } 182 if (completion == null) { 183 return Collections.singletonList(input); 184 } 185 186 return getCompletionValues(cmd, sender, completion, args, isAsync); 187 } 188 189 List<String> getCompletionValues(RegisteredCommand command, CommandIssuer sender, String completion, String[] args, boolean isAsync) { 190 completion = manager.getCommandReplacements().replace(completion); 191 192 List<String> allCompletions = new ArrayList<>(); 193 String input = args.length > 0 ? args[args.length - 1] : ""; 194 195 for (String value : ACFPatterns.PIPE.split(completion)) { 196 String[] complete = ACFPatterns.COLONEQUALS.split(value, 2); 197 CommandCompletionHandler handler = this.completionMap.get(complete[0].toLowerCase()); 198 if (handler != null) { 199 if (isAsync && !(handler instanceof AsyncCommandCompletionHandler)) { 200 ACFUtil.sneaky(new SyncCompletionRequired()); 201 return null; 202 } 203 String config = complete.length == 1 ? null : complete[1]; 204 CommandCompletionContext context = manager.createCompletionContext(command, sender, input, config, args); 205 206 try { 207 //noinspection unchecked 208 Collection<String> completions = handler.getCompletions(context); 209 if (completions != null) { 210 allCompletions.addAll(completions); 211 continue; 212 } 213 //noinspection ConstantIfStatement,ConstantConditions 214 if (false) { // Hack to fool compiler. since its sneakily thrown. 215 throw new CommandCompletionTextLookupException(); 216 } 217 } catch (CommandCompletionTextLookupException ignored) { 218 // This should only happen if some other feedback error occured. 219 } catch (Exception e) { 220 command.handleException(sender, Arrays.asList(args), e); 221 } 222 // Something went wrong in lookup, fall back to input 223 return Collections.singletonList(input); 224 } else { 225 // Plaintext value 226 allCompletions.add(value); 227 } 228 } 229 return allCompletions; 230 } 231 232 public interface CommandCompletionHandler<C extends CommandCompletionContext> { 233 Collection<String> getCompletions(C context) throws InvalidCommandArgument; 234 } 235 236 public interface AsyncCommandCompletionHandler<C extends CommandCompletionContext> extends CommandCompletionHandler<C> { 237 } 238 239 public static class SyncCompletionRequired extends RuntimeException { 240 } 241 242}