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 */
007package jline;
008
009import 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 */
055public 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}