diff --git a/reader/src/main/java/org/jline/reader/History.java b/reader/src/main/java/org/jline/reader/History.java index 37e0ff6e2..83890472b 100644 --- a/reader/src/main/java/org/jline/reader/History.java +++ b/reader/src/main/java/org/jline/reader/History.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.time.Instant; +import java.util.Iterator; import java.util.ListIterator; /** @@ -82,6 +83,24 @@ default ListIterator iterator() { return iterator(first()); } + default Iterator reverseIterator() { + return reverseIterator(last()); + } + + default Iterator reverseIterator(int index) { + return new Iterator() { + private final ListIterator it = iterator(index + 1); + @Override + public boolean hasNext() { + return it.hasPrevious(); + } + @Override + public Entry next() { + return it.previous(); + } + }; + } + // // Navigation // diff --git a/reader/src/main/java/org/jline/reader/LineReader.java b/reader/src/main/java/org/jline/reader/LineReader.java index 86421672f..db87b1640 100644 --- a/reader/src/main/java/org/jline/reader/LineReader.java +++ b/reader/src/main/java/org/jline/reader/LineReader.java @@ -354,6 +354,7 @@ enum Option { RECOGNIZE_EXACT, /** display group name before each group (else display all group names first) */ GROUP(true), + /** if completion is case insensitive or not */ CASE_INSENSITIVE, LIST_AMBIGUOUS, LIST_PACKED, @@ -391,7 +392,11 @@ enum Option { * (including the prompt) will be erased, thereby leaving the screen as it * was before the readLine call. */ - ERASE_LINE_ON_FINISH; + ERASE_LINE_ON_FINISH, + + /** if history search is fully case insensitive */ + CASE_INSENSITIVE_SEARCH, + ; private final boolean def; diff --git a/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java b/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java index a11a0486b..bc02567ec 100644 --- a/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java +++ b/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java @@ -13,26 +13,14 @@ import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; -import java.io.StringWriter; import java.time.Instant; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Objects; -import java.util.Set; -import java.util.TreeMap; import java.util.function.*; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; @@ -178,6 +166,8 @@ protected enum BellType { protected Buffer historyBuffer = null; protected CharSequence searchBuffer; protected StringBuffer searchTerm = null; + protected boolean searchFailing; + protected boolean searchBackward; protected int searchIndex = -1; @@ -2290,95 +2280,140 @@ protected boolean historyIncrementalSearchBackward() { return doSearchHistory(true); } + static class Pair { + final U u; final V v; + public Pair(U u, V v) { + this.u = u; + this.v = v; + } + public U getU() { + return u; + } + public V getV() { + return v; + } + } + protected boolean doSearchHistory(boolean backward) { if (history.isEmpty()) { return false; } - Buffer originalBuffer = buf.copy(); - String previousSearchTerm = (searchTerm != null) ? searchTerm.toString() : ""; - searchTerm = new StringBuffer(buf.toString()); - if (searchTerm.length() > 0) { - searchIndex = backward - ? searchBackwards(searchTerm.toString(), history.index(), false) - : searchForwards(searchTerm.toString(), history.index(), false); - if (searchIndex == -1) { - beep(); - } - printSearchStatus(searchTerm.toString(), - searchIndex > -1 ? history.get(searchIndex) : "", backward); - } else { - searchIndex = -1; - printSearchStatus("", "", backward); - } - - redisplay(); - KeyMap terminators = new KeyMap<>(); getString(SEARCH_TERMINATORS, DEFAULT_SEARCH_TERMINATORS) .codePoints().forEach(c -> bind(terminators, ACCEPT_LINE, new String(Character.toChars(c)))); + Buffer originalBuffer = buf.copy(); + searchIndex = -1; + searchTerm = new StringBuffer(); + searchBackward = backward; + searchFailing = false; + post = () -> new AttributedString((searchFailing ? "failing" + " " : "") + + (searchBackward ? "bck-i-search" : "fwd-i-search") + + ": " + searchTerm + "_"); + + redisplay(); try { while (true) { - Binding o = readBinding(getKeys(), terminators); - if (new Reference(SEND_BREAK).equals(o)) { - buf.copyFrom(originalBuffer); - return true; - } else if (new Reference(HISTORY_INCREMENTAL_SEARCH_BACKWARD).equals(o)) { - backward = true; - if (searchTerm.length() == 0) { - searchTerm.append(previousSearchTerm); - } - if (searchIndex > 0) { - searchIndex = searchBackwards(searchTerm.toString(), searchIndex, false); - } - } else if (new Reference(HISTORY_INCREMENTAL_SEARCH_FORWARD).equals(o)) { - backward = false; - if (searchTerm.length() == 0) { - searchTerm.append(previousSearchTerm); - } - if (searchIndex > -1 && searchIndex < history.size() - 1) { - searchIndex = searchForwards(searchTerm.toString(), searchIndex, false); - } - } else if (new Reference(BACKWARD_DELETE_CHAR).equals(o)) { - if (searchTerm.length() > 0) { - searchTerm.deleteCharAt(searchTerm.length() - 1); - if (backward) { - searchIndex = searchBackwards(searchTerm.toString(), history.index(), false); - } else { - searchIndex = searchForwards(searchTerm.toString(), history.index(), false); + int prevSearchIndex = searchIndex; + Binding operation = readBinding(getKeys(), terminators); + String ref = (operation instanceof Reference) ? ((Reference) operation).name() : ""; + boolean next = false; + switch (ref) { + case SEND_BREAK: + beep(); + buf.copyFrom(originalBuffer); + return true; + case HISTORY_INCREMENTAL_SEARCH_BACKWARD: + searchBackward = true; + next = true; + break; + case HISTORY_INCREMENTAL_SEARCH_FORWARD: + searchBackward = false; + next = true; + break; + case BACKWARD_DELETE_CHAR: + if (searchTerm.length() > 0) { + searchTerm.deleteCharAt(searchTerm.length() - 1); } - } - } else if (new Reference(SELF_INSERT).equals(o)) { - searchTerm.append(getLastBinding()); - if (backward) { - searchIndex = searchBackwards(searchTerm.toString(), history.index(), false); - } else { - searchIndex = searchForwards(searchTerm.toString(), history.index(), false); - } - } else { - // Set buffer and cursor position to the found string. - if (searchIndex != -1) { - history.moveTo(searchIndex); - } - pushBackBinding(); - return true; + break; + case SELF_INSERT: + searchTerm.append(getLastBinding()); + break; + default: + // Set buffer and cursor position to the found string. + if (searchIndex != -1) { + history.moveTo(searchIndex); + } + pushBackBinding(); + return true; } // print the search status - if (searchTerm.length() == 0) { - printSearchStatus("", "", backward); - searchIndex = -1; + String pattern = doGetSearchPattern(); + if (pattern.length() == 0) { + buf.copyFrom(originalBuffer); + searchFailing = false; } else { - if (searchIndex == -1) { - beep(); - printSearchStatus(searchTerm.toString(), "", backward); + boolean caseInsensitive = isSet(Option.CASE_INSENSITIVE_SEARCH); + Pattern pat = Pattern.compile(pattern, caseInsensitive ? Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE + : Pattern.UNICODE_CASE); + Pair pair = null; + if (searchBackward) { + boolean nextOnly = next; + pair = matches(pat, buf.toString(), searchIndex).stream() + .filter(p -> nextOnly ? p.v < buf.cursor() : p.v <= buf.cursor()) + .max(Comparator.comparing(Pair::getV)) + .orElse(null); + if (pair == null) { + pair = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(history.reverseIterator(searchIndex < 0 ? history.last() : searchIndex - 1), Spliterator.ORDERED), false) + .flatMap(e -> matches(pat, e.line(), e.index()).stream()) + .findFirst() + .orElse(null); + } } else { - printSearchStatus(searchTerm.toString(), history.get(searchIndex), backward); + boolean nextOnly = next; + pair = matches(pat, buf.toString(), searchIndex).stream() + .filter(p -> nextOnly ? p.v > buf.cursor() : p.v >= buf.cursor()) + .min(Comparator.comparing(Pair::getV)) + .orElse(null); + if (pair == null) { + pair = StreamSupport.stream( + Spliterators.spliteratorUnknownSize(history.iterator((searchIndex < 0 ? history.last() : searchIndex) + 1), Spliterator.ORDERED), false) + .flatMap(e -> matches(pat, e.line(), e.index()).stream()) + .findFirst() + .orElse(null); + if (pair == null && searchIndex >= 0) { + pair = matches(pat, originalBuffer.toString(), -1).stream() + .min(Comparator.comparing(Pair::getV)) + .orElse(null); + } + } + } + if (pair != null) { + searchIndex = pair.u; + buf.clear(); + if (searchIndex >= 0) { + buf.write(history.get(searchIndex)); + } else { + buf.write(originalBuffer.toString()); + } + buf.cursor(pair.v); + searchFailing = false; + } else { + searchFailing = true; + beep(); } } redisplay(); } + } catch (IOError e) { + // Ignore Ctrl+C interrupts and just exit the loop + if (!(e.getCause() instanceof InterruptedException)) { + throw e; + } + return true; } finally { searchTerm = null; searchIndex = -1; @@ -2386,6 +2421,40 @@ protected boolean doSearchHistory(boolean backward) { } } + private List> matches(Pattern p, String line, int index) { + List> starts = new ArrayList<>(); + Matcher m = p.matcher(line); + while (m.find()) { + starts.add(new Pair<>(index, m.start())); + } + return starts; + } + + private String doGetSearchPattern() { + StringBuilder sb = new StringBuilder(); + boolean inQuote = false; + for (int i = 0; i < searchTerm.length(); i++) { + char c = searchTerm.charAt(i); + if (Character.isLowerCase(c)) { + if (inQuote) { + sb.append("\\E"); + inQuote = false; + } + sb.append("[").append(Character.toLowerCase(c)).append(Character.toUpperCase(c)).append("]"); + } else { + if (!inQuote) { + sb.append("\\Q"); + inQuote = true; + } + sb.append(c); + } + } + if (inQuote) { + sb.append("\\E"); + } + return sb.toString(); + } + private void pushBackBinding() { pushBackBinding(false); } @@ -2492,17 +2561,17 @@ public int searchBackwards(String searchTerm) { return searchBackwards(searchTerm, history.index(), false); } - public int searchBackwards(String searchTerm, int startIndex, boolean startsWith) { ListIterator it = history.iterator(startIndex); while (it.hasPrevious()) { History.Entry e = it.previous(); + String line = e.line(); if (startsWith) { - if (e.line().startsWith(searchTerm)) { + if (line.startsWith(searchTerm)) { return e.index(); } } else { - if (e.line().contains(searchTerm)) { + if (line.contains(searchTerm)) { return e.index(); } } @@ -2553,13 +2622,6 @@ public int searchForwards(String searchTerm) { return searchForwards(searchTerm, history.index()); } - public void printSearchStatus(String searchTerm, String match, boolean backward) { - String searchLabel = backward ? "bck-i-search" : "i-search"; - post = () -> new AttributedString(searchLabel + ": " + searchTerm + "_"); - setBuffer(match); - buf.move(match.indexOf(searchTerm) - buf.cursor()); - } - protected boolean quit() { getBuffer().clear(); return acceptLine(); diff --git a/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java b/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java index fb0ffcc33..6cea4644b 100644 --- a/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java +++ b/reader/src/main/java/org/jline/reader/impl/history/DefaultHistory.java @@ -296,6 +296,11 @@ public ListIterator iterator(int index) { return items.listIterator(index - offset); } + @Override + public Spliterator spliterator() { + return items.spliterator(); + } + static class EntryImpl implements Entry { private final int index; diff --git a/reader/src/test/java/org/jline/reader/impl/HistorySearchTest.java b/reader/src/test/java/org/jline/reader/impl/HistorySearchTest.java index 66a3608ca..dd6a25581 100644 --- a/reader/src/test/java/org/jline/reader/impl/HistorySearchTest.java +++ b/reader/src/test/java/org/jline/reader/impl/HistorySearchTest.java @@ -23,6 +23,17 @@ private DefaultHistory setupHistory() { return history; } + @Test + public void testCaseInsensitive() throws Exception { + setupHistory(); + reader.setOpt(LineReader.Option.CASE_INSENSITIVE_SEARCH); + try { + assertLine("fiddle", new TestBuffer().ctrl('R').append("I").enter(), false); + } finally { + reader.unsetOpt(LineReader.Option.CASE_INSENSITIVE_SEARCH); + } + } + @Test public void testReverseHistorySearch() throws Exception { DefaultHistory history = setupHistory(); @@ -84,8 +95,8 @@ public void testSearchHistoryWithNoMatches() throws Exception { String readLineResult; in.setIn(new ByteArrayInputStream(translate("x^S^S\n").getBytes())); readLineResult = reader.readLine(); - assertEquals("", readLineResult); - assertEquals(3, history.size()); + assertEquals("x", readLineResult); + assertEquals(4, history.size()); } @Test @@ -96,8 +107,6 @@ public void testAbortingSearchRetainsCurrentBufferAndPrintsDetails() throws Exce in.setIn(new ByteArrayInputStream(translate("f^Rf^G").getBytes())); readLineResult = reader.readLine(); assertEquals(null, readLineResult); - assertTrue(out.toString().contains("bck-i-search: f_")); - assertFalse(out.toString().contains("bck-i-search: ff_")); assertEquals("f", reader.getBuffer().toString()); assertEquals(3, history.size()); } @@ -109,12 +118,13 @@ public void testAbortingAfterSearchingPreviousLinesGivesBlank() throws Exception String readLineResult; in.setIn(new ByteArrayInputStream(translate("f^Rf\nfoo^G").getBytes())); readLineResult = reader.readLine(); - assertEquals("", readLineResult); + assertEquals("f", readLineResult); + assertEquals(4, history.size()); readLineResult = reader.readLine(); assertEquals(null, readLineResult); assertEquals("", reader.getBuffer().toString()); - assertEquals(3, history.size()); + assertEquals(4, history.size()); } @Test diff --git a/reader/src/test/java/org/jline/reader/impl/ReaderTestSupport.java b/reader/src/test/java/org/jline/reader/impl/ReaderTestSupport.java index 118379b35..104a7acbe 100644 --- a/reader/src/test/java/org/jline/reader/impl/ReaderTestSupport.java +++ b/reader/src/test/java/org/jline/reader/impl/ReaderTestSupport.java @@ -139,7 +139,6 @@ protected void assertLine(final String expected, final TestBuffer buffer, String line; String prevLine = null; while ((line = reader.readLine(null, null, mask, null)) != null) { - prevLine = line; }