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