1 /* |
|
2 * Copyright (c) 2002-2016, the original author or authors. |
|
3 * |
|
4 * This software is distributable under the BSD license. See the terms of the |
|
5 * BSD license in the documentation provided with this software. |
|
6 * |
|
7 * http://www.opensource.org/licenses/bsd-license.php |
|
8 */ |
|
9 package jdk.internal.jline.console.completer; |
|
10 |
|
11 import jdk.internal.jline.internal.Log; |
|
12 |
|
13 import java.util.ArrayList; |
|
14 import java.util.Arrays; |
|
15 import java.util.Collection; |
|
16 import java.util.LinkedList; |
|
17 import java.util.List; |
|
18 |
|
19 import static jdk.internal.jline.internal.Preconditions.checkNotNull; |
|
20 |
|
21 /** |
|
22 * A {@link Completer} implementation that invokes a child completer using the appropriate <i>separator</i> argument. |
|
23 * This can be used instead of the individual completers having to know about argument parsing semantics. |
|
24 * |
|
25 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> |
|
26 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a> |
|
27 * @since 2.3 |
|
28 */ |
|
29 public class ArgumentCompleter |
|
30 implements Completer |
|
31 { |
|
32 private final ArgumentDelimiter delimiter; |
|
33 |
|
34 private final List<Completer> completers = new ArrayList<Completer>(); |
|
35 |
|
36 private boolean strict = true; |
|
37 |
|
38 /** |
|
39 * Create a new completer with the specified argument delimiter. |
|
40 * |
|
41 * @param delimiter The delimiter for parsing arguments |
|
42 * @param completers The embedded completers |
|
43 */ |
|
44 public ArgumentCompleter(final ArgumentDelimiter delimiter, final Collection<Completer> completers) { |
|
45 this.delimiter = checkNotNull(delimiter); |
|
46 checkNotNull(completers); |
|
47 this.completers.addAll(completers); |
|
48 } |
|
49 |
|
50 /** |
|
51 * Create a new completer with the specified argument delimiter. |
|
52 * |
|
53 * @param delimiter The delimiter for parsing arguments |
|
54 * @param completers The embedded completers |
|
55 */ |
|
56 public ArgumentCompleter(final ArgumentDelimiter delimiter, final Completer... completers) { |
|
57 this(delimiter, Arrays.asList(completers)); |
|
58 } |
|
59 |
|
60 /** |
|
61 * Create a new completer with the default {@link WhitespaceArgumentDelimiter}. |
|
62 * |
|
63 * @param completers The embedded completers |
|
64 */ |
|
65 public ArgumentCompleter(final Completer... completers) { |
|
66 this(new WhitespaceArgumentDelimiter(), completers); |
|
67 } |
|
68 |
|
69 /** |
|
70 * Create a new completer with the default {@link WhitespaceArgumentDelimiter}. |
|
71 * |
|
72 * @param completers The embedded completers |
|
73 */ |
|
74 public ArgumentCompleter(final List<Completer> completers) { |
|
75 this(new WhitespaceArgumentDelimiter(), completers); |
|
76 } |
|
77 |
|
78 /** |
|
79 * If true, a completion at argument index N will only succeed |
|
80 * if all the completions from 0-(N-1) also succeed. |
|
81 */ |
|
82 public void setStrict(final boolean strict) { |
|
83 this.strict = strict; |
|
84 } |
|
85 |
|
86 /** |
|
87 * Returns whether a completion at argument index N will success |
|
88 * if all the completions from arguments 0-(N-1) also succeed. |
|
89 * |
|
90 * @return True if strict. |
|
91 * @since 2.3 |
|
92 */ |
|
93 public boolean isStrict() { |
|
94 return this.strict; |
|
95 } |
|
96 |
|
97 /** |
|
98 * @since 2.3 |
|
99 */ |
|
100 public ArgumentDelimiter getDelimiter() { |
|
101 return delimiter; |
|
102 } |
|
103 |
|
104 /** |
|
105 * @since 2.3 |
|
106 */ |
|
107 public List<Completer> getCompleters() { |
|
108 return completers; |
|
109 } |
|
110 |
|
111 public int complete(final String buffer, final int cursor, final List<CharSequence> candidates) { |
|
112 // buffer can be null |
|
113 checkNotNull(candidates); |
|
114 |
|
115 ArgumentDelimiter delim = getDelimiter(); |
|
116 ArgumentList list = delim.delimit(buffer, cursor); |
|
117 int argpos = list.getArgumentPosition(); |
|
118 int argIndex = list.getCursorArgumentIndex(); |
|
119 |
|
120 if (argIndex < 0) { |
|
121 return -1; |
|
122 } |
|
123 |
|
124 List<Completer> completers = getCompleters(); |
|
125 Completer completer; |
|
126 |
|
127 // if we are beyond the end of the completers, just use the last one |
|
128 if (argIndex >= completers.size()) { |
|
129 completer = completers.get(completers.size() - 1); |
|
130 } |
|
131 else { |
|
132 completer = completers.get(argIndex); |
|
133 } |
|
134 |
|
135 // ensure that all the previous completers are successful before allowing this completer to pass (only if strict). |
|
136 for (int i = 0; isStrict() && (i < argIndex); i++) { |
|
137 Completer sub = completers.get(i >= completers.size() ? (completers.size() - 1) : i); |
|
138 String[] args = list.getArguments(); |
|
139 String arg = (args == null || i >= args.length) ? "" : args[i]; |
|
140 |
|
141 List<CharSequence> subCandidates = new LinkedList<CharSequence>(); |
|
142 |
|
143 if (sub.complete(arg, arg.length(), subCandidates) == -1) { |
|
144 return -1; |
|
145 } |
|
146 |
|
147 if (!subCandidates.contains(arg)) { |
|
148 return -1; |
|
149 } |
|
150 } |
|
151 |
|
152 int ret = completer.complete(list.getCursorArgument(), argpos, candidates); |
|
153 |
|
154 if (ret == -1) { |
|
155 return -1; |
|
156 } |
|
157 |
|
158 int pos = ret + list.getBufferPosition() - argpos; |
|
159 |
|
160 // Special case: when completing in the middle of a line, and the area under the cursor is a delimiter, |
|
161 // then trim any delimiters from the candidates, since we do not need to have an extra delimiter. |
|
162 // |
|
163 // E.g., if we have a completion for "foo", and we enter "f bar" into the buffer, and move to after the "f" |
|
164 // and hit TAB, we want "foo bar" instead of "foo bar". |
|
165 |
|
166 if ((cursor != buffer.length()) && delim.isDelimiter(buffer, cursor)) { |
|
167 for (int i = 0; i < candidates.size(); i++) { |
|
168 CharSequence val = candidates.get(i); |
|
169 |
|
170 while (val.length() > 0 && delim.isDelimiter(val, val.length() - 1)) { |
|
171 val = val.subSequence(0, val.length() - 1); |
|
172 } |
|
173 |
|
174 candidates.set(i, val); |
|
175 } |
|
176 } |
|
177 |
|
178 Log.trace("Completing ", buffer, " (pos=", cursor, ") with: ", candidates, ": offset=", pos); |
|
179 |
|
180 return pos; |
|
181 } |
|
182 |
|
183 /** |
|
184 * The {@link ArgumentCompleter.ArgumentDelimiter} allows custom breaking up of a {@link String} into individual |
|
185 * arguments in order to dispatch the arguments to the nested {@link Completer}. |
|
186 * |
|
187 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> |
|
188 */ |
|
189 public static interface ArgumentDelimiter |
|
190 { |
|
191 /** |
|
192 * Break the specified buffer into individual tokens that can be completed on their own. |
|
193 * |
|
194 * @param buffer The buffer to split |
|
195 * @param pos The current position of the cursor in the buffer |
|
196 * @return The tokens |
|
197 */ |
|
198 ArgumentList delimit(CharSequence buffer, int pos); |
|
199 |
|
200 /** |
|
201 * Returns true if the specified character is a whitespace parameter. |
|
202 * |
|
203 * @param buffer The complete command buffer |
|
204 * @param pos The index of the character in the buffer |
|
205 * @return True if the character should be a delimiter |
|
206 */ |
|
207 boolean isDelimiter(CharSequence buffer, int pos); |
|
208 } |
|
209 |
|
210 /** |
|
211 * Abstract implementation of a delimiter that uses the {@link #isDelimiter} method to determine if a particular |
|
212 * character should be used as a delimiter. |
|
213 * |
|
214 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> |
|
215 */ |
|
216 public abstract static class AbstractArgumentDelimiter |
|
217 implements ArgumentDelimiter |
|
218 { |
|
219 private char[] quoteChars = {'\'', '"'}; |
|
220 |
|
221 private char[] escapeChars = {'\\'}; |
|
222 |
|
223 public void setQuoteChars(final char[] chars) { |
|
224 this.quoteChars = chars; |
|
225 } |
|
226 |
|
227 public char[] getQuoteChars() { |
|
228 return this.quoteChars; |
|
229 } |
|
230 |
|
231 public void setEscapeChars(final char[] chars) { |
|
232 this.escapeChars = chars; |
|
233 } |
|
234 |
|
235 public char[] getEscapeChars() { |
|
236 return this.escapeChars; |
|
237 } |
|
238 |
|
239 public ArgumentList delimit(final CharSequence buffer, final int cursor) { |
|
240 List<String> args = new LinkedList<String>(); |
|
241 StringBuilder arg = new StringBuilder(); |
|
242 int argpos = -1; |
|
243 int bindex = -1; |
|
244 int quoteStart = -1; |
|
245 |
|
246 for (int i = 0; (buffer != null) && (i < buffer.length()); i++) { |
|
247 // once we reach the cursor, set the |
|
248 // position of the selected index |
|
249 if (i == cursor) { |
|
250 bindex = args.size(); |
|
251 // the position in the current argument is just the |
|
252 // length of the current argument |
|
253 argpos = arg.length(); |
|
254 } |
|
255 |
|
256 if (quoteStart < 0 && isQuoteChar(buffer, i)) { |
|
257 // Start a quote block |
|
258 quoteStart = i; |
|
259 } else if (quoteStart >= 0) { |
|
260 // In a quote block |
|
261 if (buffer.charAt(quoteStart) == buffer.charAt(i) && !isEscaped(buffer, i)) { |
|
262 // End the block; arg could be empty, but that's fine |
|
263 args.add(arg.toString()); |
|
264 arg.setLength(0); |
|
265 quoteStart = -1; |
|
266 } else if (!isEscapeChar(buffer, i)) { |
|
267 // Take the next character |
|
268 arg.append(buffer.charAt(i)); |
|
269 } |
|
270 } else { |
|
271 // Not in a quote block |
|
272 if (isDelimiter(buffer, i)) { |
|
273 if (arg.length() > 0) { |
|
274 args.add(arg.toString()); |
|
275 arg.setLength(0); // reset the arg |
|
276 } |
|
277 } else if (!isEscapeChar(buffer, i)) { |
|
278 arg.append(buffer.charAt(i)); |
|
279 } |
|
280 } |
|
281 } |
|
282 |
|
283 if (cursor == buffer.length()) { |
|
284 bindex = args.size(); |
|
285 // the position in the current argument is just the |
|
286 // length of the current argument |
|
287 argpos = arg.length(); |
|
288 } |
|
289 if (arg.length() > 0) { |
|
290 args.add(arg.toString()); |
|
291 } |
|
292 |
|
293 return new ArgumentList(args.toArray(new String[args.size()]), bindex, argpos, cursor); |
|
294 } |
|
295 |
|
296 /** |
|
297 * Returns true if the specified character is a whitespace parameter. Check to ensure that the character is not |
|
298 * escaped by any of {@link #getQuoteChars}, and is not escaped by ant of the {@link #getEscapeChars}, and |
|
299 * returns true from {@link #isDelimiterChar}. |
|
300 * |
|
301 * @param buffer The complete command buffer |
|
302 * @param pos The index of the character in the buffer |
|
303 * @return True if the character should be a delimiter |
|
304 */ |
|
305 public boolean isDelimiter(final CharSequence buffer, final int pos) { |
|
306 return !isQuoted(buffer, pos) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos); |
|
307 } |
|
308 |
|
309 public boolean isQuoted(final CharSequence buffer, final int pos) { |
|
310 return false; |
|
311 } |
|
312 |
|
313 public boolean isQuoteChar(final CharSequence buffer, final int pos) { |
|
314 if (pos < 0) { |
|
315 return false; |
|
316 } |
|
317 |
|
318 for (int i = 0; (quoteChars != null) && (i < quoteChars.length); i++) { |
|
319 if (buffer.charAt(pos) == quoteChars[i]) { |
|
320 return !isEscaped(buffer, pos); |
|
321 } |
|
322 } |
|
323 |
|
324 return false; |
|
325 } |
|
326 |
|
327 /** |
|
328 * Check if this character is a valid escape char (i.e. one that has not been escaped) |
|
329 */ |
|
330 public boolean isEscapeChar(final CharSequence buffer, final int pos) { |
|
331 if (pos < 0) { |
|
332 return false; |
|
333 } |
|
334 |
|
335 for (int i = 0; (escapeChars != null) && (i < escapeChars.length); i++) { |
|
336 if (buffer.charAt(pos) == escapeChars[i]) { |
|
337 return !isEscaped(buffer, pos); // escape escape |
|
338 } |
|
339 } |
|
340 |
|
341 return false; |
|
342 } |
|
343 |
|
344 /** |
|
345 * Check if a character is escaped (i.e. if the previous character is an escape) |
|
346 * |
|
347 * @param buffer |
|
348 * the buffer to check in |
|
349 * @param pos |
|
350 * the position of the character to check |
|
351 * @return true if the character at the specified position in the given buffer is an escape character and the character immediately preceding it is not an |
|
352 * escape character. |
|
353 */ |
|
354 public boolean isEscaped(final CharSequence buffer, final int pos) { |
|
355 if (pos <= 0) { |
|
356 return false; |
|
357 } |
|
358 |
|
359 return isEscapeChar(buffer, pos - 1); |
|
360 } |
|
361 |
|
362 /** |
|
363 * Returns true if the character at the specified position if a delimiter. This method will only be called if |
|
364 * the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by ant of the |
|
365 * {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead. |
|
366 */ |
|
367 public abstract boolean isDelimiterChar(CharSequence buffer, int pos); |
|
368 } |
|
369 |
|
370 /** |
|
371 * {@link ArgumentCompleter.ArgumentDelimiter} implementation that counts all whitespace (as reported by |
|
372 * {@link Character#isWhitespace}) as being a delimiter. |
|
373 * |
|
374 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> |
|
375 */ |
|
376 public static class WhitespaceArgumentDelimiter |
|
377 extends AbstractArgumentDelimiter |
|
378 { |
|
379 /** |
|
380 * The character is a delimiter if it is whitespace, and the |
|
381 * preceding character is not an escape character. |
|
382 */ |
|
383 @Override |
|
384 public boolean isDelimiterChar(final CharSequence buffer, final int pos) { |
|
385 return Character.isWhitespace(buffer.charAt(pos)); |
|
386 } |
|
387 } |
|
388 |
|
389 /** |
|
390 * The result of a delimited buffer. |
|
391 * |
|
392 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a> |
|
393 */ |
|
394 public static class ArgumentList |
|
395 { |
|
396 private String[] arguments; |
|
397 |
|
398 private int cursorArgumentIndex; |
|
399 |
|
400 private int argumentPosition; |
|
401 |
|
402 private int bufferPosition; |
|
403 |
|
404 /** |
|
405 * @param arguments The array of tokens |
|
406 * @param cursorArgumentIndex The token index of the cursor |
|
407 * @param argumentPosition The position of the cursor in the current token |
|
408 * @param bufferPosition The position of the cursor in the whole buffer |
|
409 */ |
|
410 public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition) { |
|
411 this.arguments = checkNotNull(arguments); |
|
412 this.cursorArgumentIndex = cursorArgumentIndex; |
|
413 this.argumentPosition = argumentPosition; |
|
414 this.bufferPosition = bufferPosition; |
|
415 } |
|
416 |
|
417 public void setCursorArgumentIndex(final int i) { |
|
418 this.cursorArgumentIndex = i; |
|
419 } |
|
420 |
|
421 public int getCursorArgumentIndex() { |
|
422 return this.cursorArgumentIndex; |
|
423 } |
|
424 |
|
425 public String getCursorArgument() { |
|
426 if ((cursorArgumentIndex < 0) || (cursorArgumentIndex >= arguments.length)) { |
|
427 return null; |
|
428 } |
|
429 |
|
430 return arguments[cursorArgumentIndex]; |
|
431 } |
|
432 |
|
433 public void setArgumentPosition(final int pos) { |
|
434 this.argumentPosition = pos; |
|
435 } |
|
436 |
|
437 public int getArgumentPosition() { |
|
438 return this.argumentPosition; |
|
439 } |
|
440 |
|
441 public void setArguments(final String[] arguments) { |
|
442 this.arguments = arguments; |
|
443 } |
|
444 |
|
445 public String[] getArguments() { |
|
446 return this.arguments; |
|
447 } |
|
448 |
|
449 public void setBufferPosition(final int pos) { |
|
450 this.bufferPosition = pos; |
|
451 } |
|
452 |
|
453 public int getBufferPosition() { |
|
454 return this.bufferPosition; |
|
455 } |
|
456 } |
|
457 } |
|