001: /*
002: * Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
003: *
004: * This software is distributable under the BSD license. See the terms of the
005: * BSD license in the documentation provided with this software.
006: */
007: package jline;
008:
009: import java.util.*;
010:
011: /**
012: * A {@link Completor} implementation that invokes a child completor
013: * using the appropriate <i>separator</i> argument. This
014: * can be used instead of the individual completors having to
015: * know about argument parsing semantics.
016: * <p>
017: * <strong>Example 1</strong>: Any argument of the command line can
018: * use file completion.
019: * <p>
020: * <pre>
021: * consoleReader.addCompletor (new ArgumentCompletor (
022: * new {@link FileNameCompletor} ()))
023: * </pre>
024: * <p>
025: * <strong>Example 2</strong>: The first argument of the command line
026: * can be completed with any of "foo", "bar", or "baz", and remaining
027: * arguments can be completed with a file name.
028: * <p>
029: * <pre>
030: * consoleReader.addCompletor (new ArgumentCompletor (
031: * new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"})));
032: * consoleReader.addCompletor (new ArgumentCompletor (
033: * new {@link FileNameCompletor} ()));
034: * </pre>
035: *
036: * <p>
037: * When the argument index is past the last embedded completors, the last
038: * completors is always used. To disable this behavior, have the last
039: * completor be a {@link NullCompletor}. For example:
040: * </p>
041: *
042: * <pre>
043: * consoleReader.addCompletor (new ArgumentCompletor (
044: * new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"}),
045: * new {@link SimpleCompletor} (new String [] { "xxx", "yyy", "xxx"}),
046: * new {@link NullCompletor}
047: * ));
048: * </pre>
049: * <p>
050: * TODO: handle argument quoting and escape characters
051: * </p>
052: *
053: * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
054: */
055: public class ArgumentCompletor implements Completor {
056: final Completor[] completors;
057: final ArgumentDelimiter delim;
058: boolean strict = true;
059:
060: /**
061: * Constuctor: create a new completor with the default
062: * argument separator of " ".
063: *
064: * @param completor the embedded completor
065: */
066: public ArgumentCompletor(final Completor completor) {
067: this (new Completor[] { completor });
068: }
069:
070: /**
071: * Constuctor: create a new completor with the default
072: * argument separator of " ".
073: *
074: * @param completors the List of completors to use
075: */
076: public ArgumentCompletor(final List completors) {
077: this ((Completor[]) completors.toArray(new Completor[completors
078: .size()]));
079: }
080:
081: /**
082: * Constuctor: create a new completor with the default
083: * argument separator of " ".
084: *
085: * @param completors the embedded argument completors
086: */
087: public ArgumentCompletor(final Completor[] completors) {
088: this (completors, new WhitespaceArgumentDelimiter());
089: }
090:
091: /**
092: * Constuctor: create a new completor with the specified
093: * argument delimiter.
094: *
095: * @param completor the embedded completor
096: * @param delim the delimiter for parsing arguments
097: */
098: public ArgumentCompletor(final Completor completor,
099: final ArgumentDelimiter delim) {
100: this (new Completor[] { completor }, delim);
101: }
102:
103: /**
104: * Constuctor: create a new completor with the specified
105: * argument delimiter.
106: *
107: * @param completors the embedded completors
108: * @param delim the delimiter for parsing arguments
109: */
110: public ArgumentCompletor(final Completor[] completors,
111: final ArgumentDelimiter delim) {
112: this .completors = completors;
113: this .delim = delim;
114: }
115:
116: /**
117: * If true, a completion at argument index N will only succeed
118: * if all the completions from 0-(N-1) also succeed.
119: */
120: public void setStrict(final boolean strict) {
121: this .strict = strict;
122: }
123:
124: /**
125: * Returns whether a completion at argument index N will succees
126: * if all the completions from arguments 0-(N-1) also succeed.
127: */
128: public boolean getStrict() {
129: return this .strict;
130: }
131:
132: public int complete(final String buffer, final int cursor,
133: final List candidates) {
134: ArgumentList list = delim.delimit(buffer, cursor);
135: int argpos = list.getArgumentPosition();
136: int argIndex = list.getCursorArgumentIndex();
137:
138: if (argIndex < 0) {
139: return -1;
140: }
141:
142: final Completor comp;
143:
144: // if we are beyond the end of the completors, just use the last one
145: if (argIndex >= completors.length) {
146: comp = completors[completors.length - 1];
147: } else {
148: comp = completors[argIndex];
149: }
150:
151: // ensure that all the previous completors are successful before
152: // allowing this completor to pass (only if strict is true).
153: for (int i = 0; getStrict() && (i < argIndex); i++) {
154: Completor sub = completors[(i >= completors.length) ? (completors.length - 1)
155: : i];
156: String[] args = list.getArguments();
157: String arg = ((args == null) || (i >= args.length)) ? ""
158: : args[i];
159:
160: List subCandidates = new LinkedList();
161:
162: if (sub.complete(arg, arg.length(), subCandidates) == -1) {
163: return -1;
164: }
165:
166: if (subCandidates.size() == 0) {
167: return -1;
168: }
169: }
170:
171: int ret = comp.complete(list.getCursorArgument(), argpos,
172: candidates);
173:
174: if (ret == -1) {
175: return -1;
176: }
177:
178: int pos = ret + (list.getBufferPosition() - argpos);
179:
180: /**
181: * Special case: when completing in the middle of a line, and the
182: * area under the cursor is a delimiter, then trim any delimiters
183: * from the candidates, since we do not need to have an extra
184: * delimiter.
185: *
186: * E.g., if we have a completion for "foo", and we
187: * enter "f bar" into the buffer, and move to after the "f"
188: * and hit TAB, we want "foo bar" instead of "foo bar".
189: */
190: if ((cursor != buffer.length())
191: && delim.isDelimiter(buffer, cursor)) {
192: for (int i = 0; i < candidates.size(); i++) {
193: String val = candidates.get(i).toString();
194:
195: while ((val.length() > 0)
196: && delim.isDelimiter(val, val.length() - 1)) {
197: val = val.substring(0, val.length() - 1);
198: }
199:
200: candidates.set(i, val);
201: }
202: }
203:
204: ConsoleReader.debug("Completing " + buffer + "(pos=" + cursor
205: + ") " + "with: " + candidates + ": offset=" + pos);
206:
207: return pos;
208: }
209:
210: /**
211: * The {@link ArgumentCompletor.ArgumentDelimiter} allows custom
212: * breaking up of a {@link String} into individual arguments in
213: * order to dispatch the arguments to the nested {@link Completor}.
214: *
215: * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
216: */
217: public static interface ArgumentDelimiter {
218: /**
219: * Break the specified buffer into individual tokens
220: * that can be completed on their own.
221: *
222: * @param buffer the buffer to split
223: * @param argumentPosition the current position of the
224: * cursor in the buffer
225: * @return the tokens
226: */
227: ArgumentList delimit(String buffer, int argumentPosition);
228:
229: /**
230: * Returns true if the specified character is a whitespace
231: * parameter.
232: *
233: * @param buffer the complete command buffer
234: * @param pos the index of the character in the buffer
235: * @return true if the character should be a delimiter
236: */
237: boolean isDelimiter(String buffer, int pos);
238: }
239:
240: /**
241: * Abstract implementation of a delimiter that uses the
242: * {@link #isDelimiter} method to determine if a particular
243: * character should be used as a delimiter.
244: *
245: * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
246: */
247: public abstract static class AbstractArgumentDelimiter implements
248: ArgumentDelimiter {
249: private char[] quoteChars = new char[] { '\'', '"' };
250: private char[] escapeChars = new char[] { '\\' };
251:
252: public void setQuoteChars(final char[] quoteChars) {
253: this .quoteChars = quoteChars;
254: }
255:
256: public char[] getQuoteChars() {
257: return this .quoteChars;
258: }
259:
260: public void setEscapeChars(final char[] escapeChars) {
261: this .escapeChars = escapeChars;
262: }
263:
264: public char[] getEscapeChars() {
265: return this .escapeChars;
266: }
267:
268: public ArgumentList delimit(final String buffer,
269: final int cursor) {
270: List args = new LinkedList();
271: StringBuffer arg = new StringBuffer();
272: int argpos = -1;
273: int bindex = -1;
274:
275: for (int i = 0; (buffer != null) && (i <= buffer.length()); i++) {
276: // once we reach the cursor, set the
277: // position of the selected index
278: if (i == cursor) {
279: bindex = args.size();
280: // the position in the current argument is just the
281: // length of the current argument
282: argpos = arg.length();
283: }
284:
285: if ((i == buffer.length()) || isDelimiter(buffer, i)) {
286: if (arg.length() > 0) {
287: args.add(arg.toString());
288: arg.setLength(0); // reset the arg
289: }
290: } else {
291: arg.append(buffer.charAt(i));
292: }
293: }
294:
295: return new ArgumentList((String[]) args
296: .toArray(new String[args.size()]), bindex, argpos,
297: cursor);
298: }
299:
300: /**
301: * Returns true if the specified character is a whitespace
302: * parameter. Check to ensure that the character is not
303: * escaped by any of
304: * {@link #getQuoteChars}, and is not escaped by ant of the
305: * {@link #getEscapeChars}, and returns true from
306: * {@link #isDelimiterChar}.
307: *
308: * @param buffer the complete command buffer
309: * @param pos the index of the character in the buffer
310: * @return true if the character should be a delimiter
311: */
312: public boolean isDelimiter(final String buffer, final int pos) {
313: if (isQuoted(buffer, pos)) {
314: return false;
315: }
316:
317: if (isEscaped(buffer, pos)) {
318: return false;
319: }
320:
321: return isDelimiterChar(buffer, pos);
322: }
323:
324: public boolean isQuoted(final String buffer, final int pos) {
325: return false;
326: }
327:
328: public boolean isEscaped(final String buffer, final int pos) {
329: if (pos <= 0) {
330: return false;
331: }
332:
333: for (int i = 0; (escapeChars != null)
334: && (i < escapeChars.length); i++) {
335: if (buffer.charAt(pos) == escapeChars[i]) {
336: return !isEscaped(buffer, pos - 1); // escape escape
337: }
338: }
339:
340: return false;
341: }
342:
343: /**
344: * Returns true if the character at the specified position
345: * if a delimiter. This method will only be called if the
346: * character is not enclosed in any of the
347: * {@link #getQuoteChars}, and is not escaped by ant of the
348: * {@link #getEscapeChars}. To perform escaping manually,
349: * override {@link #isDelimiter} instead.
350: */
351: public abstract boolean isDelimiterChar(String buffer, int pos);
352: }
353:
354: /**
355: * {@link ArgumentCompletor.ArgumentDelimiter}
356: * implementation that counts all
357: * whitespace (as reported by {@link Character#isWhitespace})
358: * as being a delimiter.
359: *
360: * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
361: */
362: public static class WhitespaceArgumentDelimiter extends
363: AbstractArgumentDelimiter {
364: /**
365: * The character is a delimiter if it is whitespace, and the
366: * preceeding character is not an escape character.
367: */
368: public boolean isDelimiterChar(String buffer, int pos) {
369: return Character.isWhitespace(buffer.charAt(pos));
370: }
371: }
372:
373: /**
374: * The result of a delimited buffer.
375: *
376: * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
377: */
378: public static class ArgumentList {
379: private String[] arguments;
380: private int cursorArgumentIndex;
381: private int argumentPosition;
382: private int bufferPosition;
383:
384: /**
385: * @param arguments the array of tokens
386: * @param cursorArgumentIndex the token index of the cursor
387: * @param argumentPosition the position of the cursor in the
388: * current token
389: * @param bufferPosition the position of the cursor in
390: * the whole buffer
391: */
392: public ArgumentList(String[] arguments,
393: int cursorArgumentIndex, int argumentPosition,
394: int bufferPosition) {
395: this .arguments = arguments;
396: this .cursorArgumentIndex = cursorArgumentIndex;
397: this .argumentPosition = argumentPosition;
398: this .bufferPosition = bufferPosition;
399: }
400:
401: public void setCursorArgumentIndex(int cursorArgumentIndex) {
402: this .cursorArgumentIndex = cursorArgumentIndex;
403: }
404:
405: public int getCursorArgumentIndex() {
406: return this .cursorArgumentIndex;
407: }
408:
409: public String getCursorArgument() {
410: if ((cursorArgumentIndex < 0)
411: || (cursorArgumentIndex >= arguments.length)) {
412: return null;
413: }
414:
415: return arguments[cursorArgumentIndex];
416: }
417:
418: public void setArgumentPosition(int argumentPosition) {
419: this .argumentPosition = argumentPosition;
420: }
421:
422: public int getArgumentPosition() {
423: return this .argumentPosition;
424: }
425:
426: public void setArguments(String[] arguments) {
427: this .arguments = arguments;
428: }
429:
430: public String[] getArguments() {
431: return this .arguments;
432: }
433:
434: public void setBufferPosition(int bufferPosition) {
435: this .bufferPosition = bufferPosition;
436: }
437:
438: public int getBufferPosition() {
439: return this.bufferPosition;
440: }
441: }
442: }
|