Skip to content

Commit

Permalink
Improve support for completion with quotes, #245
Browse files Browse the repository at this point in the history
  • Loading branch information
gnodet committed Apr 8, 2018
1 parent b84705a commit eefd7a5
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@
*
* @author Eric Bottard
*/
@FunctionalInterface
public interface CompletingParsedLine {
public interface CompletingParsedLine extends ParsedLine {

CharSequence emit(CharSequence candidate);
CharSequence escape(CharSequence candidate, boolean complete);

int rawWordCursor();

int rawWordLength();

}
11 changes: 11 additions & 0 deletions reader/src/main/java/org/jline/reader/LineReaderBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.jline.reader.impl.history.DefaultHistory;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;
import org.jline.utils.Log;

public final class LineReaderBuilder {

Expand Down Expand Up @@ -82,6 +83,16 @@ public LineReaderBuilder highlighter(Highlighter highlighter) {
}

public LineReaderBuilder parser(Parser parser) {
if (parser != null) {
try {
if (!(parser.parse("", 0) instanceof CompletingParsedLine)) {
Log.warn("The Parser of class " + parser.getClass().getName() + " does not support the CompletingParsedLine interface. " +
"Completion with escaped or quoted words won't work correctly.");
}
} catch (Throwable t) {
// Ignore
}
}
this.parser = parser;
return this;
}
Expand Down
60 changes: 51 additions & 9 deletions reader/src/main/java/org/jline/reader/impl/DefaultParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex
int wordCursor = -1;
int wordIndex = -1;
int quoteStart = -1;
int rawWordCursor = -1;
int rawWordLength = -1;
int rawWordStart = 0;

for (int i = 0; (line != null) && (i < line.length()); i++) {
// once we reach the cursor, set the
Expand All @@ -101,6 +104,7 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex
// the position in the current argument is just the
// length of the current argument
wordCursor = current.length();
rawWordCursor = i - rawWordStart;
}

if (quoteStart < 0 && isQuoteChar(line, i)) {
Expand All @@ -113,30 +117,46 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex
words.add(current.toString());
current.setLength(0);
quoteStart = -1;
} else if (!isEscapeChar(line, i)) {
// Take the next character
current.append(line.charAt(i));
if (rawWordCursor >= 0 && rawWordLength < 0) {
rawWordLength = i - rawWordStart + 1;
}
} else {
if (!isEscapeChar(line, i)) {
// Take the next character
current.append(line.charAt(i));
}
}
} else {
// Not in a quote block
if (isDelimiter(line, i)) {
if (current.length() > 0) {
words.add(current.toString());
current.setLength(0); // reset the arg
if (rawWordCursor >= 0 && rawWordLength < 0) {
rawWordLength = i - rawWordStart;
}
}
rawWordStart = i + 1;
} else {
if (!isEscapeChar(line, i)) {
current.append(line.charAt(i));
}
} else if (!isEscapeChar(line, i)) {
current.append(line.charAt(i));
}
}
}

if (current.length() > 0 || cursor == line.length()) {
words.add(current.toString());
if (rawWordCursor >= 0 && rawWordLength < 0) {
rawWordLength = line.length() - rawWordStart;
}
}

if (cursor == line.length()) {
wordIndex = words.size() - 1;
wordCursor = words.get(words.size() - 1).length();
rawWordCursor = cursor - rawWordStart;
rawWordLength = rawWordCursor;
}

if (eofOnEscapedNewLine && isEscapeChar(line, line.length() - 1)) {
Expand All @@ -148,7 +168,7 @@ public ParsedLine parse(final String line, final int cursor, ParseContext contex
}

String openingQuote = quoteStart >= 0 ? line.substring(quoteStart, quoteStart + 1) : null;
return new ArgumentList(line, words, wordIndex, wordCursor, cursor, openingQuote);
return new ArgumentList(line, words, wordIndex, wordCursor, cursor, openingQuote, rawWordCursor, rawWordLength);
}

/**
Expand Down Expand Up @@ -266,13 +286,22 @@ public class ArgumentList implements ParsedLine, CompletingParsedLine

private final String openingQuote;

public ArgumentList(final String line, final List<String> words, final int wordIndex, final int wordCursor, final int cursor, final String openingQuote) {
private final int rawWordCursor;

private final int rawWordLength;

public ArgumentList(final String line, final List<String> words,
final int wordIndex, final int wordCursor,
final int cursor, final String openingQuote,
final int rawWordCursor, final int rawWordLength) {
this.line = line;
this.words = Collections.unmodifiableList(Objects.requireNonNull(words));
this.wordIndex = wordIndex;
this.wordCursor = wordCursor;
this.cursor = cursor;
this.openingQuote = openingQuote;
this.rawWordCursor = rawWordCursor;
this.rawWordLength = rawWordLength;
}

public int wordIndex() {
Expand Down Expand Up @@ -303,7 +332,7 @@ public String line() {
return line;
}

public CharSequence emit(CharSequence candidate) {
public CharSequence escape(CharSequence candidate, boolean complete) {
StringBuilder sb = new StringBuilder(candidate);
Predicate<Integer> needToBeEscaped;
// Completion is protected by an opening quote:
Expand All @@ -323,10 +352,23 @@ public CharSequence emit(CharSequence candidate) {
}
}
if (openingQuote != null) {
sb.append(openingQuote);
sb.insert(0, openingQuote);
if (complete) {
sb.append(openingQuote);
}
}
return sb;
}

@Override
public int rawWordCursor() {
return rawWordCursor;
}

@Override
public int rawWordLength() {
return rawWordLength;
}
}

}
79 changes: 58 additions & 21 deletions reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -3860,9 +3860,9 @@ protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix
}

// Parse the command line
ParsedLine line;
CompletingParsedLine line;
try {
line = parser.parse(buf.toString(), buf.cursor(), ParseContext.COMPLETE);
line = wrap(parser.parse(buf.toString(), buf.cursor(), ParseContext.COMPLETE));
} catch (Exception e) {
Log.info("Error while parsing line", e);
return false;
Expand Down Expand Up @@ -3967,7 +3967,7 @@ protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix
List<Candidate> possible = matching.entrySet().stream()
.flatMap(e -> e.getValue().stream())
.collect(Collectors.toList());
doList(possible, line.word(), false);
doList(possible, line.word(), false, line::escape);
return !possible.isEmpty();
}

Expand All @@ -3988,13 +3988,12 @@ else if (isSet(Option.RECOGNIZE_EXACT)) {
// Complete and exit
if (completion != null && !completion.value().isEmpty()) {
if (prefix) {
buf.backspace(line.wordCursor());
buf.backspace(line.rawWordCursor());
} else {
buf.move(line.word().length() - line.wordCursor());
buf.backspace(line.word().length());
buf.move(line.rawWordLength() - line.rawWordCursor());
buf.backspace(line.rawWordLength());
}
CompletingParsedLine cpl = (line instanceof CompletingParsedLine) ? ((CompletingParsedLine) line) : t -> t;
buf.write(cpl.emit(completion.value()));
buf.write(line.escape(completion.value(), completion.complete()));
if (completion.complete()) {
if (buf.currChar() != ' ') {
buf.write(" ");
Expand Down Expand Up @@ -4028,7 +4027,7 @@ else if (isSet(Option.RECOGNIZE_EXACT)) {
if (useMenu) {
buf.move(line.word().length() - line.wordCursor());
buf.backspace(line.word().length());
doMenu(possible, line.word());
doMenu(possible, line.word(), line::escape);
return true;
}

Expand All @@ -4038,7 +4037,7 @@ else if (isSet(Option.RECOGNIZE_EXACT)) {
current = line.word().substring(0, line.wordCursor());
} else {
current = line.word();
buf.move(current.length() - line.wordCursor());
buf.move(line.rawWordLength() - line.rawWordCursor());
}
// Now, we need to find the unambiguous completion
// TODO: need to find common suffix
Expand All @@ -4049,8 +4048,8 @@ else if (isSet(Option.RECOGNIZE_EXACT)) {
boolean hasUnambiguous = commonPrefix.startsWith(current) && !commonPrefix.equals(current);

if (hasUnambiguous) {
buf.backspace(current.length());
buf.write(commonPrefix);
buf.backspace(line.rawWordLength());
buf.write(line.escape(commonPrefix, false));
current = commonPrefix;
if ((!isSet(Option.AUTO_LIST) && isSet(Option.AUTO_MENU))
|| (isSet(Option.AUTO_LIST) && isSet(Option.LIST_AMBIGUOUS))) {
Expand All @@ -4060,17 +4059,53 @@ else if (isSet(Option.RECOGNIZE_EXACT)) {
}
}
if (isSet(Option.AUTO_LIST)) {
if (!doList(possible, current, true)) {
if (!doList(possible, current, true, line::escape)) {
return true;
}
}
if (isSet(Option.AUTO_MENU)) {
buf.backspace(current.length());
doMenu(possible, line.word());
doMenu(possible, line.word(), line::escape);
}
return true;
}

private CompletingParsedLine wrap(ParsedLine line) {
if (line instanceof CompletingParsedLine) {
return (CompletingParsedLine) line;
} else {
return new CompletingParsedLine() {
public String word() {
return line.word();
}
public int wordCursor() {
return line.wordCursor();
}
public int wordIndex() {
return line.wordIndex();
}
public List<String> words() {
return line.words();
}
public String line() {
return line.line();
}
public int cursor() {
return line.cursor();
}
public CharSequence escape(CharSequence candidate, boolean complete) {
return candidate;
}
public int rawWordCursor() {
return wordCursor();
}
public int rawWordLength() {
return word().length();
}
};
}
}

protected Comparator<Candidate> getCandidateComparator(boolean caseInsensitive, String word) {
String wdi = caseInsensitive ? word.toLowerCase() : word;
ToIntFunction<String> wordDistance = w -> distance(wdi, caseInsensitive ? w.toLowerCase() : w);
Expand Down Expand Up @@ -4166,6 +4201,7 @@ protected boolean nextBindingIsComplete() {

private class MenuSupport implements Supplier<AttributedString> {
final List<Candidate> possible;
final BiFunction<CharSequence, Boolean, CharSequence> escaper;
int selection;
int topLine;
String word;
Expand All @@ -4174,8 +4210,9 @@ private class MenuSupport implements Supplier<AttributedString> {
int columns;
String completed;

public MenuSupport(List<Candidate> original, String completed) {
public MenuSupport(List<Candidate> original, String completed, BiFunction<CharSequence, Boolean, CharSequence> escaper) {
this.possible = new ArrayList<>();
this.escaper = escaper;
this.selection = -1;
this.topLine = 0;
this.word = "";
Expand Down Expand Up @@ -4276,7 +4313,7 @@ public void right() {

private void update() {
buf.backspace(word.length());
word = completion().value();
word = escaper.apply(completion().value(), true).toString();
buf.write(word);

// Compute displayed prompt
Expand Down Expand Up @@ -4324,14 +4361,14 @@ public AttributedString get() {

}

protected boolean doMenu(List<Candidate> original, String completed) {
protected boolean doMenu(List<Candidate> original, String completed, BiFunction<CharSequence, Boolean, CharSequence> escaper) {
// Reorder candidates according to display order
final List<Candidate> possible = new ArrayList<>();
mergeCandidates(original);
computePost(original, null, possible, completed);

// Build menu support
MenuSupport menuSupport = new MenuSupport(original, completed);
MenuSupport menuSupport = new MenuSupport(original, completed, escaper);
post = menuSupport;
redisplay();

Expand Down Expand Up @@ -4394,7 +4431,7 @@ && getLastBinding().charAt(0) != ' '
return false;
}

protected boolean doList(List<Candidate> possible, String completed, boolean runLoop) {
protected boolean doList(List<Candidate> possible, String completed, boolean runLoop, BiFunction<CharSequence, Boolean, CharSequence> escaper) {
// If we list only and if there's a big
// number of items, we should ask the user
// for confirmation, display the list
Expand Down Expand Up @@ -4484,8 +4521,8 @@ protected boolean doList(List<Candidate> possible, String completed, boolean run
post = null;
pushBackBinding();
} else if (isSet(Option.AUTO_MENU)) {
buf.backspace(current.length());
doMenu(cands, current);
buf.backspace(escaper.apply(current, false).length());
doMenu(cands, current, escaper);
}
return false;
} else {
Expand Down
Loading

0 comments on commit eefd7a5

Please sign in to comment.