ConsoleIOContext.java revision 3767:b265444e51db
1167857Simp/*
2167857Simp * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
3167857Simp * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4167857Simp *
5167857Simp * This code is free software; you can redistribute it and/or modify it
6167857Simp * under the terms of the GNU General Public License version 2 only, as
7167857Simp * published by the Free Software Foundation.  Oracle designates this
8167857Simp * particular file as subject to the "Classpath" exception as provided
9167857Simp * by Oracle in the LICENSE file that accompanied this code.
10167857Simp *
11167857Simp * This code is distributed in the hope that it will be useful, but WITHOUT
12167857Simp * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13167857Simp * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14167857Simp * version 2 for more details (a copy is included in the LICENSE file that
15167857Simp * accompanied this code).
16167857Simp *
17167857Simp * You should have received a copy of the GNU General Public License version
18167857Simp * 2 along with this work; if not, write to the Free Software Foundation,
19167857Simp * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20167857Simp *
21167857Simp * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22167857Simp * or visit www.oracle.com if you need additional information or have any
23167857Simp * questions.
24167857Simp */
25167857Simp
26167857Simppackage jdk.internal.jshell.tool;
27167857Simp
28167857Simpimport jdk.jshell.SourceCodeAnalysis.Documentation;
29167857Simpimport jdk.jshell.SourceCodeAnalysis.QualifiedNames;
30289727Sianimport jdk.jshell.SourceCodeAnalysis.Suggestion;
31289727Sian
32289727Sianimport java.io.IOException;
33167857Simpimport java.io.InputStream;
34167857Simpimport java.io.InterruptedIOException;
35167857Simpimport java.io.PrintStream;
36167857Simpimport java.io.UncheckedIOException;
37167857Simpimport java.util.ArrayList;
38167857Simpimport java.util.Arrays;
39167857Simpimport java.util.Collection;
40181305Sjhbimport java.util.Collections;
41346548Sianimport java.util.HashMap;
42167857Simpimport java.util.Iterator;
43167857Simpimport java.util.List;
44289727Sianimport java.util.Locale;
45289727Sianimport java.util.Map;
46289727Sianimport java.util.Objects;
47289727Sianimport java.util.Optional;
48289727Sianimport java.util.function.Function;
49289727Sianimport java.util.prefs.BackingStoreException;
50167857Simpimport java.util.stream.Collectors;
51167857Simpimport java.util.stream.Stream;
52167857Simp
53167857Simpimport jdk.internal.shellsupport.doc.JavadocFormatter;
54167857Simpimport jdk.internal.jline.NoInterruptUnixTerminal;
55289727Sianimport jdk.internal.jline.Terminal;
56289727Sianimport jdk.internal.jline.TerminalFactory;
57289727Sianimport jdk.internal.jline.TerminalSupport;
58289727Sianimport jdk.internal.jline.WindowsTerminal;
59289727Sianimport jdk.internal.jline.console.ConsoleReader;
60289727Sianimport jdk.internal.jline.console.KeyMap;
61167857Simpimport jdk.internal.jline.console.UserInterruptException;
62289727Sianimport jdk.internal.jline.console.completer.Completer;
63167857Simpimport jdk.internal.jline.console.history.History;
64167857Simpimport jdk.internal.jline.console.history.MemoryHistory;
65289727Sianimport jdk.internal.jline.extra.EditingHistory;
66167857Simpimport jdk.internal.jshell.tool.StopDetectingInputStream.State;
67289727Sianimport jdk.internal.misc.Signal;
68167857Simpimport jdk.internal.misc.Signal.Handler;
69289727Sian
70167857Simpclass ConsoleIOContext extends IOContext {
71167857Simp
72167857Simp    private static final String HISTORY_LINE_PREFIX = "HISTORY_LINE_";
73289727Sian
74289727Sian    final JShellTool repl;
75289727Sian    final StopDetectingInputStream input;
76289727Sian    final ConsoleReader in;
77289727Sian    final EditingHistory history;
78289727Sian    final MemoryHistory userInputHistory = new MemoryHistory();
79289727Sian
80289727Sian    String prefix = "";
81289727Sian
82289727Sian    ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout) throws Exception {
83289727Sian        this.repl = repl;
84289727Sian        this.input = new StopDetectingInputStream(() -> repl.state.stop(), ex -> repl.hard("Error on input: %s", ex));
85289727Sian        Terminal term;
86289727Sian        if (System.getProperty("test.jdk") != null) {
87289727Sian            term = new TestTerminal(input);
88289727Sian        } else if (System.getProperty("os.name").toLowerCase(Locale.US).contains(TerminalFactory.WINDOWS)) {
89289727Sian            term = new JShellWindowsTerminal(input);
90289727Sian        } else {
91289727Sian            term = new JShellUnixTerminal(input);
92289727Sian        }
93289727Sian        term.init();
94289727Sian        in = new ConsoleReader(cmdin, cmdout, term);
95289727Sian        in.setExpandEvents(false);
96289727Sian        in.setHandleUserInterrupt(true);
97289727Sian        List<String> persistenHistory = Stream.of(repl.prefs.keys())
98289727Sian                                              .filter(key -> key.startsWith(HISTORY_LINE_PREFIX))
99289727Sian                                              .sorted()
100289727Sian                                              .map(key -> repl.prefs.get(key, null))
101289727Sian                                              .collect(Collectors.toList());
102289727Sian        in.setHistory(history = new EditingHistory(in, persistenHistory) {
103289727Sian            @Override protected boolean isComplete(CharSequence input) {
104289727Sian                return repl.analysis.analyzeCompletion(input.toString()).completeness().isComplete();
105289727Sian            }
106289727Sian        });
107289727Sian        in.setBellEnabled(true);
108289727Sian        in.setCopyPasteDetection(true);
109289727Sian        in.addCompleter(new Completer() {
110289727Sian            private String lastTest;
111167857Simp            private int lastCursor;
112167857Simp            private boolean allowSmart = false;
113167857Simp            @Override public int complete(String test, int cursor, List<CharSequence> result) {
114167857Simp                int[] anchor = new int[] {-1};
115167857Simp                List<Suggestion> suggestions;
116167857Simp                if (prefix.isEmpty() && test.trim().startsWith("/")) {
117167857Simp                    suggestions = repl.commandCompletionSuggestions(test, cursor, anchor);
118167857Simp                } else {
119167857Simp                    int prefixLength = prefix.length();
120167857Simp                    suggestions = repl.analysis.completionSuggestions(prefix + test, cursor + prefixLength, anchor);
121167857Simp                    anchor[0] -= prefixLength;
122181305Sjhb                }
123167857Simp                if (!Objects.equals(lastTest, test) || lastCursor != cursor)
124167857Simp                    allowSmart = true;
125167857Simp
126167857Simp                boolean smart = allowSmart &&
127167857Simp                                suggestions.stream()
128167857Simp                                           .anyMatch(s -> s.matchesType());
129289727Sian
130167857Simp                lastTest = test;
131167857Simp                lastCursor = cursor;
132167857Simp                allowSmart = !allowSmart;
133289727Sian
134289083Sian                suggestions.stream()
135289727Sian                           .filter(s -> !smart || s.matchesType())
136289727Sian                           .map(s -> s.continuation())
137289727Sian                           .forEach(result::add);
138289727Sian
139289727Sian                boolean onlySmart = suggestions.stream()
140289727Sian                                               .allMatch(s -> s.matchesType());
141289727Sian
142289727Sian                if (smart && !onlySmart) {
143289727Sian                    Optional<String> prefix =
144289727Sian                            suggestions.stream()
145289727Sian                                       .map(s -> s.continuation())
146289727Sian                                       .reduce(ConsoleIOContext::commonPrefix);
147289727Sian
148289727Sian                    String prefixStr = prefix.orElse("").substring(cursor - anchor[0]);
149289727Sian                    try {
150289727Sian                        in.putString(prefixStr);
151289727Sian                        cursor += prefixStr.length();
152289727Sian                    } catch (IOException ex) {
153289727Sian                        throw new IllegalStateException(ex);
154289727Sian                    }
155289727Sian                    result.add(repl.messageFormat("jshell.console.see.more"));
156289727Sian                    return cursor; //anchor should not be used.
157289727Sian                }
158289727Sian
159289727Sian                if (result.isEmpty()) {
160289727Sian                    try {
161289727Sian                        //provide "empty completion" feedback
162289727Sian                        //XXX: this only works correctly when there is only one Completer:
163289727Sian                        in.beep();
164289727Sian                    } catch (IOException ex) {
165289727Sian                        throw new UncheckedIOException(ex);
166167857Simp                    }
167186833Snwhitehorn                }
168167857Simp
169167857Simp                return anchor[0];
170289727Sian            }
171289727Sian        });
172289727Sian        bind(DOCUMENTATION_SHORTCUT, (Runnable) () -> documentation(repl));
173289727Sian        for (FixComputer computer : FIX_COMPUTERS) {
174289727Sian            for (String shortcuts : SHORTCUT_FIXES) {
175289727Sian                bind(shortcuts + computer.shortcut, (Runnable) () -> fixes(computer));
176289727Sian            }
177289727Sian        }
178289727Sian        try {
179289727Sian            Signal.handle(new Signal("CONT"), new Handler() {
180289727Sian                @Override public void handle(Signal sig) {
181289727Sian                    try {
182289727Sian                        in.getTerminal().reset();
183289727Sian                        in.redrawLine();
184167857Simp                        in.flush();
185167857Simp                    } catch (Exception ex) {
186167857Simp                        ex.printStackTrace();
187167857Simp                    }
188346548Sian                }
189346548Sian            });
190167857Simp        } catch (IllegalArgumentException ignored) {
191289727Sian            //the CONT signal does not exist on this platform
192181325Sstas        }
193289727Sian    }
194289727Sian
195289727Sian    @Override
196289727Sian    public String readLine(String prompt, String prefix) throws IOException, InputInterruptedException {
197289727Sian        this.prefix = prefix;
198289727Sian        try {
199289727Sian            return in.readLine(prompt);
200167857Simp        } catch (UserInterruptException ex) {
201289727Sian            throw (InputInterruptedException) new InputInterruptedException().initCause(ex);
202167857Simp        }
203167857Simp    }
204167857Simp
205167857Simp    @Override
206289727Sian    public boolean interactiveOutput() {
207167857Simp        return true;
208167857Simp    }
209346548Sian
210346548Sian    @Override
211346548Sian    public Iterable<String> currentSessionHistory() {
212346548Sian        return history.currentSessionEntries();
213346548Sian    }
214346548Sian
215346548Sian    @Override
216346548Sian    public void close() throws IOException {
217346548Sian        //save history:
218346548Sian        try {
219289727Sian            for (String key : repl.prefs.keys()) {
220167857Simp                if (key.startsWith(HISTORY_LINE_PREFIX))
221167857Simp                    repl.prefs.remove(key);
222323931Sian            }
223323931Sian            Collection<? extends String> savedHistory = history.save();
224323931Sian            if (!savedHistory.isEmpty()) {
225323931Sian                int len = (int) Math.ceil(Math.log10(savedHistory.size()+1));
226323931Sian                String format = HISTORY_LINE_PREFIX + "%0" + len + "d";
227323931Sian                int index = 0;
228323931Sian                for (String historyLine : savedHistory) {
229323931Sian                    repl.prefs.put(String.format(format, index++), historyLine);
230323931Sian                }
231167857Simp            }
232167857Simp        } catch (BackingStoreException ex) {
233167857Simp            throw new IllegalStateException(ex);
234323931Sian        }
235167857Simp        in.shutdown();
236323931Sian        try {
237323931Sian            in.getTerminal().restore();
238323931Sian        } catch (Exception ex) {
239323931Sian            throw new IOException(ex);
240289083Sian        }
241167857Simp        input.shutdown();
242167857Simp    }
243167857Simp
244167857Simp    private void bind(String shortcut, Object action) {
245167857Simp        KeyMap km = in.getKeys();
246323931Sian        for (int i = 0; i < shortcut.length(); i++) {
247167857Simp            Object value = km.getBound(Character.toString(shortcut.charAt(i)));
248323931Sian            if (value instanceof KeyMap) {
249323931Sian                km = (KeyMap) value;
250167857Simp            } else {
251167857Simp                km.bind(shortcut.substring(i), action);
252167857Simp            }
253167857Simp        }
254167857Simp    }
255167857Simp
256167857Simp    private static final String DOCUMENTATION_SHORTCUT = "\033\133\132"; //Shift-TAB
257167857Simp    private static final String[] SHORTCUT_FIXES = {
258167857Simp        "\033\015", //Alt-Enter (Linux)
259167857Simp        "\033\012", //Alt-Enter (Linux)
260167857Simp        "\033\133\061\067\176", //F6/Alt-F1 (Mac)
261167857Simp        "\u001BO3P" //Alt-F1 (Linux)
262167857Simp    };
263167857Simp
264167857Simp    private String lastDocumentationBuffer;
265167857Simp    private int lastDocumentationCursor = (-1);
266167857Simp
267167857Simp    private void documentation(JShellTool repl) {
268167857Simp        String buffer = in.getCursorBuffer().buffer.toString();
269167857Simp        int cursor = in.getCursorBuffer().cursor;
270167857Simp        boolean firstInvocation = !buffer.equals(lastDocumentationBuffer) || cursor != lastDocumentationCursor;
271167857Simp        lastDocumentationBuffer = buffer;
272167857Simp        lastDocumentationCursor = cursor;
273167857Simp        List<String> doc;
274167857Simp        String seeMore;
275167857Simp        Terminal term = in.getTerminal();
276289727Sian        if (prefix.isEmpty() && buffer.trim().startsWith("/")) {
277167857Simp            doc = Arrays.asList(repl.commandDocumentation(buffer, cursor, firstInvocation));
278167857Simp            seeMore = "jshell.console.see.help";
279167857Simp        } else {
280167857Simp            JavadocFormatter formatter = new JavadocFormatter(term.getWidth(),
281167857Simp                                                              term.isAnsiSupported());
282167857Simp            Function<Documentation, String> convertor;
283167857Simp            if (firstInvocation) {
284167857Simp                convertor = d -> d.signature();
285167857Simp            } else {
286167857Simp                convertor = d -> formatter.formatJavadoc(d.signature(), d.javadoc()) +
287167857Simp                                 (d.javadoc() == null ? repl.messageFormat("jshell.console.no.javadoc")
288167857Simp                                                      : "");
289167857Simp            }
290167857Simp            doc = repl.analysis.documentation(prefix + buffer, cursor + prefix.length(), !firstInvocation)
291167857Simp                               .stream()
292167857Simp                               .map(convertor)
293167857Simp                               .collect(Collectors.toList());
294167857Simp            seeMore = "jshell.console.see.javadoc";
295289727Sian        }
296289105Sian
297289105Sian        try {
298167857Simp            if (doc != null && !doc.isEmpty()) {
299289105Sian                if (firstInvocation) {
300167857Simp                    in.println();
301167857Simp                    in.println(doc.stream().collect(Collectors.joining("\n")));
302167857Simp                    in.println(repl.messageFormat(seeMore));
303167857Simp                    in.redrawLine();
304167857Simp                    in.flush();
305167857Simp                } else {
306167857Simp                    in.println();
307167857Simp
308167857Simp                    int height = term.getHeight();
309167857Simp                    String lastNote = "";
310167857Simp
311167857Simp                    PRINT_DOC: for (Iterator<String> docIt = doc.iterator(); docIt.hasNext(); ) {
312167857Simp                        String currentDoc = docIt.next();
313167857Simp                        String[] lines = currentDoc.split("\n");
314167857Simp                        int firstLine = 0;
315167857Simp
316167857Simp                        PRINT_PAGE: while (true) {
317167857Simp                            int toPrint = height - 1;
318167857Simp
319167857Simp                            while (toPrint > 0 && firstLine < lines.length) {
320167857Simp                                in.println(lines[firstLine++]);
321167857Simp                                toPrint--;
322167857Simp                            }
323167857Simp
324167857Simp                            if (firstLine >= lines.length) {
325167857Simp                                break;
326167857Simp                            }
327167857Simp
328167857Simp                            lastNote = repl.getResourceString("jshell.console.see.next.page");
329167857Simp                            in.print(lastNote + ConsoleReader.RESET_LINE);
330167857Simp                            in.flush();
331289118Sian
332167857Simp                            while (true) {
333167857Simp                                int r = in.readCharacter();
334167857Simp
335167857Simp                                switch (r) {
336167857Simp                                    case ' ': continue PRINT_PAGE;
337167857Simp                                    case 'q':
338167857Simp                                    case 3:
339167857Simp                                        break PRINT_DOC;
340167857Simp                                    default:
341167857Simp                                        in.beep();
342167857Simp                                        break;
343167857Simp                                }
344167857Simp                            }
345167857Simp                        }
346167857Simp
347167857Simp                        if (docIt.hasNext()) {
348167857Simp                            lastNote = repl.getResourceString("jshell.console.see.next.javadoc");
349167857Simp                            in.print(lastNote + ConsoleReader.RESET_LINE);
350167857Simp                            in.flush();
351167857Simp
352167857Simp                            while (true) {
353167857Simp                                int r = in.readCharacter();
354167857Simp
355289727Sian                                switch (r) {
356289105Sian                                    case ' ': continue PRINT_DOC;
357289105Sian                                    case 'q':
358167857Simp                                    case 3:
359289105Sian                                        break PRINT_DOC;
360289083Sian                                    default:
361167857Simp                                        in.beep();
362167857Simp                                        break;
363289083Sian                                }
364289727Sian                            }
365289727Sian                        }
366167857Simp                    }
367289105Sian                    //clear the "press space" line:
368289105Sian                    in.getCursorBuffer().buffer.replace(0, buffer.length(), lastNote);
369289083Sian                    in.getCursorBuffer().cursor = 0;
370289105Sian                    in.killLine();
371167857Simp                    in.getCursorBuffer().buffer.append(buffer);
372167857Simp                    in.getCursorBuffer().cursor = cursor;
373167857Simp                    in.redrawLine();
374167857Simp                    in.flush();
375167857Simp                }
376167857Simp            } else {
377167857Simp                in.beep();
378323931Sian            }
379167857Simp        } catch (IOException ex) {
380246128Ssbz            throw new IllegalStateException(ex);
381167857Simp        }
382167857Simp    }
383167857Simp
384167857Simp    private static String commonPrefix(String str1, String str2) {
385167857Simp        for (int i = 0; i < str2.length(); i++) {
386167857Simp            if (!str1.startsWith(str2.substring(0, i + 1))) {
387167857Simp                return str2.substring(0, i);
388167857Simp            }
389167857Simp        }
390167857Simp
391167857Simp        return str2;
392167857Simp    }
393
394    @Override
395    public boolean terminalEditorRunning() {
396        Terminal terminal = in.getTerminal();
397        if (terminal instanceof JShellUnixTerminal)
398            return ((JShellUnixTerminal) terminal).isRaw();
399        return false;
400    }
401
402    @Override
403    public void suspend() {
404        try {
405            in.getTerminal().restore();
406        } catch (Exception ex) {
407            throw new IllegalStateException(ex);
408        }
409    }
410
411    @Override
412    public void resume() {
413        try {
414            in.getTerminal().init();
415        } catch (Exception ex) {
416            throw new IllegalStateException(ex);
417        }
418    }
419
420    public void beforeUserCode() {
421        synchronized (this) {
422            inputBytes = null;
423        }
424        input.setState(State.BUFFER);
425    }
426
427    public void afterUserCode() {
428        input.setState(State.WAIT);
429    }
430
431    @Override
432    public void replaceLastHistoryEntry(String source) {
433        history.fullHistoryReplace(source);
434    }
435
436    //compute possible options/Fixes based on the selected FixComputer, present them to the user,
437    //and perform the selected one:
438    private void fixes(FixComputer computer) {
439        String input = prefix + in.getCursorBuffer().toString();
440        int cursor = prefix.length() + in.getCursorBuffer().cursor;
441        FixResult candidates = computer.compute(repl, input, cursor);
442
443        try {
444            final boolean printError = candidates.error != null && !candidates.error.isEmpty();
445            if (printError) {
446                in.println(candidates.error);
447            }
448            if (candidates.fixes.isEmpty()) {
449                in.beep();
450                if (printError) {
451                    in.redrawLine();
452                    in.flush();
453                }
454            } else if (candidates.fixes.size() == 1 && !computer.showMenu) {
455                if (printError) {
456                    in.redrawLine();
457                    in.flush();
458                }
459                candidates.fixes.get(0).perform(in);
460            } else {
461                List<Fix> fixes = new ArrayList<>(candidates.fixes);
462                fixes.add(0, new Fix() {
463                    @Override
464                    public String displayName() {
465                        return repl.messageFormat("jshell.console.do.nothing");
466                    }
467
468                    @Override
469                    public void perform(ConsoleReader in) throws IOException {
470                        in.redrawLine();
471                    }
472                });
473
474                Map<Character, Fix> char2Fix = new HashMap<>();
475                in.println();
476                for (int i = 0; i < fixes.size(); i++) {
477                    Fix fix = fixes.get(i);
478                    char2Fix.put((char) ('0' + i), fix);
479                    in.println("" + i + ": " + fixes.get(i).displayName());
480                }
481                in.print(repl.messageFormat("jshell.console.choice"));
482                in.flush();
483                int read;
484
485                read = in.readCharacter();
486
487                Fix fix = char2Fix.get((char) read);
488
489                if (fix == null) {
490                    in.beep();
491                    fix = fixes.get(0);
492                }
493
494                in.println();
495
496                fix.perform(in);
497
498                in.flush();
499            }
500        } catch (IOException ex) {
501            ex.printStackTrace();
502        }
503    }
504
505    private byte[] inputBytes;
506    private int inputBytesPointer;
507
508    @Override
509    public synchronized int readUserInput() throws IOException {
510        while (inputBytes == null || inputBytes.length <= inputBytesPointer) {
511            boolean prevHandleUserInterrupt = in.getHandleUserInterrupt();
512            History prevHistory = in.getHistory();
513
514            try {
515                input.setState(State.WAIT);
516                in.setHandleUserInterrupt(true);
517                in.setHistory(userInputHistory);
518                inputBytes = (in.readLine("") + System.getProperty("line.separator")).getBytes();
519                inputBytesPointer = 0;
520            } catch (UserInterruptException ex) {
521                throw new InterruptedIOException();
522            } finally {
523                in.setHistory(prevHistory);
524                in.setHandleUserInterrupt(prevHandleUserInterrupt);
525                input.setState(State.BUFFER);
526            }
527        }
528        return inputBytes[inputBytesPointer++];
529    }
530
531    /**
532     * A possible action which the user can choose to perform.
533     */
534    public interface Fix {
535        /**
536         * A name that should be shown to the user.
537         */
538        public String displayName();
539        /**
540         * Perform the given action.
541         */
542        public void perform(ConsoleReader in) throws IOException;
543    }
544
545    /**
546     * A factory for {@link Fix}es.
547     */
548    public abstract static class FixComputer {
549        private final char shortcut;
550        private final boolean showMenu;
551
552        /**
553         * Construct a new FixComputer. {@code shortcut} defines the key which should trigger this FixComputer.
554         * If {@code showMenu} is {@code false}, and this computer returns exactly one {@code Fix},
555         * no options will be show to the user, and the given {@code Fix} will be performed.
556         */
557        public FixComputer(char shortcut, boolean showMenu) {
558            this.shortcut = shortcut;
559            this.showMenu = showMenu;
560        }
561
562        /**
563         * Compute possible actions for the given code.
564         */
565        public abstract FixResult compute(JShellTool repl, String code, int cursor);
566    }
567
568    /**
569     * A list of {@code Fix}es with a possible error that should be shown to the user.
570     */
571    public static class FixResult {
572        public final List<Fix> fixes;
573        public final String error;
574
575        public FixResult(List<Fix> fixes, String error) {
576            this.fixes = fixes;
577            this.error = error;
578        }
579    }
580
581    private static final FixComputer[] FIX_COMPUTERS = new FixComputer[] {
582        new FixComputer('v', false) { //compute "Introduce variable" Fix:
583            private void performToVar(ConsoleReader in, String type) throws IOException {
584                in.redrawLine();
585                in.setCursorPosition(0);
586                in.putString(type + "  = ");
587                in.setCursorPosition(in.getCursorBuffer().cursor - 3);
588                in.flush();
589            }
590
591            @Override
592            public FixResult compute(JShellTool repl, String code, int cursor) {
593                String type = repl.analysis.analyzeType(code, cursor);
594                if (type == null) {
595                    return new FixResult(Collections.emptyList(), null);
596                }
597                List<Fix> fixes = new ArrayList<>();
598                fixes.add(new Fix() {
599                    @Override
600                    public String displayName() {
601                        return repl.messageFormat("jshell.console.create.variable");
602                    }
603
604                    @Override
605                    public void perform(ConsoleReader in) throws IOException {
606                        performToVar(in, type);
607                    }
608                });
609                int idx = type.lastIndexOf(".");
610                if (idx > 0) {
611                    String stype = type.substring(idx + 1);
612                    QualifiedNames res = repl.analysis.listQualifiedNames(stype, stype.length());
613                    if (res.isUpToDate() && res.getNames().contains(type)
614                            && !res.isResolvable()) {
615                        fixes.add(new Fix() {
616                            @Override
617                            public String displayName() {
618                                return "import: " + type + ". " +
619                                        repl.messageFormat("jshell.console.create.variable");
620                            }
621
622                            @Override
623                            public void perform(ConsoleReader in) throws IOException {
624                                repl.state.eval("import " + type + ";");
625                                in.println("Imported: " + type);
626                                performToVar(in, stype);
627                            }
628                        });
629                    }
630                }
631                return new FixResult(fixes, null);
632            }
633        },
634        new FixComputer('i', true) { //compute "Add import" Fixes:
635            @Override
636            public FixResult compute(JShellTool repl, String code, int cursor) {
637                QualifiedNames res = repl.analysis.listQualifiedNames(code, cursor);
638                List<Fix> fixes = new ArrayList<>();
639                for (String fqn : res.getNames()) {
640                    fixes.add(new Fix() {
641                        @Override
642                        public String displayName() {
643                            return "import: " + fqn;
644                        }
645
646                        @Override
647                        public void perform(ConsoleReader in) throws IOException {
648                            repl.state.eval("import " + fqn + ";");
649                            in.println("Imported: " + fqn);
650                            in.redrawLine();
651                        }
652                    });
653                }
654                if (res.isResolvable()) {
655                    return new FixResult(Collections.emptyList(),
656                            repl.messageFormat("jshell.console.resolvable"));
657                } else {
658                    String error = "";
659                    if (fixes.isEmpty()) {
660                        error = repl.messageFormat("jshell.console.no.candidate");
661                    }
662                    if (!res.isUpToDate()) {
663                        error += repl.messageFormat("jshell.console.incomplete");
664                    }
665                    return new FixResult(fixes, error);
666                }
667            }
668        }
669    };
670
671    private static final class JShellUnixTerminal extends NoInterruptUnixTerminal {
672
673        private final StopDetectingInputStream input;
674
675        public JShellUnixTerminal(StopDetectingInputStream input) throws Exception {
676            this.input = input;
677        }
678
679        public boolean isRaw() {
680            try {
681                return getSettings().get("-a").contains("-icanon");
682            } catch (IOException | InterruptedException ex) {
683                return false;
684            }
685        }
686
687        @Override
688        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
689            return input.setInputStream(super.wrapInIfNeeded(in));
690        }
691
692        @Override
693        public void disableInterruptCharacter() {
694        }
695
696        @Override
697        public void enableInterruptCharacter() {
698        }
699
700    }
701
702    private static final class JShellWindowsTerminal extends WindowsTerminal {
703
704        private final StopDetectingInputStream input;
705
706        public JShellWindowsTerminal(StopDetectingInputStream input) throws Exception {
707            this.input = input;
708        }
709
710        @Override
711        public void init() throws Exception {
712            super.init();
713            setAnsiSupported(false);
714        }
715
716        @Override
717        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
718            return input.setInputStream(super.wrapInIfNeeded(in));
719        }
720
721    }
722
723    private static final class TestTerminal extends TerminalSupport {
724
725        private final StopDetectingInputStream input;
726
727        public TestTerminal(StopDetectingInputStream input) throws Exception {
728            super(true);
729            setAnsiSupported(false);
730            setEchoEnabled(true);
731            this.input = input;
732        }
733
734        @Override
735        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
736            return input.setInputStream(super.wrapInIfNeeded(in));
737        }
738
739    }
740}
741