/* * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package jdk.internal.jline.extra; import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.function.Supplier; import jdk.internal.jline.console.ConsoleReader; import jdk.internal.jline.console.KeyMap; import jdk.internal.jline.console.history.History; import jdk.internal.jline.console.history.History.Entry; import jdk.internal.jline.console.history.MemoryHistory; /*Public for tests (HistoryTest). */ public abstract class EditingHistory implements History { private final History fullHistory; private History currentDelegate; protected EditingHistory(ConsoleReader in, Iterable<? extends String> originalHistory) { MemoryHistory fullHistory = new MemoryHistory(); fullHistory.setIgnoreDuplicates(false); this.fullHistory = fullHistory; this.currentDelegate = fullHistory; bind(in, CTRL_UP, (Runnable) () -> moveHistoryToSnippet(in, ((EditingHistory) in.getHistory())::previousSnippet)); bind(in, CTRL_DOWN, (Runnable) () -> moveHistoryToSnippet(in, ((EditingHistory) in.getHistory())::nextSnippet)); if (originalHistory != null) { load(originalHistory); } } private void moveHistoryToSnippet(ConsoleReader in, Supplier<Boolean> action) { if (!action.get()) { try { in.beep(); } catch (IOException ex) { throw new IllegalStateException(ex); } } else { try { //could use: //in.resetPromptLine(in.getPrompt(), in.getHistory().current().toString(), -1); //but that would mean more re-writing on the screen, (and prints an additional //empty line), so using setBuffer directly: Method setBuffer = in.getClass().getDeclaredMethod("setBuffer", String.class); setBuffer.setAccessible(true); setBuffer.invoke(in, in.getHistory().current().toString()); in.flush(); } catch (ReflectiveOperationException | IOException ex) { throw new IllegalStateException(ex); } } } private void bind(ConsoleReader in, String shortcut, Object action) { KeyMap km = in.getKeys(); for (int i = 0; i < shortcut.length(); i++) { Object value = km.getBound(Character.toString(shortcut.charAt(i))); if (value instanceof KeyMap) { km = (KeyMap) value; } else { km.bind(shortcut.substring(i), action); } } } private static final String CTRL_UP = "\033\133\061\073\065\101"; //Ctrl-UP private static final String CTRL_DOWN = "\033\133\061\073\065\102"; //Ctrl-DOWN @Override public int size() { return currentDelegate.size(); } @Override public boolean isEmpty() { return currentDelegate.isEmpty(); } @Override public int index() { return currentDelegate.index(); } @Override public void clear() { if (currentDelegate != fullHistory) throw new IllegalStateException("narrowed"); currentDelegate.clear(); } @Override public CharSequence get(int index) { return currentDelegate.get(index); } @Override public void add(CharSequence line) { NarrowingHistoryLine currentLine = null; int origIndex = fullHistory.index(); int fullSize; try { fullHistory.moveToEnd(); fullSize = fullHistory.index(); if (currentDelegate == fullHistory) { if (origIndex < fullHistory.index()) { for (Entry entry : fullHistory) { if (!(entry.value() instanceof NarrowingHistoryLine)) continue; int[] cluster = ((NarrowingHistoryLine) entry.value()).span; if (cluster[0] == origIndex && cluster[1] > cluster[0]) { currentDelegate = new MemoryHistory(); for (int i = cluster[0]; i <= cluster[1]; i++) { currentDelegate.add(fullHistory.get(i)); } } } } } fullHistory.moveToEnd(); while (fullHistory.previous()) { CharSequence c = fullHistory.current(); if (c instanceof NarrowingHistoryLine) { currentLine = (NarrowingHistoryLine) c; break; } } } finally { fullHistory.moveTo(origIndex); } if (currentLine == null || currentLine.span[1] != (-1)) { line = currentLine = new NarrowingHistoryLine(line, fullSize); } StringBuilder complete = new StringBuilder(); for (int i = currentLine.span[0]; i < fullSize; i++) { complete.append(fullHistory.get(i)); } complete.append(line); if (isComplete(complete)) { currentLine.span[1] = fullSize; //TODO: +1? currentDelegate = fullHistory; } fullHistory.add(line); } protected abstract boolean isComplete(CharSequence input); @Override public void set(int index, CharSequence item) { if (currentDelegate != fullHistory) throw new IllegalStateException("narrowed"); currentDelegate.set(index, item); } @Override public CharSequence remove(int i) { if (currentDelegate != fullHistory) throw new IllegalStateException("narrowed"); return currentDelegate.remove(i); } @Override public CharSequence removeFirst() { if (currentDelegate != fullHistory) throw new IllegalStateException("narrowed"); return currentDelegate.removeFirst(); } @Override public CharSequence removeLast() { if (currentDelegate != fullHistory) throw new IllegalStateException("narrowed"); return currentDelegate.removeLast(); } @Override public void replace(CharSequence item) { if (currentDelegate != fullHistory) throw new IllegalStateException("narrowed"); currentDelegate.replace(item); } @Override public ListIterator<Entry> entries(int index) { return currentDelegate.entries(index); } @Override public ListIterator<Entry> entries() { return currentDelegate.entries(); } @Override public Iterator<Entry> iterator() { return currentDelegate.iterator(); } @Override public CharSequence current() { return currentDelegate.current(); } @Override public boolean previous() { return currentDelegate.previous(); } @Override public boolean next() { return currentDelegate.next(); } @Override public boolean moveToFirst() { return currentDelegate.moveToFirst(); } @Override public boolean moveToLast() { return currentDelegate.moveToLast(); } @Override public boolean moveTo(int index) { return currentDelegate.moveTo(index); } @Override public void moveToEnd() { currentDelegate.moveToEnd(); } public boolean previousSnippet() { for (int i = index() - 1; i >= 0; i--) { if (get(i) instanceof NarrowingHistoryLine) { moveTo(i); return true; } } return false; } public boolean nextSnippet() { for (int i = index() + 1; i < size(); i++) { if (get(i) instanceof NarrowingHistoryLine) { moveTo(i); return true; } } if (index() < size()) { moveToEnd(); return true; } return false; } public final void load(Iterable<? extends String> originalHistory) { NarrowingHistoryLine currentHistoryLine = null; boolean start = true; int currentLine = 0; for (String historyItem : originalHistory) { StringBuilder line = new StringBuilder(historyItem); int trailingBackSlashes = countTrailintBackslashes(line); boolean continuation = trailingBackSlashes % 2 != 0; line.delete(line.length() - trailingBackSlashes / 2 - (continuation ? 1 : 0), line.length()); if (start) { class PersistentNarrowingHistoryLine extends NarrowingHistoryLine implements PersistentEntryMarker { public PersistentNarrowingHistoryLine(CharSequence delegate, int start) { super(delegate, start); } } fullHistory.add(currentHistoryLine = new PersistentNarrowingHistoryLine(line, currentLine)); } else { class PersistentLine implements CharSequence, PersistentEntryMarker { private final CharSequence delegate; public PersistentLine(CharSequence delegate) { this.delegate = delegate; } @Override public int length() { return delegate.length(); } @Override public char charAt(int index) { return delegate.charAt(index); } @Override public CharSequence subSequence(int start, int end) { return delegate.subSequence(start, end); } @Override public String toString() { return delegate.toString(); } } fullHistory.add(new PersistentLine(line)); } start = !continuation; currentHistoryLine.span[1] = currentLine; currentLine++; } } public Collection<? extends String> save() { Collection<String> result = new ArrayList<>(); Iterator<Entry> entries = fullHistory.iterator(); if (entries.hasNext()) { Entry entry = entries.next(); while (entry != null) { StringBuilder historyLine = new StringBuilder(entry.value()); int trailingBackSlashes = countTrailintBackslashes(historyLine); for (int i = 0; i < trailingBackSlashes; i++) { historyLine.append("\\"); } entry = entries.hasNext() ? entries.next() : null; if (entry != null && !(entry.value() instanceof NarrowingHistoryLine)) { historyLine.append("\\"); } result.add(historyLine.toString()); } } return result; } private int countTrailintBackslashes(CharSequence text) { int count = 0; for (int i = text.length() - 1; i >= 0; i--) { if (text.charAt(i) == '\\') { count++; } else { break; } } return count; } public List<String> currentSessionEntries() { List<String> result = new ArrayList<>(); for (Entry e : fullHistory) { if (!(e.value() instanceof PersistentEntryMarker)) { result.add(e.value().toString()); } } return result; } public void fullHistoryReplace(String source) { fullHistory.replace(source); } private class NarrowingHistoryLine implements CharSequence { private final CharSequence delegate; private final int[] span; public NarrowingHistoryLine(CharSequence delegate, int start) { this.delegate = delegate; this.span = new int[] {start, -1}; } @Override public int length() { return delegate.length(); } @Override public char charAt(int index) { return delegate.charAt(index); } @Override public CharSequence subSequence(int start, int end) { return delegate.subSequence(start, end); } @Override public String toString() { return delegate.toString(); } } private interface PersistentEntryMarker {} }