ConsoleIOContext.java revision 3062:15bdc18525ff
1/*
2 * Copyright (c) 2015, 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.jshell.tool;
27
28import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
29import jdk.jshell.SourceCodeAnalysis.Suggestion;
30
31import java.awt.event.ActionListener;
32import java.io.IOException;
33import java.io.InputStream;
34import java.io.PrintStream;
35import java.io.UncheckedIOException;
36import java.lang.reflect.Method;
37import java.util.List;
38import java.util.Locale;
39import java.util.Objects;
40import java.util.Optional;
41import java.util.function.Supplier;
42
43import jdk.internal.jline.NoInterruptUnixTerminal;
44import jdk.internal.jline.Terminal;
45import jdk.internal.jline.TerminalFactory;
46import jdk.internal.jline.WindowsTerminal;
47import jdk.internal.jline.console.ConsoleReader;
48import jdk.internal.jline.console.KeyMap;
49import jdk.internal.jline.console.UserInterruptException;
50import jdk.internal.jline.console.completer.Completer;
51import jdk.internal.jshell.tool.StopDetectingInputStream.State;
52
53class ConsoleIOContext extends IOContext {
54
55    final JShellTool repl;
56    final StopDetectingInputStream input;
57    final ConsoleReader in;
58    final EditingHistory history;
59
60    String prefix = "";
61
62    ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout) throws Exception {
63        this.repl = repl;
64        this.input = new StopDetectingInputStream(() -> repl.state.stop(), ex -> repl.hard("Error on input: %s", ex));
65        Terminal term;
66        if (System.getProperty("os.name").toLowerCase(Locale.US).contains(TerminalFactory.WINDOWS)) {
67            term = new JShellWindowsTerminal(input);
68        } else {
69            term = new JShellUnixTerminal(input);
70        }
71        term.init();
72        in = new ConsoleReader(cmdin, cmdout, term);
73        in.setExpandEvents(false);
74        in.setHandleUserInterrupt(true);
75        in.setHistory(history = new EditingHistory(JShellTool.PREFS) {
76            @Override protected CompletionInfo analyzeCompletion(String input) {
77                return repl.analysis.analyzeCompletion(input);
78            }
79        });
80        in.setBellEnabled(true);
81        in.addCompleter(new Completer() {
82            private String lastTest;
83            private int lastCursor;
84            private boolean allowSmart = false;
85            @Override public int complete(String test, int cursor, List<CharSequence> result) {
86                int[] anchor = new int[] {-1};
87                List<Suggestion> suggestions;
88                if (prefix.isEmpty() && test.trim().startsWith("/")) {
89                    suggestions = repl.commandCompletionSuggestions(test, cursor, anchor);
90                } else {
91                    int prefixLength = prefix.length();
92                    suggestions = repl.analysis.completionSuggestions(prefix + test, cursor + prefixLength, anchor);
93                    anchor[0] -= prefixLength;
94                }
95                if (!Objects.equals(lastTest, test) || lastCursor != cursor)
96                    allowSmart = true;
97
98                boolean smart = allowSmart &&
99                                suggestions.stream()
100                                           .anyMatch(s -> s.isSmart);
101
102                lastTest = test;
103                lastCursor = cursor;
104                allowSmart = !allowSmart;
105
106                suggestions.stream()
107                           .filter(s -> !smart || s.isSmart)
108                           .map(s -> s.continuation)
109                           .forEach(result::add);
110
111                boolean onlySmart = suggestions.stream()
112                                               .allMatch(s -> s.isSmart);
113
114                if (smart && !onlySmart) {
115                    Optional<String> prefix =
116                            suggestions.stream()
117                                       .map(s -> s.continuation)
118                                       .reduce(ConsoleIOContext::commonPrefix);
119
120                    String prefixStr = prefix.orElse("").substring(cursor - anchor[0]);
121                    try {
122                        in.putString(prefixStr);
123                        cursor += prefixStr.length();
124                    } catch (IOException ex) {
125                        throw new IllegalStateException(ex);
126                    }
127                    result.add("<press tab to see more>");
128                    return cursor; //anchor should not be used.
129                }
130
131                if (result.isEmpty()) {
132                    try {
133                        //provide "empty completion" feedback
134                        //XXX: this only works correctly when there is only one Completer:
135                        in.beep();
136                    } catch (IOException ex) {
137                        throw new UncheckedIOException(ex);
138                    }
139                }
140
141                return anchor[0];
142            }
143        });
144        bind(DOCUMENTATION_SHORTCUT, (ActionListener) evt -> documentation(repl));
145        bind(CTRL_UP, (ActionListener) evt -> moveHistoryToSnippet(((EditingHistory) in.getHistory())::previousSnippet));
146        bind(CTRL_DOWN, (ActionListener) evt -> moveHistoryToSnippet(((EditingHistory) in.getHistory())::nextSnippet));
147    }
148
149    @Override
150    public String readLine(String prompt, String prefix) throws IOException, InputInterruptedException {
151        this.prefix = prefix;
152        try {
153            return in.readLine(prompt);
154        } catch (UserInterruptException ex) {
155            throw (InputInterruptedException) new InputInterruptedException().initCause(ex);
156        }
157    }
158
159    @Override
160    public boolean interactiveOutput() {
161        return true;
162    }
163
164    @Override
165    public Iterable<String> currentSessionHistory() {
166        return history.currentSessionEntries();
167    }
168
169    @Override
170    public void close() throws IOException {
171        history.save();
172        in.shutdown();
173        try {
174            in.getTerminal().restore();
175        } catch (Exception ex) {
176            throw new IOException(ex);
177        }
178    }
179
180    private void moveHistoryToSnippet(Supplier<Boolean> action) {
181        if (!action.get()) {
182            try {
183                in.beep();
184            } catch (IOException ex) {
185                throw new IllegalStateException(ex);
186            }
187        } else {
188            try {
189                //could use:
190                //in.resetPromptLine(in.getPrompt(), in.getHistory().current().toString(), -1);
191                //but that would mean more re-writing on the screen, (and prints an additional
192                //empty line), so using setBuffer directly:
193                Method setBuffer = in.getClass().getDeclaredMethod("setBuffer", String.class);
194
195                setBuffer.setAccessible(true);
196                setBuffer.invoke(in, in.getHistory().current().toString());
197                in.flush();
198            } catch (ReflectiveOperationException | IOException ex) {
199                throw new IllegalStateException(ex);
200            }
201        }
202    }
203
204    private void bind(String shortcut, Object action) {
205        KeyMap km = in.getKeys();
206        for (int i = 0; i < shortcut.length(); i++) {
207            Object value = km.getBound(Character.toString(shortcut.charAt(i)));
208            if (value instanceof KeyMap) {
209                km = (KeyMap) value;
210            } else {
211                km.bind(shortcut.substring(i), action);
212            }
213        }
214    }
215
216    private static final String DOCUMENTATION_SHORTCUT = "\033\133\132"; //Shift-TAB
217    private static final String CTRL_UP = "\033\133\061\073\065\101"; //Ctrl-UP
218    private static final String CTRL_DOWN = "\033\133\061\073\065\102"; //Ctrl-DOWN
219
220    private void documentation(JShellTool repl) {
221        String buffer = in.getCursorBuffer().buffer.toString();
222        int cursor = in.getCursorBuffer().cursor;
223        String doc;
224        if (prefix.isEmpty() && buffer.trim().startsWith("/")) {
225            doc = repl.commandDocumentation(buffer, cursor);
226        } else {
227            doc = repl.analysis.documentation(prefix + buffer, cursor + prefix.length());
228        }
229
230        try {
231            if (doc != null) {
232                in.println();
233                in.println(doc);
234                in.redrawLine();
235                in.flush();
236            } else {
237                in.beep();
238            }
239        } catch (IOException ex) {
240            throw new IllegalStateException(ex);
241        }
242    }
243
244    private static String commonPrefix(String str1, String str2) {
245        for (int i = 0; i < str2.length(); i++) {
246            if (!str1.startsWith(str2.substring(0, i + 1))) {
247                return str2.substring(0, i);
248            }
249        }
250
251        return str2;
252    }
253
254    @Override
255    public boolean terminalEditorRunning() {
256        Terminal terminal = in.getTerminal();
257        if (terminal instanceof JShellUnixTerminal)
258            return ((JShellUnixTerminal) terminal).isRaw();
259        return false;
260    }
261
262    @Override
263    public void suspend() {
264        try {
265            in.getTerminal().restore();
266        } catch (Exception ex) {
267            throw new IllegalStateException(ex);
268        }
269    }
270
271    @Override
272    public void resume() {
273        try {
274            in.getTerminal().init();
275        } catch (Exception ex) {
276            throw new IllegalStateException(ex);
277        }
278    }
279
280    public void beforeUserCode() {
281        input.setState(State.BUFFER);
282    }
283
284    public void afterUserCode() {
285        input.setState(State.WAIT);
286    }
287
288    @Override
289    public void replaceLastHistoryEntry(String source) {
290        history.fullHistoryReplace(source);
291    }
292
293    private static final class JShellUnixTerminal extends NoInterruptUnixTerminal {
294
295        private final StopDetectingInputStream input;
296
297        public JShellUnixTerminal(StopDetectingInputStream input) throws Exception {
298            this.input = input;
299        }
300
301        public boolean isRaw() {
302            try {
303                return getSettings().get("-a").contains("-icanon");
304            } catch (IOException | InterruptedException ex) {
305                return false;
306            }
307        }
308
309        @Override
310        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
311            return input.setInputStream(super.wrapInIfNeeded(in));
312        }
313
314        @Override
315        public void disableInterruptCharacter() {
316        }
317
318        @Override
319        public void enableInterruptCharacter() {
320        }
321
322    }
323
324    private static final class JShellWindowsTerminal extends WindowsTerminal {
325
326        private final StopDetectingInputStream input;
327
328        public JShellWindowsTerminal(StopDetectingInputStream input) throws Exception {
329            this.input = input;
330        }
331
332        @Override
333        public void init() throws Exception {
334            super.init();
335            setAnsiSupported(false);
336        }
337
338        @Override
339        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
340            return input.setInputStream(super.wrapInIfNeeded(in));
341        }
342
343    }
344}
345