/*
* Copyright (c) 2002-2007, Marc Prud'hommeaux. All rights reserved.
*
* This software is distributable under the BSD license. See the terms of the
* BSD license in the documentation provided with this software.
*/
package jline;
import java.util.*;
/**
* A {@link Completor} implementation that invokes a child completor
* using the appropriate <i>separator</i> argument. This
* can be used instead of the individual completors having to
* know about argument parsing semantics.
* <p>
* <strong>Example 1</strong>: Any argument of the command line can
* use file completion.
* <p>
* <pre>
* consoleReader.addCompletor (new ArgumentCompletor (
* new {@link FileNameCompletor} ()))
* </pre>
* <p>
* <strong>Example 2</strong>: The first argument of the command line
* can be completed with any of "foo", "bar", or "baz", and remaining
* arguments can be completed with a file name.
* <p>
* <pre>
* consoleReader.addCompletor (new ArgumentCompletor (
* new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"})));
* consoleReader.addCompletor (new ArgumentCompletor (
* new {@link FileNameCompletor} ()));
* </pre>
*
* <p>
* When the argument index is past the last embedded completors, the last
* completors is always used. To disable this behavior, have the last
* completor be a {@link NullCompletor}. For example:
* </p>
*
* <pre>
* consoleReader.addCompletor (new ArgumentCompletor (
* new {@link SimpleCompletor} (new String [] { "foo", "bar", "baz"}),
* new {@link SimpleCompletor} (new String [] { "xxx", "yyy", "xxx"}),
* new {@link NullCompletor}
* ));
* </pre>
* <p>
* TODO: handle argument quoting and escape characters
* </p>
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
*/
public class
ArgumentCompletor implements
Completor {
final
Completor[]
completors;
final
ArgumentDelimiter delim;
boolean
strict = true;
/**
* Constuctor: create a new completor with the default
* argument separator of " ".
*
* @param completor the embedded completor
*/
public
ArgumentCompletor(final
Completor completor) {
this(new
Completor[] {
completor
});
}
/**
* Constuctor: create a new completor with the default
* argument separator of " ".
*
* @param completors the List of completors to use
*/
public
ArgumentCompletor(final
List completors) {
this((
Completor[])
completors.
toArray(new
Completor[
completors.
size()]));
}
/**
* Constuctor: create a new completor with the default
* argument separator of " ".
*
* @param completors the embedded argument completors
*/
public
ArgumentCompletor(final
Completor[]
completors) {
this(
completors, new
WhitespaceArgumentDelimiter());
}
/**
* Constuctor: create a new completor with the specified
* argument delimiter.
*
* @param completor the embedded completor
* @param delim the delimiter for parsing arguments
*/
public
ArgumentCompletor(final
Completor completor,
final
ArgumentDelimiter delim) {
this(new
Completor[] {
completor
},
delim);
}
/**
* Constuctor: create a new completor with the specified
* argument delimiter.
*
* @param completors the embedded completors
* @param delim the delimiter for parsing arguments
*/
public
ArgumentCompletor(final
Completor[]
completors,
final
ArgumentDelimiter delim) {
this.
completors =
completors;
this.
delim =
delim;
}
/**
* 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 succees
* if all the completions from arguments 0-(N-1) also succeed.
*/
public boolean
getStrict() {
return this.
strict;
}
public int
complete(final
String buffer, final int
cursor,
final
List candidates) {
ArgumentList list =
delim.
delimit(
buffer,
cursor);
int
argpos =
list.
getArgumentPosition();
int
argIndex =
list.
getCursorArgumentIndex();
if (
argIndex < 0) {
return -1;
}
final
Completor comp;
// if we are beyond the end of the completors, just use the last one
if (
argIndex >=
completors.length) {
comp =
completors[
completors.length - 1];
} else {
comp =
completors[
argIndex];
}
// ensure that all the previous completors are successful before
// allowing this completor to pass (only if strict is true).
for (int
i = 0;
getStrict() && (
i <
argIndex);
i++) {
Completor sub =
completors[(
i >=
completors.length) ? (
completors.length - 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 =
comp.
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++) {
String val =
candidates.
get(
i).
toString();
while ((
val.
length() > 0)
&&
delim.
isDelimiter(
val,
val.
length() - 1)) {
val =
val.
substring(0,
val.
length() - 1);
}
candidates.
set(
i,
val);
}
}
ConsoleReader.
debug("Completing " +
buffer + "(pos=" +
cursor + ") "
+ "with: " +
candidates + ": offset=" +
pos);
return
pos;
}
/**
* The {@link ArgumentCompletor.ArgumentDelimiter} allows custom
* breaking up of a {@link String} into individual arguments in
* order to dispatch the arguments to the nested {@link Completor}.
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
*/
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 argumentPosition the current position of the
* cursor in the buffer
* @return the tokens
*/
ArgumentList delimit(
String buffer, int
argumentPosition);
/**
* 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(
String 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 <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
*/
public abstract static class
AbstractArgumentDelimiter
implements
ArgumentDelimiter {
private char[]
quoteChars = new char[] { '\'', '"' };
private char[]
escapeChars = new char[] { '\\' };
public void
setQuoteChars(final char[]
quoteChars) {
this.
quoteChars =
quoteChars;
}
public char[]
getQuoteChars() {
return this.
quoteChars;
}
public void
setEscapeChars(final char[]
escapeChars) {
this.
escapeChars =
escapeChars;
}
public char[]
getEscapeChars() {
return this.
escapeChars;
}
public
ArgumentList delimit(final
String buffer, final int
cursor) {
List args = new
LinkedList();
StringBuffer arg = new
StringBuffer();
int
argpos = -1;
int
bindex = -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 ((
i ==
buffer.
length()) ||
isDelimiter(
buffer,
i)) {
if (
arg.
length() > 0) {
args.
add(
arg.
toString());
arg.
setLength(0); // reset the arg
}
} else {
arg.
append(
buffer.
charAt(
i));
}
}
return new
ArgumentList((
String[])
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
String buffer, final int
pos) {
if (
isQuoted(
buffer,
pos)) {
return false;
}
if (
isEscaped(
buffer,
pos)) {
return false;
}
return
isDelimiterChar(
buffer,
pos);
}
public boolean
isQuoted(final
String buffer, final int
pos) {
return false;
}
public boolean
isEscaped(final
String 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 - 1); // escape escape
}
}
return false;
}
/**
* 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(
String buffer, int
pos);
}
/**
* {@link ArgumentCompletor.ArgumentDelimiter}
* implementation that counts all
* whitespace (as reported by {@link Character#isWhitespace})
* as being a delimiter.
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
*/
public static class
WhitespaceArgumentDelimiter
extends
AbstractArgumentDelimiter {
/**
* The character is a delimiter if it is whitespace, and the
* preceeding character is not an escape character.
*/
public boolean
isDelimiterChar(
String buffer, int
pos) {
return
Character.
isWhitespace(
buffer.
charAt(
pos));
}
}
/**
* The result of a delimited buffer.
*
* @author <a href="mailto:mwp1@cornell.edu">Marc Prud'hommeaux</a>
*/
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(
String[]
arguments, int
cursorArgumentIndex,
int
argumentPosition, int
bufferPosition) {
this.
arguments =
arguments;
this.
cursorArgumentIndex =
cursorArgumentIndex;
this.
argumentPosition =
argumentPosition;
this.
bufferPosition =
bufferPosition;
}
public void
setCursorArgumentIndex(int
cursorArgumentIndex) {
this.
cursorArgumentIndex =
cursorArgumentIndex;
}
public int
getCursorArgumentIndex() {
return this.
cursorArgumentIndex;
}
public
String getCursorArgument() {
if ((
cursorArgumentIndex < 0)
|| (
cursorArgumentIndex >=
arguments.length)) {
return null;
}
return
arguments[
cursorArgumentIndex];
}
public void
setArgumentPosition(int
argumentPosition) {
this.
argumentPosition =
argumentPosition;
}
public int
getArgumentPosition() {
return this.
argumentPosition;
}
public void
setArguments(
String[]
arguments) {
this.
arguments =
arguments;
}
public
String[]
getArguments() {
return this.
arguments;
}
public void
setBufferPosition(int
bufferPosition) {
this.
bufferPosition =
bufferPosition;
}
public int
getBufferPosition() {
return this.
bufferPosition;
}
}
}