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[] {
068                     completor
069                 });
070        }
071    
072        /**
073         *  Constuctor: create a new completor with the default
074         *  argument separator of " ".
075         *
076         *  @param  completors  the List of completors to use
077         */
078        public ArgumentCompletor(final List completors) {
079            this((Completor[]) completors.toArray(new Completor[completors.size()]));
080        }
081    
082        /**
083         *  Constuctor: create a new completor with the default
084         *  argument separator of " ".
085         *
086         *  @param  completors  the embedded argument completors
087         */
088        public ArgumentCompletor(final Completor[] completors) {
089            this(completors, new WhitespaceArgumentDelimiter());
090        }
091    
092        /**
093         *  Constuctor: create a new completor with the specified
094         *  argument delimiter.
095         *
096         *  @param  completor the embedded completor
097         *  @param  delim     the delimiter for parsing arguments
098         */
099        public ArgumentCompletor(final Completor completor,
100                                 final ArgumentDelimiter delim) {
101            this(new Completor[] {
102                     completor
103                 }, delim);
104        }
105    
106        /**
107         *  Constuctor: create a new completor with the specified
108         *  argument delimiter.
109         *
110         *  @param  completors the embedded completors
111         *  @param  delim      the delimiter for parsing arguments
112         */
113        public ArgumentCompletor(final Completor[] completors,
114                                 final ArgumentDelimiter delim) {
115            this.completors = completors;
116            this.delim = delim;
117        }
118    
119        /**
120         *  If true, a completion at argument index N will only succeed
121         *  if all the completions from 0-(N-1) also succeed.
122         */
123        public void setStrict(final boolean strict) {
124            this.strict = strict;
125        }
126    
127        /**
128         *  Returns whether a completion at argument index N will succees
129         *  if all the completions from arguments 0-(N-1) also succeed.
130         */
131        public boolean getStrict() {
132            return this.strict;
133        }
134    
135        public int complete(final String buffer, final int cursor,
136                            final List candidates) {
137            ArgumentList list = delim.delimit(buffer, cursor);
138            int argpos = list.getArgumentPosition();
139            int argIndex = list.getCursorArgumentIndex();
140    
141            if (argIndex < 0) {
142                return -1;
143            }
144    
145            final Completor comp;
146    
147            // if we are beyond the end of the completors, just use the last one
148            if (argIndex >= completors.length) {
149                comp = completors[completors.length - 1];
150            } else {
151                comp = completors[argIndex];
152            }
153    
154            // ensure that all the previous completors are successful before
155            // allowing this completor to pass (only if strict is true).
156            for (int i = 0; getStrict() && (i < argIndex); i++) {
157                Completor sub =
158                    completors[(i >= completors.length) ? (completors.length - 1) : i];
159                String[] args = list.getArguments();
160                String arg = ((args == null) || (i >= args.length)) ? "" : args[i];
161    
162                List subCandidates = new LinkedList();
163    
164                if (sub.complete(arg, arg.length(), subCandidates) == -1) {
165                    return -1;
166                }
167    
168                if (subCandidates.size() == 0) {
169                    return -1;
170                }
171            }
172    
173            int ret = comp.complete(list.getCursorArgument(), argpos, candidates);
174    
175            if (ret == -1) {
176                return -1;
177            }
178    
179            int pos = ret + (list.getBufferPosition() - argpos);
180    
181            /**
182             *  Special case: when completing in the middle of a line, and the
183             *  area under the cursor is a delimiter, then trim any delimiters
184             *  from the candidates, since we do not need to have an extra
185             *  delimiter.
186             *
187             *  E.g., if we have a completion for "foo", and we
188             *  enter "f bar" into the buffer, and move to after the "f"
189             *  and hit TAB, we want "foo bar" instead of "foo  bar".
190             */
191            if ((cursor != buffer.length()) && 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
248            implements 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, final int cursor) {
269                List args = new LinkedList();
270                StringBuffer arg = new StringBuffer();
271                int argpos = -1;
272                int bindex = -1;
273    
274                for (int i = 0; (buffer != null) && (i <= buffer.length()); i++) {
275                    // once we reach the cursor, set the
276                    // position of the selected index
277                    if (i == cursor) {
278                        bindex = args.size();
279                        // the position in the current argument is just the
280                        // length of the current argument
281                        argpos = arg.length();
282                    }
283    
284                    if ((i == buffer.length()) || isDelimiter(buffer, i)) {
285                        if (arg.length() > 0) {
286                            args.add(arg.toString());
287                            arg.setLength(0); // reset the arg
288                        }
289                    } else {
290                        arg.append(buffer.charAt(i));
291                    }
292                }
293    
294                return new ArgumentList((String[]) args.
295                    toArray(new String[args.size()]), bindex, argpos, cursor);
296            }
297    
298            /**
299             *  Returns true if the specified character is a whitespace
300             *  parameter. Check to ensure that the character is not
301             *  escaped by any of
302             *  {@link #getQuoteChars}, and is not escaped by ant of the
303             *  {@link #getEscapeChars}, and returns true from
304             *  {@link #isDelimiterChar}.
305             *
306             *  @param  buffer the complete command buffer
307             *  @param  pos    the index of the character in the buffer
308             *  @return        true if the character should be a delimiter
309             */
310            public boolean isDelimiter(final String buffer, final int pos) {
311                if (isQuoted(buffer, pos)) {
312                    return false;
313                }
314    
315                if (isEscaped(buffer, pos)) {
316                    return false;
317                }
318    
319                return isDelimiterChar(buffer, pos);
320            }
321    
322            public boolean isQuoted(final String buffer, final int pos) {
323                return false;
324            }
325    
326            public boolean isEscaped(final String buffer, final int pos) {
327                if (pos <= 0) {
328                    return false;
329                }
330    
331                for (int i = 0; (escapeChars != null) && (i < escapeChars.length);
332                         i++) {
333                    if (buffer.charAt(pos) == escapeChars[i]) {
334                        return !isEscaped(buffer, pos - 1); // escape escape
335                    }
336                }
337    
338                return false;
339            }
340    
341            /**
342             *  Returns true if the character at the specified position
343             *  if a delimiter. This method will only be called if the
344             *  character is not enclosed in any of the
345             *  {@link #getQuoteChars}, and is not escaped by ant of the
346             *  {@link #getEscapeChars}. To perform escaping manually,
347             *  override {@link #isDelimiter} instead.
348             */
349            public abstract boolean isDelimiterChar(String buffer, int pos);
350        }
351    
352        /**
353         *  {@link ArgumentCompletor.ArgumentDelimiter}
354         *  implementation that counts all
355         *  whitespace (as reported by {@link Character#isWhitespace})
356         *  as being a delimiter.
357         *
358         *  @author  <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
359         */
360        public static class WhitespaceArgumentDelimiter
361            extends AbstractArgumentDelimiter {
362            /**
363             *  The character is a delimiter if it is whitespace, and the
364             *  preceeding character is not an escape character.
365             */
366            public boolean isDelimiterChar(String buffer, int pos) {
367                return Character.isWhitespace(buffer.charAt(pos));
368            }
369        }
370    
371        /**
372         *  The result of a delimited buffer.
373         *
374         *  @author  <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
375         */
376        public static class ArgumentList {
377            private String[] arguments;
378            private int cursorArgumentIndex;
379            private int argumentPosition;
380            private int bufferPosition;
381    
382            /**
383             *  @param  arguments           the array of tokens
384             *  @param  cursorArgumentIndex the token index of the cursor
385             *  @param  argumentPosition    the position of the cursor in the
386             *                              current token
387             *  @param  bufferPosition      the position of the cursor in
388             *                              the whole buffer
389             */
390            public ArgumentList(String[] arguments, int cursorArgumentIndex,
391                int argumentPosition, int bufferPosition) {
392                this.arguments = arguments;
393                this.cursorArgumentIndex = cursorArgumentIndex;
394                this.argumentPosition = argumentPosition;
395                this.bufferPosition = bufferPosition;
396            }
397    
398            public void setCursorArgumentIndex(int cursorArgumentIndex) {
399                this.cursorArgumentIndex = cursorArgumentIndex;
400            }
401    
402            public int getCursorArgumentIndex() {
403                return this.cursorArgumentIndex;
404            }
405    
406            public String getCursorArgument() {
407                if ((cursorArgumentIndex < 0)
408                    || (cursorArgumentIndex >= arguments.length)) {
409                    return null;
410                }
411    
412                return arguments[cursorArgumentIndex];
413            }
414    
415            public void setArgumentPosition(int argumentPosition) {
416                this.argumentPosition = argumentPosition;
417            }
418    
419            public int getArgumentPosition() {
420                return this.argumentPosition;
421            }
422    
423            public void setArguments(String[] arguments) {
424                this.arguments = arguments;
425            }
426    
427            public String[] getArguments() {
428                return this.arguments;
429            }
430    
431            public void setBufferPosition(int bufferPosition) {
432                this.bufferPosition = bufferPosition;
433            }
434    
435            public int getBufferPosition() {
436                return this.bufferPosition;
437            }
438        }
439    }