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