--- /dev/null 2015-04-26 06:51:08.003313989 -0700 +++ new/jdk/src/jdk.jline/share/classes/jdk/internal/jline/console/completer/ArgumentCompleter.java 2015-06-18 03:04:40.269632083 -0700 @@ -0,0 +1,461 @@ +/* + * Copyright (c) 2002-2012, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * http://www.opensource.org/licenses/bsd-license.php + */ +package jdk.internal.jline.console.completer; + +import jdk.internal.jline.internal.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import static jdk.internal.jline.internal.Preconditions.checkNotNull; + +/** + * A {@link Completer} implementation that invokes a child completer using the appropriate separator argument. + * This can be used instead of the individual completers having to know about argument parsing semantics. + * + * @author Marc Prud'hommeaux + * @author Jason Dillon + * @since 2.3 + */ +public class ArgumentCompleter + implements Completer +{ + private final ArgumentDelimiter delimiter; + + private final List completers = new ArrayList(); + + private boolean strict = true; + + /** + * Create a new completer with the specified argument delimiter. + * + * @param delimiter The delimiter for parsing arguments + * @param completers The embedded completers + */ + public ArgumentCompleter(final ArgumentDelimiter delimiter, final Collection completers) { + this.delimiter = checkNotNull(delimiter); + checkNotNull(completers); + this.completers.addAll(completers); + } + + /** + * Create a new completer with the specified argument delimiter. + * + * @param delimiter The delimiter for parsing arguments + * @param completers The embedded completers + */ + public ArgumentCompleter(final ArgumentDelimiter delimiter, final Completer... completers) { + this(delimiter, Arrays.asList(completers)); + } + + /** + * Create a new completer with the default {@link WhitespaceArgumentDelimiter}. + * + * @param completers The embedded completers + */ + public ArgumentCompleter(final Completer... completers) { + this(new WhitespaceArgumentDelimiter(), completers); + } + + /** + * Create a new completer with the default {@link WhitespaceArgumentDelimiter}. + * + * @param completers The embedded completers + */ + public ArgumentCompleter(final List completers) { + this(new WhitespaceArgumentDelimiter(), completers); + } + + /** + * If true, a completion at argument index N will only succeed + * if all the completions from 0-(N-1) also succeed. + */ + public void setStrict(final boolean strict) { + this.strict = strict; + } + + /** + * Returns whether a completion at argument index N will success + * if all the completions from arguments 0-(N-1) also succeed. + * + * @return True if strict. + * @since 2.3 + */ + public boolean isStrict() { + return this.strict; + } + + /** + * @since 2.3 + */ + public ArgumentDelimiter getDelimiter() { + return delimiter; + } + + /** + * @since 2.3 + */ + public List getCompleters() { + return completers; + } + + public int complete(final String buffer, final int cursor, final List candidates) { + // buffer can be null + checkNotNull(candidates); + + ArgumentDelimiter delim = getDelimiter(); + ArgumentList list = delim.delimit(buffer, cursor); + int argpos = list.getArgumentPosition(); + int argIndex = list.getCursorArgumentIndex(); + + if (argIndex < 0) { + return -1; + } + + List completers = getCompleters(); + Completer completer; + + // if we are beyond the end of the completers, just use the last one + if (argIndex >= completers.size()) { + completer = completers.get(completers.size() - 1); + } + else { + completer = completers.get(argIndex); + } + + // ensure that all the previous completers are successful before allowing this completer to pass (only if strict). + for (int i = 0; isStrict() && (i < argIndex); i++) { + Completer sub = completers.get(i >= completers.size() ? (completers.size() - 1) : i); + String[] args = list.getArguments(); + String arg = (args == null || i >= args.length) ? "" : args[i]; + + List subCandidates = new LinkedList(); + + if (sub.complete(arg, arg.length(), subCandidates) == -1) { + return -1; + } + + if (subCandidates.size() == 0) { + return -1; + } + } + + int ret = completer.complete(list.getCursorArgument(), argpos, candidates); + + if (ret == -1) { + return -1; + } + + int pos = ret + list.getBufferPosition() - argpos; + + // Special case: when completing in the middle of a line, and the area under the cursor is a delimiter, + // then trim any delimiters from the candidates, since we do not need to have an extra delimiter. + // + // E.g., if we have a completion for "foo", and we enter "f bar" into the buffer, and move to after the "f" + // and hit TAB, we want "foo bar" instead of "foo bar". + + if ((cursor != buffer.length()) && delim.isDelimiter(buffer, cursor)) { + for (int i = 0; i < candidates.size(); i++) { + CharSequence val = candidates.get(i); + + while (val.length() > 0 && delim.isDelimiter(val, val.length() - 1)) { + val = val.subSequence(0, val.length() - 1); + } + + candidates.set(i, val); + } + } + + Log.trace("Completing ", buffer, " (pos=", cursor, ") with: ", candidates, ": offset=", pos); + + return pos; + } + + /** + * The {@link ArgumentCompleter.ArgumentDelimiter} allows custom breaking up of a {@link String} into individual + * arguments in order to dispatch the arguments to the nested {@link Completer}. + * + * @author Marc Prud'hommeaux + */ + public static interface ArgumentDelimiter + { + /** + * Break the specified buffer into individual tokens that can be completed on their own. + * + * @param buffer The buffer to split + * @param pos The current position of the cursor in the buffer + * @return The tokens + */ + ArgumentList delimit(CharSequence buffer, int pos); + + /** + * Returns true if the specified character is a whitespace parameter. + * + * @param buffer The complete command buffer + * @param pos The index of the character in the buffer + * @return True if the character should be a delimiter + */ + boolean isDelimiter(CharSequence buffer, int pos); + } + + /** + * Abstract implementation of a delimiter that uses the {@link #isDelimiter} method to determine if a particular + * character should be used as a delimiter. + * + * @author Marc Prud'hommeaux + */ + public abstract static class AbstractArgumentDelimiter + implements ArgumentDelimiter + { + private char[] quoteChars = {'\'', '"'}; + + private char[] escapeChars = {'\\'}; + + public void setQuoteChars(final char[] chars) { + this.quoteChars = chars; + } + + public char[] getQuoteChars() { + return this.quoteChars; + } + + public void setEscapeChars(final char[] chars) { + this.escapeChars = chars; + } + + public char[] getEscapeChars() { + return this.escapeChars; + } + + public ArgumentList delimit(final CharSequence buffer, final int cursor) { + List args = new LinkedList(); + StringBuilder arg = new StringBuilder(); + int argpos = -1; + int bindex = -1; + int quoteStart = -1; + + for (int i = 0; (buffer != null) && (i < buffer.length()); i++) { + // once we reach the cursor, set the + // position of the selected index + if (i == cursor) { + bindex = args.size(); + // the position in the current argument is just the + // length of the current argument + argpos = arg.length(); + } + + if (quoteStart < 0 && isQuoteChar(buffer, i)) { + // Start a quote block + quoteStart = i; + } else if (quoteStart >= 0) { + // In a quote block + if (buffer.charAt(quoteStart) == buffer.charAt(i) && !isEscaped(buffer, i)) { + // End the block; arg could be empty, but that's fine + args.add(arg.toString()); + arg.setLength(0); + quoteStart = -1; + } else if (!isEscapeChar(buffer, i)) { + // Take the next character + arg.append(buffer.charAt(i)); + } + } else { + // Not in a quote block + if (isDelimiter(buffer, i)) { + if (arg.length() > 0) { + args.add(arg.toString()); + arg.setLength(0); // reset the arg + } + } else if (!isEscapeChar(buffer, i)) { + arg.append(buffer.charAt(i)); + } + } + } + + if (cursor == buffer.length()) { + bindex = args.size(); + // the position in the current argument is just the + // length of the current argument + argpos = arg.length(); + } + if (arg.length() > 0) { + args.add(arg.toString()); + } + + return new ArgumentList(args.toArray(new String[args.size()]), bindex, argpos, cursor); + } + + /** + * Returns true if the specified character is a whitespace parameter. Check to ensure that the character is not + * escaped by any of {@link #getQuoteChars}, and is not escaped by ant of the {@link #getEscapeChars}, and + * returns true from {@link #isDelimiterChar}. + * + * @param buffer The complete command buffer + * @param pos The index of the character in the buffer + * @return True if the character should be a delimiter + */ + public boolean isDelimiter(final CharSequence buffer, final int pos) { + return !isQuoted(buffer, pos) && !isEscaped(buffer, pos) && isDelimiterChar(buffer, pos); + } + + public boolean isQuoted(final CharSequence buffer, final int pos) { + return false; + } + + public boolean isQuoteChar(final CharSequence buffer, final int pos) { + if (pos < 0) { + return false; + } + + for (int i = 0; (quoteChars != null) && (i < quoteChars.length); i++) { + if (buffer.charAt(pos) == quoteChars[i]) { + return !isEscaped(buffer, pos); + } + } + + return false; + } + + /** + * Check if this character is a valid escape char (i.e. one that has not been escaped) + * + * @param buffer + * @param pos + * @return + */ + public boolean isEscapeChar(final CharSequence buffer, final int pos) { + if (pos < 0) { + return false; + } + + for (int i = 0; (escapeChars != null) && (i < escapeChars.length); i++) { + if (buffer.charAt(pos) == escapeChars[i]) { + return !isEscaped(buffer, pos); // escape escape + } + } + + return false; + } + + /** + * Check if a character is escaped (i.e. if the previous character is an escape) + * + * @param buffer + * the buffer to check in + * @param pos + * the position of the character to check + * @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 + * escape character. + */ + public boolean isEscaped(final CharSequence buffer, final int pos) { + if (pos <= 0) { + return false; + } + + return isEscapeChar(buffer, pos - 1); + } + + /** + * Returns true if the character at the specified position if a delimiter. This method will only be called if + * the character is not enclosed in any of the {@link #getQuoteChars}, and is not escaped by ant of the + * {@link #getEscapeChars}. To perform escaping manually, override {@link #isDelimiter} instead. + */ + public abstract boolean isDelimiterChar(CharSequence buffer, int pos); + } + + /** + * {@link ArgumentCompleter.ArgumentDelimiter} implementation that counts all whitespace (as reported by + * {@link Character#isWhitespace}) as being a delimiter. + * + * @author Marc Prud'hommeaux + */ + public static class WhitespaceArgumentDelimiter + extends AbstractArgumentDelimiter + { + /** + * The character is a delimiter if it is whitespace, and the + * preceding character is not an escape character. + */ + @Override + public boolean isDelimiterChar(final CharSequence buffer, final int pos) { + return Character.isWhitespace(buffer.charAt(pos)); + } + } + + /** + * The result of a delimited buffer. + * + * @author Marc Prud'hommeaux + */ + public static class ArgumentList + { + private String[] arguments; + + private int cursorArgumentIndex; + + private int argumentPosition; + + private int bufferPosition; + + /** + * @param arguments The array of tokens + * @param cursorArgumentIndex The token index of the cursor + * @param argumentPosition The position of the cursor in the current token + * @param bufferPosition The position of the cursor in the whole buffer + */ + public ArgumentList(final String[] arguments, final int cursorArgumentIndex, final int argumentPosition, final int bufferPosition) { + this.arguments = checkNotNull(arguments); + this.cursorArgumentIndex = cursorArgumentIndex; + this.argumentPosition = argumentPosition; + this.bufferPosition = bufferPosition; + } + + public void setCursorArgumentIndex(final int i) { + this.cursorArgumentIndex = i; + } + + public int getCursorArgumentIndex() { + return this.cursorArgumentIndex; + } + + public String getCursorArgument() { + if ((cursorArgumentIndex < 0) || (cursorArgumentIndex >= arguments.length)) { + return null; + } + + return arguments[cursorArgumentIndex]; + } + + public void setArgumentPosition(final int pos) { + this.argumentPosition = pos; + } + + public int getArgumentPosition() { + return this.argumentPosition; + } + + public void setArguments(final String[] arguments) { + this.arguments = arguments; + } + + public String[] getArguments() { + return this.arguments; + } + + public void setBufferPosition(final int pos) { + this.bufferPosition = pos; + } + + public int getBufferPosition() { + return this.bufferPosition; + } + } +}