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 jdk.internal.jline.console.completer;
  10 
  11 import jdk.internal.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 jdk.internal.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 }