src/jdk.internal.le/share/classes/jdk/internal/jline/console/completer/ArgumentCompleter.java
changeset 53333 fd6de53a0d6e
parent 53332 ab474ef0a0ac
parent 53010 086dfcfc3731
child 53334 b94283cb226b
equal deleted inserted replaced
53332:ab474ef0a0ac 53333:fd6de53a0d6e
     1 /*
       
     2  * Copyright (c) 2002-2016, 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.contains(arg)) {
       
   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         public boolean isEscapeChar(final CharSequence buffer, final int pos) {
       
   331             if (pos < 0) {
       
   332                 return false;
       
   333             }
       
   334 
       
   335             for (int i = 0; (escapeChars != null) && (i < escapeChars.length); i++) {
       
   336                 if (buffer.charAt(pos) == escapeChars[i]) {
       
   337                     return !isEscaped(buffer, pos); // escape escape
       
   338                 }
       
   339             }
       
   340 
       
   341             return false;
       
   342         }
       
   343 
       
   344         /**
       
   345          * Check if a character is escaped (i.e. if the previous character is an escape)
       
   346          *
       
   347          * @param buffer
       
   348          *          the buffer to check in
       
   349          * @param pos
       
   350          *          the position of the character to check
       
   351          * @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
       
   352          *         escape character.
       
   353          */
       
   354         public boolean isEscaped(final CharSequence buffer, final int pos) {
       
   355             if (pos <= 0) {
       
   356                 return false;
       
   357             }
       
   358 
       
   359             return isEscapeChar(buffer, pos - 1);
       
   360         }
       
   361 
       
   362         /**
       
   363          * Returns true if the character at the specified position if a delimiter. This method will only be called if
       
   364          * the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by ant of the
       
   365          * {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead.
       
   366          */
       
   367         public abstract boolean isDelimiterChar(CharSequence buffer, int pos);
       
   368     }
       
   369 
       
   370     /**
       
   371      * {@link ArgumentCompleter.ArgumentDelimiter} implementation that counts all whitespace (as reported by
       
   372      * {@link Character#isWhitespace}) as being a delimiter.
       
   373      *
       
   374      * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
       
   375      */
       
   376     public static class WhitespaceArgumentDelimiter
       
   377         extends AbstractArgumentDelimiter
       
   378     {
       
   379         /**
       
   380          * The character is a delimiter if it is whitespace, and the
       
   381          * preceding character is not an escape character.
       
   382          */
       
   383         @Override
       
   384         public boolean isDelimiterChar(final CharSequence buffer, final int pos) {
       
   385             return Character.isWhitespace(buffer.charAt(pos));
       
   386         }
       
   387     }
       
   388 
       
   389     /**
       
   390      * The result of a delimited buffer.
       
   391      *
       
   392      * @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
       
   393      */
       
   394     public static class ArgumentList
       
   395     {
       
   396         private String[] arguments;
       
   397 
       
   398         private int cursorArgumentIndex;
       
   399 
       
   400         private int argumentPosition;
       
   401 
       
   402         private int bufferPosition;
       
   403 
       
   404         /**
       
   405          * @param arguments             The array of tokens
       
   406          * @param cursorArgumentIndex   The token index of the cursor
       
   407          * @param argumentPosition      The position of the cursor in the current token
       
   408          * @param bufferPosition        The position of the cursor in the whole buffer
       
   409          */
       
   410         public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition) {
       
   411             this.arguments = checkNotNull(arguments);
       
   412             this.cursorArgumentIndex = cursorArgumentIndex;
       
   413             this.argumentPosition = argumentPosition;
       
   414             this.bufferPosition = bufferPosition;
       
   415         }
       
   416 
       
   417         public void setCursorArgumentIndex(final int i) {
       
   418             this.cursorArgumentIndex = i;
       
   419         }
       
   420 
       
   421         public int getCursorArgumentIndex() {
       
   422             return this.cursorArgumentIndex;
       
   423         }
       
   424 
       
   425         public String getCursorArgument() {
       
   426             if ((cursorArgumentIndex < 0) || (cursorArgumentIndex >= arguments.length)) {
       
   427                 return null;
       
   428             }
       
   429 
       
   430             return arguments[cursorArgumentIndex];
       
   431         }
       
   432 
       
   433         public void setArgumentPosition(final int pos) {
       
   434             this.argumentPosition = pos;
       
   435         }
       
   436 
       
   437         public int getArgumentPosition() {
       
   438             return this.argumentPosition;
       
   439         }
       
   440 
       
   441         public void setArguments(final String[] arguments) {
       
   442             this.arguments = arguments;
       
   443         }
       
   444 
       
   445         public String[] getArguments() {
       
   446             return this.arguments;
       
   447         }
       
   448 
       
   449         public void setBufferPosition(final int pos) {
       
   450             this.bufferPosition = pos;
       
   451         }
       
   452 
       
   453         public int getBufferPosition() {
       
   454             return this.bufferPosition;
       
   455         }
       
   456     }
       
   457 }