1 /*
2 * Copyright (c) 2002-2012, 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 jline.console.completer;
10
11 import 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 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.size() == 0) {
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 * @param buffer
331 * @param pos
332 * @return
333 */
334 public boolean isEscapeChar(final CharSequence buffer, final int pos) {
335 if (pos < 0) {
336 return false;
337 }
338
339 for (int i = 0; (escapeChars != null) && (i < escapeChars.length); i++) {
340 if (buffer.charAt(pos) == escapeChars[i]) {
341 return !isEscaped(buffer, pos); // escape escape
342 }
343 }
344
345 return false;
346 }
347
348 /**
349 * Check if a character is escaped (i.e. if the previous character is an escape)
350 *
351 * @param buffer
352 * the buffer to check in
353 * @param pos
354 * the position of the character to check
355 * @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
356 * escape character.
357 */
358 public boolean isEscaped(final CharSequence buffer, final int pos) {
359 if (pos <= 0) {
360 return false;
361 }
362
363 return isEscapeChar(buffer, pos - 1);
364 }
365
366 /**
367 * Returns true if the character at the specified position if a delimiter. This method will only be called if
368 * the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by ant of the
369 * {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead.
370 */
371 public abstract boolean isDelimiterChar(CharSequence buffer, int pos);
372 }
373
374 /**
375 * {@link ArgumentCompleter.ArgumentDelimiter} implementation that counts all whitespace (as reported by
376 * {@link Character#isWhitespace}) as being a delimiter.
377 *
378 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
379 */
380 public static class WhitespaceArgumentDelimiter
381 extends AbstractArgumentDelimiter
382 {
383 /**
384 * The character is a delimiter if it is whitespace, and the
385 * preceding character is not an escape character.
386 */
387 @Override
388 public boolean isDelimiterChar(final CharSequence buffer, final int pos) {
389 return Character.isWhitespace(buffer.charAt(pos));
390 }
391 }
392
393 /**
394 * The result of a delimited buffer.
395 *
396 * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
397 */
398 public static class ArgumentList
399 {
400 private String[] arguments;
401
402 private int cursorArgumentIndex;
403
404 private int argumentPosition;
405
406 private int bufferPosition;
407
408 /**
409 * @param arguments The array of tokens
410 * @param cursorArgumentIndex The token index of the cursor
411 * @param argumentPosition The position of the cursor in the current token
412 * @param bufferPosition The position of the cursor in the whole buffer
413 */
414 public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition) {
415 this.arguments = checkNotNull(arguments);
416 this.cursorArgumentIndex = cursorArgumentIndex;
417 this.argumentPosition = argumentPosition;
418 this.bufferPosition = bufferPosition;
419 }
420
421 public void setCursorArgumentIndex(final int i) {
422 this.cursorArgumentIndex = i;
423 }
424
425 public int getCursorArgumentIndex() {
426 return this.cursorArgumentIndex;
427 }
428
429 public String getCursorArgument() {
430 if ((cursorArgumentIndex < 0) || (cursorArgumentIndex >= arguments.length)) {
431 return null;
432 }
433
434 return arguments[cursorArgumentIndex];
435 }
436
437 public void setArgumentPosition(final int pos) {
438 this.argumentPosition = pos;
439 }
440
441 public int getArgumentPosition() {
442 return this.argumentPosition;
443 }
444
445 public void setArguments(final String[] arguments) {
446 this.arguments = arguments;
447 }
448
449 public String[] getArguments() {
450 return this.arguments;
451 }
452
453 public void setBufferPosition(final int pos) {
454 this.bufferPosition = pos;
455 }
456
457 public int getBufferPosition() {
458 return this.bufferPosition;
459 }
460 }
461 }