1/* 2 * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26package jdk.internal.jline.extra; 27 28import java.io.IOException; 29import java.lang.reflect.Method; 30import java.util.ArrayList; 31import java.util.Collection; 32import java.util.Iterator; 33import java.util.List; 34import java.util.ListIterator; 35import java.util.function.Supplier; 36 37import jdk.internal.jline.console.ConsoleReader; 38import jdk.internal.jline.console.KeyMap; 39import jdk.internal.jline.console.history.History; 40import jdk.internal.jline.console.history.History.Entry; 41import jdk.internal.jline.console.history.MemoryHistory; 42 43/*Public for tests (HistoryTest). 44 */ 45public abstract class EditingHistory implements History { 46 47 private final History fullHistory; 48 private History currentDelegate; 49 50 protected EditingHistory(ConsoleReader in, Iterable<? extends String> originalHistory) { 51 MemoryHistory fullHistory = new MemoryHistory(); 52 fullHistory.setIgnoreDuplicates(false); 53 this.fullHistory = fullHistory; 54 this.currentDelegate = fullHistory; 55 bind(in, CTRL_UP, 56 (Runnable) () -> moveHistoryToSnippet(in, ((EditingHistory) in.getHistory())::previousSnippet)); 57 bind(in, CTRL_DOWN, 58 (Runnable) () -> moveHistoryToSnippet(in, ((EditingHistory) in.getHistory())::nextSnippet)); 59 if (originalHistory != null) { 60 load(originalHistory); 61 } 62 } 63 64 private void moveHistoryToSnippet(ConsoleReader in, Supplier<Boolean> action) { 65 if (!action.get()) { 66 try { 67 in.beep(); 68 } catch (IOException ex) { 69 throw new IllegalStateException(ex); 70 } 71 } else { 72 try { 73 //could use: 74 //in.resetPromptLine(in.getPrompt(), in.getHistory().current().toString(), -1); 75 //but that would mean more re-writing on the screen, (and prints an additional 76 //empty line), so using setBuffer directly: 77 Method setBuffer = ConsoleReader.class.getDeclaredMethod("setBuffer", String.class); 78 79 setBuffer.setAccessible(true); 80 setBuffer.invoke(in, in.getHistory().current().toString()); 81 in.flush(); 82 } catch (ReflectiveOperationException | IOException ex) { 83 throw new IllegalStateException(ex); 84 } 85 } 86 } 87 88 private void bind(ConsoleReader in, String shortcut, Object action) { 89 KeyMap km = in.getKeys(); 90 for (int i = 0; i < shortcut.length(); i++) { 91 Object value = km.getBound(Character.toString(shortcut.charAt(i))); 92 if (value instanceof KeyMap) { 93 km = (KeyMap) value; 94 } else { 95 km.bind(shortcut.substring(i), action); 96 } 97 } 98 } 99 100 private static final String CTRL_UP = "\033\133\061\073\065\101"; //Ctrl-UP 101 private static final String CTRL_DOWN = "\033\133\061\073\065\102"; //Ctrl-DOWN 102 103 @Override 104 public int size() { 105 return currentDelegate.size(); 106 } 107 108 @Override 109 public boolean isEmpty() { 110 return currentDelegate.isEmpty(); 111 } 112 113 @Override 114 public int index() { 115 return currentDelegate.index(); 116 } 117 118 @Override 119 public void clear() { 120 if (currentDelegate != fullHistory) 121 throw new IllegalStateException("narrowed"); 122 currentDelegate.clear(); 123 } 124 125 @Override 126 public CharSequence get(int index) { 127 return currentDelegate.get(index); 128 } 129 130 @Override 131 public void add(CharSequence line) { 132 NarrowingHistoryLine currentLine = null; 133 int origIndex = fullHistory.index(); 134 int fullSize; 135 try { 136 fullHistory.moveToEnd(); 137 fullSize = fullHistory.index(); 138 if (currentDelegate == fullHistory) { 139 if (origIndex < fullHistory.index()) { 140 for (Entry entry : fullHistory) { 141 if (!(entry.value() instanceof NarrowingHistoryLine)) 142 continue; 143 int[] cluster = ((NarrowingHistoryLine) entry.value()).span; 144 if (cluster[0] == origIndex && cluster[1] > cluster[0]) { 145 currentDelegate = new MemoryHistory(); 146 for (int i = cluster[0]; i <= cluster[1]; i++) { 147 currentDelegate.add(fullHistory.get(i)); 148 } 149 } 150 } 151 } 152 } 153 fullHistory.moveToEnd(); 154 while (fullHistory.previous()) { 155 CharSequence c = fullHistory.current(); 156 if (c instanceof NarrowingHistoryLine) { 157 currentLine = (NarrowingHistoryLine) c; 158 break; 159 } 160 } 161 } finally { 162 fullHistory.moveTo(origIndex); 163 } 164 if (currentLine == null || currentLine.span[1] != (-1)) { 165 line = currentLine = new NarrowingHistoryLine(line, fullSize); 166 } 167 StringBuilder complete = new StringBuilder(); 168 for (int i = currentLine.span[0]; i < fullSize; i++) { 169 complete.append(fullHistory.get(i)); 170 } 171 complete.append(line); 172 if (isComplete(complete)) { 173 currentLine.span[1] = fullSize; //TODO: +1? 174 currentDelegate = fullHistory; 175 } 176 fullHistory.add(line); 177 } 178 179 protected abstract boolean isComplete(CharSequence input); 180 181 @Override 182 public void set(int index, CharSequence item) { 183 if (currentDelegate != fullHistory) 184 throw new IllegalStateException("narrowed"); 185 currentDelegate.set(index, item); 186 } 187 188 @Override 189 public CharSequence remove(int i) { 190 if (currentDelegate != fullHistory) 191 throw new IllegalStateException("narrowed"); 192 return currentDelegate.remove(i); 193 } 194 195 @Override 196 public CharSequence removeFirst() { 197 if (currentDelegate != fullHistory) 198 throw new IllegalStateException("narrowed"); 199 return currentDelegate.removeFirst(); 200 } 201 202 @Override 203 public CharSequence removeLast() { 204 if (currentDelegate != fullHistory) 205 throw new IllegalStateException("narrowed"); 206 return currentDelegate.removeLast(); 207 } 208 209 @Override 210 public void replace(CharSequence item) { 211 if (currentDelegate != fullHistory) 212 throw new IllegalStateException("narrowed"); 213 currentDelegate.replace(item); 214 } 215 216 @Override 217 public ListIterator<Entry> entries(int index) { 218 return currentDelegate.entries(index); 219 } 220 221 @Override 222 public ListIterator<Entry> entries() { 223 return currentDelegate.entries(); 224 } 225 226 @Override 227 public Iterator<Entry> iterator() { 228 return currentDelegate.iterator(); 229 } 230 231 @Override 232 public CharSequence current() { 233 return currentDelegate.current(); 234 } 235 236 @Override 237 public boolean previous() { 238 return currentDelegate.previous(); 239 } 240 241 @Override 242 public boolean next() { 243 return currentDelegate.next(); 244 } 245 246 @Override 247 public boolean moveToFirst() { 248 return currentDelegate.moveToFirst(); 249 } 250 251 @Override 252 public boolean moveToLast() { 253 return currentDelegate.moveToLast(); 254 } 255 256 @Override 257 public boolean moveTo(int index) { 258 return currentDelegate.moveTo(index); 259 } 260 261 @Override 262 public void moveToEnd() { 263 currentDelegate.moveToEnd(); 264 } 265 266 public boolean previousSnippet() { 267 for (int i = index() - 1; i >= 0; i--) { 268 if (get(i) instanceof NarrowingHistoryLine) { 269 moveTo(i); 270 return true; 271 } 272 } 273 274 return false; 275 } 276 277 public boolean nextSnippet() { 278 for (int i = index() + 1; i < size(); i++) { 279 if (get(i) instanceof NarrowingHistoryLine) { 280 moveTo(i); 281 return true; 282 } 283 } 284 285 if (index() < size()) { 286 moveToEnd(); 287 return true; 288 } 289 290 return false; 291 } 292 293 public final void load(Iterable<? extends String> originalHistory) { 294 NarrowingHistoryLine currentHistoryLine = null; 295 boolean start = true; 296 int currentLine = 0; 297 for (String historyItem : originalHistory) { 298 StringBuilder line = new StringBuilder(historyItem); 299 int trailingBackSlashes = countTrailintBackslashes(line); 300 boolean continuation = trailingBackSlashes % 2 != 0; 301 line.delete(line.length() - trailingBackSlashes / 2 - (continuation ? 1 : 0), line.length()); 302 if (start) { 303 class PersistentNarrowingHistoryLine extends NarrowingHistoryLine implements PersistentEntryMarker { 304 public PersistentNarrowingHistoryLine(CharSequence delegate, int start) { 305 super(delegate, start); 306 } 307 } 308 fullHistory.add(currentHistoryLine = new PersistentNarrowingHistoryLine(line, currentLine)); 309 } else { 310 class PersistentLine implements CharSequence, PersistentEntryMarker { 311 private final CharSequence delegate; 312 public PersistentLine(CharSequence delegate) { 313 this.delegate = delegate; 314 } 315 @Override public int length() { 316 return delegate.length(); 317 } 318 @Override public char charAt(int index) { 319 return delegate.charAt(index); 320 } 321 @Override public CharSequence subSequence(int start, int end) { 322 return delegate.subSequence(start, end); 323 } 324 @Override public String toString() { 325 return delegate.toString(); 326 } 327 } 328 fullHistory.add(new PersistentLine(line)); 329 } 330 start = !continuation; 331 currentHistoryLine.span[1] = currentLine; 332 currentLine++; 333 } 334 } 335 336 public Collection<? extends String> save() { 337 Collection<String> result = new ArrayList<>(); 338 Iterator<Entry> entries = fullHistory.iterator(); 339 340 if (entries.hasNext()) { 341 Entry entry = entries.next(); 342 while (entry != null) { 343 StringBuilder historyLine = new StringBuilder(entry.value()); 344 int trailingBackSlashes = countTrailintBackslashes(historyLine); 345 for (int i = 0; i < trailingBackSlashes; i++) { 346 historyLine.append("\\"); 347 } 348 entry = entries.hasNext() ? entries.next() : null; 349 if (entry != null && !(entry.value() instanceof NarrowingHistoryLine)) { 350 historyLine.append("\\"); 351 } 352 result.add(historyLine.toString()); 353 } 354 } 355 356 return result; 357 } 358 359 private int countTrailintBackslashes(CharSequence text) { 360 int count = 0; 361 362 for (int i = text.length() - 1; i >= 0; i--) { 363 if (text.charAt(i) == '\\') { 364 count++; 365 } else { 366 break; 367 } 368 } 369 370 return count; 371 } 372 373 public List<String> currentSessionEntries() { 374 List<String> result = new ArrayList<>(); 375 376 for (Entry e : fullHistory) { 377 if (!(e.value() instanceof PersistentEntryMarker)) { 378 result.add(e.value().toString()); 379 } 380 } 381 382 return result; 383 } 384 385 public void fullHistoryReplace(String source) { 386 fullHistory.replace(source); 387 } 388 389 private class NarrowingHistoryLine implements CharSequence { 390 private final CharSequence delegate; 391 private final int[] span; 392 393 public NarrowingHistoryLine(CharSequence delegate, int start) { 394 this.delegate = delegate; 395 this.span = new int[] {start, -1}; 396 } 397 398 @Override 399 public int length() { 400 return delegate.length(); 401 } 402 403 @Override 404 public char charAt(int index) { 405 return delegate.charAt(index); 406 } 407 408 @Override 409 public CharSequence subSequence(int start, int end) { 410 return delegate.subSequence(start, end); 411 } 412 413 @Override 414 public String toString() { 415 return delegate.toString(); 416 } 417 418 } 419 420 private interface PersistentEntryMarker {} 421} 422 423