diff --git a/src/main/java/jline/console/completer/CandidateListCompletionHandler.java b/src/main/java/jline/console/completer/CandidateListCompletionHandler.java index e8eb6f3d..8507594b 100644 --- a/src/main/java/jline/console/completer/CandidateListCompletionHandler.java +++ b/src/main/java/jline/console/completer/CandidateListCompletionHandler.java @@ -37,6 +37,13 @@ public class CandidateListCompletionHandler private boolean printSpaceAfterFullCompletion = true; private boolean stripAnsi; + /** + * if true, existing text after cursor matchinga completion to insert + * will not be pushed back behind the completion, but replaced + * by the completion + */ + private boolean consumeMatchingSuffix = false; + public boolean getPrintSpaceAfterFullCompletion() { return printSpaceAfterFullCompletion; } @@ -45,6 +52,14 @@ public void setPrintSpaceAfterFullCompletion(boolean printSpaceAfterFullCompleti this.printSpaceAfterFullCompletion = printSpaceAfterFullCompletion; } + public boolean getConsumeMatchingSuffix() { + return consumeMatchingSuffix; + } + + public void setConsumeMatchingSuffix(boolean consumeMatchingSuffix) { + this.consumeMatchingSuffix = consumeMatchingSuffix; + } + public boolean isStripAnsi() { return stripAnsi; } @@ -63,21 +78,7 @@ public boolean complete(final ConsoleReader reader, final List can // if there is only one completion, then fill in the buffer if (candidates.size() == 1) { String value = Ansi.stripAnsi(candidates.get(0).toString()); - - if (buf.cursor == buf.buffer.length() - && printSpaceAfterFullCompletion - && !value.endsWith(" ")) { - value += " "; - } - - // fail if the only candidate is the same as the current buffer - if (value.equals(buf.toString())) { - return false; - } - - setBuffer(reader, value, pos); - - return true; + return completeSingleCandidate(reader, pos, buf, value); } else if (candidates.size() > 1) { String value = getUnambiguousCompletions(candidates); @@ -92,17 +93,94 @@ else if (candidates.size() > 1) { return true; } - public static void setBuffer(final ConsoleReader reader, final CharSequence value, final int offset) throws + protected boolean completeSingleCandidate(ConsoleReader reader, int pos, CursorBuffer buf, String value) throws IOException { + // no insert if the only candidate is the same as the current buffer + if (buf.length() >= pos + value.length() && + value.equals(buf.toString().substring(pos, pos + value.length()))) { + reader.setCursorPosition(pos + value.length()); + } else { + setBuffer(reader, value, pos); + } + + if (printSpaceAfterFullCompletion + && !value.endsWith(" ")) { + doPrintSpaceAfterFullCOmpletion(reader); + } + + return true; + } + + /** + * This method is called after completing a candidate that + * does not end with a blank, when the option printSpaceAfterFullCompletion is true. + * + * The standard behavior is to insert a blank unless the next char is a blank, + * wherever the cursor is in the buffer, and to move the cursor beyond the + * inserted / existing blank. + * + * @param reader + * @throws IOException + */ + protected void doPrintSpaceAfterFullCOmpletion(ConsoleReader reader) throws IOException { + // at end of buffer or next char is not blank already + if ((reader.getCursorBuffer().cursor >= reader.getCursorBuffer().length() || + reader.getCursorBuffer().buffer.toString().charAt(reader.getCursorBuffer().cursor) != ' ')) { + reader.putString(" "); + } else { + // if blank existed, move beyond it + reader.moveCursor(1); + } + } + + public void setBuffer(final ConsoleReader reader, final CharSequence value, final int offset) throws IOException { + if (getConsumeMatchingSuffix()) { + // consume only if prefix matches + int commonPrefixLength = greatestCommonPrefixLength(value, + reader.getCursorBuffer().buffer.toString().substring(offset)); + if (commonPrefixLength == value.length()) { + // nothing to do other than advancing the cursor + reader.setCursorPosition(offset + value.length()); + return; + } + } + int suffixStart = 0; + // backspace cursor to start of completion while ((reader.getCursorBuffer().cursor > offset) && reader.backspace()) { - // empty + suffixStart++; + } + + if (getConsumeMatchingSuffix()) { + int currentVirtualPos = offset; + String currentBuffer = reader.getCursorBuffer().buffer.toString(); + while ( + suffixStart < value.length() // value still has chars to delete + && currentBuffer.length() > currentVirtualPos // buffer still has chars to delete + // character to delete matches value suffix + && currentBuffer.charAt(currentVirtualPos) == value.charAt(suffixStart) + // do delete + && reader.delete()) { + suffixStart ++; + currentVirtualPos++; + } } reader.putString(value); reader.setCursorPosition(offset + value.length()); } + static int greatestCommonPrefixLength(final CharSequence a, final CharSequence b) { + int minLength = Math.min(a.length(), b.length()); + int i = 0; + for (; i < minLength; i++) { + if (a.charAt(i) != b.charAt(i)) { + break; + } + } + return i; + } + /** * Print out the candidates. If the size of the candidates is greater than the * {@link ConsoleReader#getAutoprintThreshold}, they prompt with a warning. diff --git a/src/main/java/jline/console/completer/CompletionHandler.java b/src/main/java/jline/console/completer/CompletionHandler.java index ef04fbe6..e22aa2ee 100644 --- a/src/main/java/jline/console/completer/CompletionHandler.java +++ b/src/main/java/jline/console/completer/CompletionHandler.java @@ -22,5 +22,13 @@ */ public interface CompletionHandler { + /** + * execute completion, by showing candidates as alternatives, and possibly + * inserting a candidate at position, removing all characters between + * position and current cursol location. + * + * @param position start position in buffer for candidates + * @throws IOException + */ boolean complete(ConsoleReader reader, List candidates, int position) throws IOException; } diff --git a/src/main/java/jline/internal/Configuration.java b/src/main/java/jline/internal/Configuration.java index 1a74b2ce..26846763 100644 --- a/src/main/java/jline/internal/Configuration.java +++ b/src/main/java/jline/internal/Configuration.java @@ -58,7 +58,13 @@ private static Properties initProperties() { private static void loadProperties(final URL url, final Properties props) throws IOException { Log.debug("Loading properties from: ", url); - InputStream input = url.openStream(); + InputStream input; + try { + input = url.openStream(); + } catch (IOException e) { + Log.debug("Could not load properties from " + url + " : " + e.getMessage()); + return; + } try { props.load(new BufferedInputStream(input)); } diff --git a/src/test/java/jline/console/ConsoleReaderTest.java b/src/test/java/jline/console/ConsoleReaderTest.java index 663bc33c..e7f3011a 100644 --- a/src/test/java/jline/console/ConsoleReaderTest.java +++ b/src/test/java/jline/console/ConsoleReaderTest.java @@ -642,7 +642,7 @@ public void testComplete() throws Exception { out.write("read and\033[D\033[D\t\n".getBytes()); - assertEquals("read andnd", console.readLine()); + assertEquals("read and ", console.readLine()); out.close(); } diff --git a/src/test/java/jline/console/completer/CandidateListCompletionHandlerTest.java b/src/test/java/jline/console/completer/CandidateListCompletionHandlerTest.java new file mode 100644 index 00000000..3879d907 --- /dev/null +++ b/src/test/java/jline/console/completer/CandidateListCompletionHandlerTest.java @@ -0,0 +1,237 @@ +package jline.console.completer; + +import jline.console.ConsoleReaderTestSupport; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.fusesource.jansi.Ansi.Attribute.INTENSITY_BOLD; +import static org.fusesource.jansi.Ansi.ansi; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class CandidateListCompletionHandlerTest extends ConsoleReaderTestSupport { + + @Test + public void testGreatestCommonPrefixLength() { + assertEquals(0, CandidateListCompletionHandler.greatestCommonPrefixLength("foo", "bar")); + assertEquals(0, CandidateListCompletionHandler.greatestCommonPrefixLength("foo", "")); + assertEquals(3, CandidateListCompletionHandler.greatestCommonPrefixLength("foo", "foobar")); + assertEquals(3, CandidateListCompletionHandler.greatestCommonPrefixLength("foobar", "foogle")); + } + + @Test + public void testCompleteNoCandidates() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + List candidates = new ArrayList(); + assertTrue(handler.complete(console, candidates, 0)); + + assertEquals("", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidate() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + List candidates = new ArrayList(); + candidates.add("foo"); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foo ", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateInsert() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + List candidates = new ArrayList(); + candidates.add("foobar"); + console.putString("foo"); + console.setCursorPosition(0); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foobar foo", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateInsertMiddle() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + List candidates = new ArrayList(); + candidates.add("foo"); + console.putString("the fis"); + console.setCursorPosition(5); + assertTrue(handler.complete(console, candidates, 4)); + assertEquals("the foo is", console.getCursorBuffer().toString()); + assertEquals(8, console.getCursorBuffer().cursor); + } + + @Test + public void testCompleteOneCandidateInsertMiddleWhitespace() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + List candidates = new ArrayList(); + candidates.add("foo"); + console.putString("the f is"); + console.setCursorPosition(5); + assertTrue(handler.complete(console, candidates, 4)); + assertEquals("the foo is", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateOverwritePartial() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setConsumeMatchingSuffix(true); + List candidates = new ArrayList(); + candidates.add("foobar"); + console.putString("foo"); + console.setCursorPosition(0); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foobar ", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateOverwriteNonMatchingPrefix() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setConsumeMatchingSuffix(true); + List candidates = new ArrayList(); + candidates.add("foobar"); + console.putString("bum"); + console.setCursorPosition(0); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foobar bum", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateOverwritePartialWithin() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setConsumeMatchingSuffix(true); + List candidates = new ArrayList(); + candidates.add("foobar"); + console.putString("foo"); + console.setCursorPosition(2); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foobar ", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateOverwritePartialEnd() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setConsumeMatchingSuffix(true); + List candidates = new ArrayList(); + candidates.add("foobar"); + console.putString("foo"); + console.setCursorPosition(3); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foobar ", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateOverwriteFull() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setConsumeMatchingSuffix(true); + List candidates = new ArrayList(); + candidates.add("foobar"); + console.putString("the foobar"); + console.setCursorPosition(4); + assertTrue(handler.complete(console, candidates, 4)); + assertEquals("the foobar ", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateOverwriteFullMiddle() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setConsumeMatchingSuffix(true); + List candidates = new ArrayList(); + candidates.add("foobar"); + console.putString("the foobar"); + console.setCursorPosition(7); + assertTrue(handler.complete(console, candidates, 4)); + assertEquals("the foobar ", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateOverwriteFullEnd() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setConsumeMatchingSuffix(true); + List candidates = new ArrayList(); + candidates.add("foobar"); + console.putString("the foobar"); + console.setCursorPosition(10); + assertTrue(handler.complete(console, candidates, 4)); + assertEquals("the foobar ", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateOverwriteNonMatchingSuffix() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setConsumeMatchingSuffix(true); + List candidates = new ArrayList(); + candidates.add("foobar"); + console.putString("foobum"); + console.setCursorPosition(0); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foobar um", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidatePrefix() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + List candidates = new ArrayList(); + candidates.add("foo"); + String buffer = "the "; + console.putString(buffer); + console.moveCursor(buffer.length()); + assertTrue(handler.complete(console, candidates, buffer.length())); + assertEquals("the foo ", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateNoWhitespace() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setPrintSpaceAfterFullCompletion(false); + List candidates = new ArrayList(); + candidates.add("foo"); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foo", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteOneCandidateANSI() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + //handler.setStripAnsi(true); + List candidates = new ArrayList(); + candidates.add(ansi().a(INTENSITY_BOLD).a("foo").reset().toString()); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foo ", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteMultiCandidate() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + //handler.setStripAnsi(true); + List candidates = new ArrayList(); + candidates.add("foobar"); + candidates.add("foobuz"); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foob", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteMultiCandidateANSI() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + handler.setStripAnsi(true); + List candidates = new ArrayList(); + candidates.add(ansi().a(INTENSITY_BOLD).a("foobar").reset().toString()); + candidates.add(ansi().a(INTENSITY_BOLD).a("foobuz").reset().toString()); + assertTrue(handler.complete(console, candidates, 0)); + assertEquals("foob", console.getCursorBuffer().toString()); + } + + @Test + public void testCompleteMultiCandidateANSIDisabled() throws Exception { + CandidateListCompletionHandler handler = new CandidateListCompletionHandler(); + List candidates = new ArrayList(); + candidates.add(ansi().a(INTENSITY_BOLD).a("foobar").reset().toString()); + candidates.add(ansi().a(INTENSITY_BOLD).a("foobuz").reset().toString()); + assertTrue(handler.complete(console, candidates, 0)); + assertTrue(console.getCursorBuffer().toString().endsWith("foob")); + assertFalse(console.getCursorBuffer().toString().startsWith("foob")); + } +}