ConsoleIOContext.java revision 4279:d78323fc3fd5
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.jshell.tool;
27
28import jdk.jshell.SourceCodeAnalysis.Documentation;
29import jdk.jshell.SourceCodeAnalysis.QualifiedNames;
30import jdk.jshell.SourceCodeAnalysis.Suggestion;
31
32import java.io.IOException;
33import java.io.InputStream;
34import java.io.InterruptedIOException;
35import java.io.PrintStream;
36import java.util.ArrayList;
37import java.util.Arrays;
38import java.util.Collection;
39import java.util.Collections;
40import java.util.HashMap;
41import java.util.Iterator;
42import java.util.List;
43import java.util.Locale;
44import java.util.Map;
45import java.util.Optional;
46import java.util.function.Function;
47import java.util.stream.Collectors;
48import java.util.stream.Stream;
49
50import jdk.internal.shellsupport.doc.JavadocFormatter;
51import jdk.internal.jline.NoInterruptUnixTerminal;
52import jdk.internal.jline.Terminal;
53import jdk.internal.jline.TerminalFactory;
54import jdk.internal.jline.TerminalSupport;
55import jdk.internal.jline.WindowsTerminal;
56import jdk.internal.jline.console.ConsoleReader;
57import jdk.internal.jline.console.KeyMap;
58import jdk.internal.jline.console.Operation;
59import jdk.internal.jline.console.UserInterruptException;
60import jdk.internal.jline.console.history.History;
61import jdk.internal.jline.console.history.MemoryHistory;
62import jdk.internal.jline.extra.EditingHistory;
63import jdk.internal.jline.internal.NonBlockingInputStream;
64import jdk.internal.jshell.tool.StopDetectingInputStream.State;
65import jdk.internal.misc.Signal;
66import jdk.internal.misc.Signal.Handler;
67import jdk.jshell.ExpressionSnippet;
68import jdk.jshell.Snippet;
69import jdk.jshell.Snippet.SubKind;
70import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
71import jdk.jshell.VarSnippet;
72
73class ConsoleIOContext extends IOContext {
74
75    private static final String HISTORY_LINE_PREFIX = "HISTORY_LINE_";
76
77    final JShellTool repl;
78    final StopDetectingInputStream input;
79    final ConsoleReader in;
80    final EditingHistory history;
81    final MemoryHistory userInputHistory = new MemoryHistory();
82
83    String prefix = "";
84
85    ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout) throws Exception {
86        this.repl = repl;
87        this.input = new StopDetectingInputStream(() -> repl.stop(), ex -> repl.hard("Error on input: %s", ex));
88        Terminal term;
89        if (System.getProperty("test.jdk") != null) {
90            term = new TestTerminal(input);
91        } else if (System.getProperty("os.name").toLowerCase(Locale.US).contains(TerminalFactory.WINDOWS)) {
92            term = new JShellWindowsTerminal(input);
93        } else {
94            term = new JShellUnixTerminal(input);
95        }
96        term.init();
97        List<CompletionTask> completionTODO = new ArrayList<>();
98        in = new ConsoleReader(cmdin, cmdout, term) {
99            @Override public KeyMap getKeys() {
100                return new CheckCompletionKeyMap(super.getKeys(), completionTODO);
101            }
102            @Override
103            protected boolean complete() throws IOException {
104                return ConsoleIOContext.this.complete(completionTODO);
105            }
106        };
107        in.setExpandEvents(false);
108        in.setHandleUserInterrupt(true);
109        List<String> persistenHistory = Stream.of(repl.prefs.keys())
110                                              .filter(key -> key.startsWith(HISTORY_LINE_PREFIX))
111                                              .sorted()
112                                              .map(key -> repl.prefs.get(key))
113                                              .collect(Collectors.toList());
114        in.setHistory(history = new EditingHistory(in, persistenHistory) {
115            @Override protected boolean isComplete(CharSequence input) {
116                return repl.analysis.analyzeCompletion(input.toString()).completeness().isComplete();
117            }
118        });
119        in.setBellEnabled(true);
120        in.setCopyPasteDetection(true);
121        bind(FIXES_SHORTCUT, (Runnable) () -> fixes());
122        try {
123            Signal.handle(new Signal("CONT"), new Handler() {
124                @Override public void handle(Signal sig) {
125                    try {
126                        in.getTerminal().reset();
127                        in.redrawLine();
128                        in.flush();
129                    } catch (Exception ex) {
130                        ex.printStackTrace();
131                    }
132                }
133            });
134        } catch (IllegalArgumentException ignored) {
135            //the CONT signal does not exist on this platform
136        }
137    }
138
139    @Override
140    public String readLine(String prompt, String prefix) throws IOException, InputInterruptedException {
141        this.prefix = prefix;
142        try {
143            return in.readLine(prompt);
144        } catch (UserInterruptException ex) {
145            throw (InputInterruptedException) new InputInterruptedException().initCause(ex);
146        }
147    }
148
149    @Override
150    public boolean interactiveOutput() {
151        return true;
152    }
153
154    @Override
155    public Iterable<String> currentSessionHistory() {
156        return history.currentSessionEntries();
157    }
158
159    @Override
160    public void close() throws IOException {
161        //save history:
162        for (String key : repl.prefs.keys()) {
163            if (key.startsWith(HISTORY_LINE_PREFIX)) {
164                repl.prefs.remove(key);
165            }
166        }
167        Collection<? extends String> savedHistory = history.save();
168        if (!savedHistory.isEmpty()) {
169            int len = (int) Math.ceil(Math.log10(savedHistory.size()+1));
170            String format = HISTORY_LINE_PREFIX + "%0" + len + "d";
171            int index = 0;
172            for (String historyLine : savedHistory) {
173                repl.prefs.put(String.format(format, index++), historyLine);
174            }
175        }
176        repl.prefs.flush();
177        in.shutdown();
178        try {
179            in.getTerminal().restore();
180        } catch (Exception ex) {
181            throw new IOException(ex);
182        }
183        input.shutdown();
184    }
185
186    private void bind(String shortcut, Object action) {
187        KeyMap km = in.getKeys();
188        for (int i = 0; i < shortcut.length(); i++) {
189            Object value = km.getBound(Character.toString(shortcut.charAt(i)));
190            if (value instanceof KeyMap) {
191                km = (KeyMap) value;
192            } else {
193                km.bind(shortcut.substring(i), action);
194            }
195        }
196    }
197
198    private static final String FIXES_SHORTCUT = "\033\133\132"; //Shift-TAB
199
200    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
201    private static final String LINE_SEPARATORS2 = LINE_SEPARATOR + LINE_SEPARATOR;
202
203    @SuppressWarnings("fallthrough")
204    private boolean complete(List<CompletionTask> todo) {
205        //The completion has multiple states (invoked by subsequent presses of <tab>).
206        //On the first invocation in a given sequence, all steps are precomputed
207        //and placed into the todo list. The todo list is then followed on both the first
208        //and subsequent <tab> presses:
209        try {
210            String text = in.getCursorBuffer().toString();
211            int cursor = in.getCursorBuffer().cursor;
212            if (todo.isEmpty()) {
213                int[] anchor = new int[] {-1};
214                List<Suggestion> suggestions;
215                List<String> doc;
216                boolean command = prefix.isEmpty() && text.trim().startsWith("/");
217                if (command) {
218                    suggestions = repl.commandCompletionSuggestions(text, cursor, anchor);
219                    doc = repl.commandDocumentation(text, cursor, true);
220                } else {
221                    int prefixLength = prefix.length();
222                    suggestions = repl.analysis.completionSuggestions(prefix + text, cursor + prefixLength, anchor);
223                    anchor[0] -= prefixLength;
224                    doc = repl.analysis.documentation(prefix + text, cursor + prefix.length(), false)
225                                       .stream()
226                                       .map(Documentation::signature)
227                                       .collect(Collectors.toList());
228                }
229                long smartCount = suggestions.stream().filter(Suggestion::matchesType).count();
230                boolean hasSmart = smartCount > 0 && smartCount <= in.getAutoprintThreshold();
231                boolean hasBoth = hasSmart &&
232                                  suggestions.stream()
233                                             .map(s -> s.matchesType())
234                                             .distinct()
235                                             .count() == 2;
236                boolean tooManyItems = suggestions.size() > in.getAutoprintThreshold();
237                CompletionTask ordinaryCompletion = new OrdinaryCompletionTask(suggestions, anchor[0], !command && !doc.isEmpty(), hasSmart);
238                CompletionTask allCompletion = new AllSuggestionsCompletionTask(suggestions, anchor[0]);
239
240                //the main decission tree:
241                if (command) {
242                    CompletionTask shortDocumentation = new CommandSynopsisTask(doc);
243                    CompletionTask fullDocumentation = new CommandFullDocumentationTask(todo);
244
245                    if (!doc.isEmpty()) {
246                        if (tooManyItems) {
247                            todo.add(new NoopCompletionTask());
248                            todo.add(allCompletion);
249                        } else {
250                            todo.add(ordinaryCompletion);
251                        }
252                        todo.add(shortDocumentation);
253                        todo.add(fullDocumentation);
254                    } else {
255                        todo.add(new NoSuchCommandCompletionTask());
256                    }
257                } else {
258                    if (doc.isEmpty()) {
259                        if (hasSmart) {
260                            todo.add(ordinaryCompletion);
261                        } else if (tooManyItems) {
262                            todo.add(new NoopCompletionTask());
263                        }
264                        if (!hasSmart || hasBoth) {
265                            todo.add(allCompletion);
266                        }
267                    } else {
268                        CompletionTask shortDocumentation = new ExpressionSignaturesTask(doc);
269                        CompletionTask fullDocumentation = new ExpressionJavadocTask(todo);
270
271                        if (hasSmart) {
272                            todo.add(ordinaryCompletion);
273                        }
274                        todo.add(shortDocumentation);
275                        if (!hasSmart || hasBoth) {
276                            todo.add(allCompletion);
277                        }
278                        if (tooManyItems) {
279                            todo.add(todo.size() - 1, fullDocumentation);
280                        } else {
281                            todo.add(fullDocumentation);
282                        }
283                    }
284                }
285            }
286
287            boolean success = false;
288            boolean repaint = true;
289
290            OUTER: while (!todo.isEmpty()) {
291                CompletionTask.Result result = todo.remove(0).perform(text, cursor);
292
293                switch (result) {
294                    case CONTINUE:
295                        break;
296                    case SKIP_NOREPAINT:
297                        repaint = false;
298                    case SKIP:
299                        todo.clear();
300                        //intentional fall-through
301                    case FINISH:
302                        success = true;
303                        //intentional fall-through
304                    case NO_DATA:
305                        if (!todo.isEmpty()) {
306                            in.println();
307                            in.println(todo.get(0).description());
308                        }
309                        break OUTER;
310                }
311            }
312
313            if (repaint) {
314                in.redrawLine();
315                in.flush();
316            }
317
318            return success;
319        } catch (IOException ex) {
320            throw new IllegalStateException(ex);
321        }
322    }
323
324    private CompletionTask.Result doPrintFullDocumentation(List<CompletionTask> todo, List<String> doc, boolean command) {
325        if (doc != null && !doc.isEmpty()) {
326            Terminal term = in.getTerminal();
327            int pageHeight = term.getHeight() - NEEDED_LINES;
328            List<CompletionTask> thisTODO = new ArrayList<>();
329
330            for (Iterator<String> docIt = doc.iterator(); docIt.hasNext(); ) {
331                String currentDoc = docIt.next();
332                String[] lines = currentDoc.split("\n");
333                int firstLine = 0;
334
335                while (firstLine < lines.length) {
336                    boolean first = firstLine == 0;
337                    String[] thisPageLines =
338                            Arrays.copyOfRange(lines,
339                                               firstLine,
340                                               Math.min(firstLine + pageHeight, lines.length));
341
342                    thisTODO.add(new CompletionTask() {
343                        @Override
344                        public String description() {
345                            String key =  !first ? "jshell.console.see.next.page"
346                                                 : command ? "jshell.console.see.next.command.doc"
347                                                           : "jshell.console.see.next.javadoc";
348
349                            return repl.getResourceString(key);
350                        }
351
352                        @Override
353                        public Result perform(String text, int cursor) throws IOException {
354                            in.println();
355                            for (String line : thisPageLines) {
356                                in.println(line);
357                            }
358                            return Result.FINISH;
359                        }
360                    });
361
362                    firstLine += pageHeight;
363                }
364            }
365
366            todo.addAll(0, thisTODO);
367
368            return CompletionTask.Result.CONTINUE;
369        }
370
371        return CompletionTask.Result.FINISH;
372    }
373    //where:
374        private static final int NEEDED_LINES = 4;
375
376    private static String commonPrefix(String str1, String str2) {
377        for (int i = 0; i < str2.length(); i++) {
378            if (!str1.startsWith(str2.substring(0, i + 1))) {
379                return str2.substring(0, i);
380            }
381        }
382
383        return str2;
384    }
385
386    private interface CompletionTask {
387        public String description();
388        public Result perform(String text, int cursor) throws IOException;
389
390        enum Result {
391            NO_DATA,
392            CONTINUE,
393            FINISH,
394            SKIP,
395            SKIP_NOREPAINT;
396        }
397    }
398
399    private final class NoopCompletionTask implements CompletionTask {
400
401        @Override
402        public String description() {
403            throw new UnsupportedOperationException("Should not get here.");
404        }
405
406        @Override
407        public Result perform(String text, int cursor) throws IOException {
408            return Result.FINISH;
409        }
410
411    }
412
413    private final class NoSuchCommandCompletionTask implements CompletionTask {
414
415        @Override
416        public String description() {
417            throw new UnsupportedOperationException("Should not get here.");
418        }
419
420        @Override
421        public Result perform(String text, int cursor) throws IOException {
422            in.println();
423            in.println(repl.getResourceString("jshell.console.no.such.command"));
424            in.println();
425            return Result.SKIP;
426        }
427
428    }
429
430    private final class OrdinaryCompletionTask implements CompletionTask {
431        private final List<Suggestion> suggestions;
432        private final int anchor;
433        private final boolean cont;
434        private final boolean smart;
435
436        public OrdinaryCompletionTask(List<Suggestion> suggestions,
437                                      int anchor,
438                                      boolean cont,
439                                      boolean smart) {
440            this.suggestions = suggestions;
441            this.anchor = anchor;
442            this.cont = cont;
443            this.smart = smart;
444        }
445
446        @Override
447        public String description() {
448            throw new UnsupportedOperationException("Should not get here.");
449        }
450
451        @Override
452        public Result perform(String text, int cursor) throws IOException {
453            List<CharSequence> toShow;
454
455            if (smart) {
456                toShow =
457                    suggestions.stream()
458                               .filter(Suggestion::matchesType)
459                               .map(Suggestion::continuation)
460                               .distinct()
461                               .collect(Collectors.toList());
462            } else {
463                toShow =
464                    suggestions.stream()
465                               .map(Suggestion::continuation)
466                               .distinct()
467                               .collect(Collectors.toList());
468            }
469
470            if (toShow.isEmpty()) {
471                return Result.CONTINUE;
472            }
473
474            Optional<String> prefix =
475                    suggestions.stream()
476                               .map(Suggestion::continuation)
477                               .reduce(ConsoleIOContext::commonPrefix);
478
479            String prefixStr = prefix.orElse("").substring(cursor - anchor);
480            in.putString(prefixStr);
481
482            boolean showItems = toShow.size() > 1 || smart;
483
484            if (showItems) {
485                in.println();
486                in.printColumns(toShow);
487            }
488
489            if (!prefixStr.isEmpty())
490                return showItems ? Result.SKIP : Result.SKIP_NOREPAINT;
491
492            return cont ? Result.CONTINUE : Result.FINISH;
493        }
494
495    }
496
497    private final class AllSuggestionsCompletionTask implements CompletionTask {
498        private final List<Suggestion> suggestions;
499        private final int anchor;
500
501        public AllSuggestionsCompletionTask(List<Suggestion> suggestions,
502                                            int anchor) {
503            this.suggestions = suggestions;
504            this.anchor = anchor;
505        }
506
507        @Override
508        public String description() {
509            if (suggestions.size() <= in.getAutoprintThreshold()) {
510                return repl.getResourceString("jshell.console.completion.all.completions");
511            } else {
512                return repl.messageFormat("jshell.console.completion.all.completions.number", suggestions.size());
513            }
514        }
515
516        @Override
517        public Result perform(String text, int cursor) throws IOException {
518            List<String> candidates =
519                    suggestions.stream()
520                               .map(Suggestion::continuation)
521                               .distinct()
522                               .collect(Collectors.toList());
523
524            Optional<String> prefix =
525                    candidates.stream()
526                              .reduce(ConsoleIOContext::commonPrefix);
527
528            String prefixStr = prefix.map(str -> str.substring(cursor - anchor)).orElse("");
529            in.putString(prefixStr);
530            if (candidates.size() > 1) {
531                in.println();
532                in.printColumns(candidates);
533            }
534            return suggestions.isEmpty() ? Result.NO_DATA : Result.FINISH;
535        }
536
537    }
538
539    private final class CommandSynopsisTask implements CompletionTask {
540
541        private final List<String> synopsis;
542
543        public CommandSynopsisTask(List<String> synposis) {
544            this.synopsis = synposis;
545        }
546
547        @Override
548        public String description() {
549            return repl.getResourceString("jshell.console.see.synopsis");
550        }
551
552        @Override
553        public Result perform(String text, int cursor) throws IOException {
554            try {
555                in.println();
556                in.println(synopsis.stream()
557                                   .map(l -> l.replaceAll("\n", LINE_SEPARATOR))
558                                   .collect(Collectors.joining(LINE_SEPARATORS2)));
559            } catch (IOException ex) {
560                throw new IllegalStateException(ex);
561            }
562            return Result.FINISH;
563        }
564
565    }
566
567    private final class CommandFullDocumentationTask implements CompletionTask {
568
569        private final List<CompletionTask> todo;
570
571        public CommandFullDocumentationTask(List<CompletionTask> todo) {
572            this.todo = todo;
573        }
574
575        @Override
576        public String description() {
577            return repl.getResourceString("jshell.console.see.full.documentation");
578        }
579
580        @Override
581        public Result perform(String text, int cursor) throws IOException {
582            List<String> fullDoc = repl.commandDocumentation(text, cursor, false);
583            return doPrintFullDocumentation(todo, fullDoc, true);
584        }
585
586    }
587
588    private final class ExpressionSignaturesTask implements CompletionTask {
589
590        private final List<String> doc;
591
592        public ExpressionSignaturesTask(List<String> doc) {
593            this.doc = doc;
594        }
595
596        @Override
597        public String description() {
598            throw new UnsupportedOperationException("Should not get here.");
599        }
600
601        @Override
602        public Result perform(String text, int cursor) throws IOException {
603            in.println();
604            in.println(repl.getResourceString("jshell.console.completion.current.signatures"));
605            in.println(doc.stream().collect(Collectors.joining(LINE_SEPARATOR)));
606            return Result.FINISH;
607        }
608
609    }
610
611    private final class ExpressionJavadocTask implements CompletionTask {
612
613        private final List<CompletionTask> todo;
614
615        public ExpressionJavadocTask(List<CompletionTask> todo) {
616            this.todo = todo;
617        }
618
619        @Override
620        public String description() {
621            return repl.getResourceString("jshell.console.see.documentation");
622        }
623
624        @Override
625        public Result perform(String text, int cursor) throws IOException {
626            //schedule showing javadoc:
627            Terminal term = in.getTerminal();
628            JavadocFormatter formatter = new JavadocFormatter(term.getWidth(),
629                                                              term.isAnsiSupported());
630            Function<Documentation, String> convertor = d -> formatter.formatJavadoc(d.signature(), d.javadoc()) +
631                             (d.javadoc() == null ? repl.messageFormat("jshell.console.no.javadoc")
632                                                  : "");
633            List<String> doc = repl.analysis.documentation(prefix + text, cursor + prefix.length(), true)
634                                            .stream()
635                                            .map(convertor)
636                                            .collect(Collectors.toList());
637            return doPrintFullDocumentation(todo, doc, false);
638        }
639
640    }
641
642    @Override
643    public boolean terminalEditorRunning() {
644        Terminal terminal = in.getTerminal();
645        if (terminal instanceof SuspendableTerminal)
646            return ((SuspendableTerminal) terminal).isRaw();
647        return false;
648    }
649
650    @Override
651    public void suspend() {
652        Terminal terminal = in.getTerminal();
653        if (terminal instanceof SuspendableTerminal)
654            ((SuspendableTerminal) terminal).suspend();
655    }
656
657    @Override
658    public void resume() {
659        Terminal terminal = in.getTerminal();
660        if (terminal instanceof SuspendableTerminal)
661            ((SuspendableTerminal) terminal).resume();
662    }
663
664    @Override
665    public void beforeUserCode() {
666        synchronized (this) {
667            inputBytes = null;
668        }
669        input.setState(State.BUFFER);
670    }
671
672    @Override
673    public void afterUserCode() {
674        input.setState(State.WAIT);
675    }
676
677    @Override
678    public void replaceLastHistoryEntry(String source) {
679        history.fullHistoryReplace(source);
680    }
681
682    private static final long ESCAPE_TIMEOUT = 100;
683
684    private void fixes() {
685        try {
686            int c = in.readCharacter();
687
688            if (c == (-1)) {
689                return ;
690            }
691
692            for (FixComputer computer : FIX_COMPUTERS) {
693                if (computer.shortcut == c) {
694                    fixes(computer);
695                    return ;
696                }
697            }
698
699            readOutRemainingEscape(c);
700
701            in.beep();
702            in.println();
703            in.println(repl.getResourceString("jshell.fix.wrong.shortcut"));
704            in.redrawLine();
705            in.flush();
706        } catch (IOException ex) {
707            ex.printStackTrace();
708        }
709    }
710
711    private void readOutRemainingEscape(int c) throws IOException {
712        if (c == '\033') {
713            //escape, consume waiting input:
714            InputStream inp = in.getInput();
715
716            if (inp instanceof NonBlockingInputStream) {
717                NonBlockingInputStream nbis = (NonBlockingInputStream) inp;
718
719                while (nbis.isNonBlockingEnabled() && nbis.peek(ESCAPE_TIMEOUT) > 0) {
720                    in.readCharacter();
721                }
722            }
723        }
724    }
725
726    //compute possible options/Fixes based on the selected FixComputer, present them to the user,
727    //and perform the selected one:
728    private void fixes(FixComputer computer) {
729        String input = prefix + in.getCursorBuffer().toString();
730        int cursor = prefix.length() + in.getCursorBuffer().cursor;
731        FixResult candidates = computer.compute(repl, input, cursor);
732
733        try {
734            final boolean printError = candidates.error != null && !candidates.error.isEmpty();
735            if (printError) {
736                in.println(candidates.error);
737            }
738            if (candidates.fixes.isEmpty()) {
739                in.beep();
740                if (printError) {
741                    in.redrawLine();
742                    in.flush();
743                }
744            } else if (candidates.fixes.size() == 1 && !computer.showMenu) {
745                if (printError) {
746                    in.redrawLine();
747                    in.flush();
748                }
749                candidates.fixes.get(0).perform(in);
750            } else {
751                List<Fix> fixes = new ArrayList<>(candidates.fixes);
752                fixes.add(0, new Fix() {
753                    @Override
754                    public String displayName() {
755                        return repl.messageFormat("jshell.console.do.nothing");
756                    }
757
758                    @Override
759                    public void perform(ConsoleReader in) throws IOException {
760                        in.redrawLine();
761                    }
762                });
763
764                Map<Character, Fix> char2Fix = new HashMap<>();
765                in.println();
766                for (int i = 0; i < fixes.size(); i++) {
767                    Fix fix = fixes.get(i);
768                    char2Fix.put((char) ('0' + i), fix);
769                    in.println("" + i + ": " + fixes.get(i).displayName());
770                }
771                in.print(repl.messageFormat("jshell.console.choice"));
772                in.flush();
773                int read;
774
775                read = in.readCharacter();
776
777                Fix fix = char2Fix.get((char) read);
778
779                if (fix == null) {
780                    in.beep();
781                    fix = fixes.get(0);
782                }
783
784                in.println();
785
786                fix.perform(in);
787
788                in.flush();
789            }
790        } catch (IOException ex) {
791            throw new IllegalStateException(ex);
792        }
793    }
794
795    private byte[] inputBytes;
796    private int inputBytesPointer;
797
798    @Override
799    public synchronized int readUserInput() throws IOException {
800        while (inputBytes == null || inputBytes.length <= inputBytesPointer) {
801            boolean prevHandleUserInterrupt = in.getHandleUserInterrupt();
802            History prevHistory = in.getHistory();
803
804            try {
805                input.setState(State.WAIT);
806                in.setHandleUserInterrupt(true);
807                in.setHistory(userInputHistory);
808                inputBytes = (in.readLine("") + System.getProperty("line.separator")).getBytes();
809                inputBytesPointer = 0;
810            } catch (UserInterruptException ex) {
811                throw new InterruptedIOException();
812            } finally {
813                in.setHistory(prevHistory);
814                in.setHandleUserInterrupt(prevHandleUserInterrupt);
815                input.setState(State.BUFFER);
816            }
817        }
818        return inputBytes[inputBytesPointer++];
819    }
820
821    /**
822     * A possible action which the user can choose to perform.
823     */
824    public interface Fix {
825        /**
826         * A name that should be shown to the user.
827         */
828        public String displayName();
829        /**
830         * Perform the given action.
831         */
832        public void perform(ConsoleReader in) throws IOException;
833    }
834
835    /**
836     * A factory for {@link Fix}es.
837     */
838    public abstract static class FixComputer {
839        private final char shortcut;
840        private final boolean showMenu;
841
842        /**
843         * Construct a new FixComputer. {@code shortcut} defines the key which should trigger this FixComputer.
844         * If {@code showMenu} is {@code false}, and this computer returns exactly one {@code Fix},
845         * no options will be show to the user, and the given {@code Fix} will be performed.
846         */
847        public FixComputer(char shortcut, boolean showMenu) {
848            this.shortcut = shortcut;
849            this.showMenu = showMenu;
850        }
851
852        /**
853         * Compute possible actions for the given code.
854         */
855        public abstract FixResult compute(JShellTool repl, String code, int cursor);
856    }
857
858    /**
859     * A list of {@code Fix}es with a possible error that should be shown to the user.
860     */
861    public static class FixResult {
862        public final List<Fix> fixes;
863        public final String error;
864
865        public FixResult(List<Fix> fixes, String error) {
866            this.fixes = fixes;
867            this.error = error;
868        }
869    }
870
871    private static final FixComputer[] FIX_COMPUTERS = new FixComputer[] {
872        new FixComputer('v', false) { //compute "Introduce variable" Fix:
873            private void performToVar(ConsoleReader in, String type) throws IOException {
874                in.redrawLine();
875                in.setCursorPosition(0);
876                in.putString(type + "  = ");
877                in.setCursorPosition(in.getCursorBuffer().cursor - 3);
878                in.flush();
879            }
880
881            @Override
882            public FixResult compute(JShellTool repl, String code, int cursor) {
883                String type = repl.analysis.analyzeType(code, cursor);
884                if (type == null) {
885                    return new FixResult(Collections.emptyList(), null);
886                }
887                List<Fix> fixes = new ArrayList<>();
888                fixes.add(new Fix() {
889                    @Override
890                    public String displayName() {
891                        return repl.messageFormat("jshell.console.create.variable");
892                    }
893
894                    @Override
895                    public void perform(ConsoleReader in) throws IOException {
896                        performToVar(in, type);
897                    }
898                });
899                int idx = type.lastIndexOf(".");
900                if (idx > 0) {
901                    String stype = type.substring(idx + 1);
902                    QualifiedNames res = repl.analysis.listQualifiedNames(stype, stype.length());
903                    if (res.isUpToDate() && res.getNames().contains(type)
904                            && !res.isResolvable()) {
905                        fixes.add(new Fix() {
906                            @Override
907                            public String displayName() {
908                                return "import: " + type + ". " +
909                                        repl.messageFormat("jshell.console.create.variable");
910                            }
911
912                            @Override
913                            public void perform(ConsoleReader in) throws IOException {
914                                repl.processCompleteSource("import " + type + ";");
915                                in.println("Imported: " + type);
916                                performToVar(in, stype);
917                            }
918                        });
919                    }
920                }
921                return new FixResult(fixes, null);
922            }
923        },
924        new FixComputer('m', false) { //compute "Introduce method" Fix:
925            private void performToMethod(ConsoleReader in, String type, String code) throws IOException {
926                in.redrawLine();
927                if (!code.trim().endsWith(";")) {
928                    in.putString(";");
929                }
930                in.putString(" }");
931                in.setCursorPosition(0);
932                String afterCursor = type.equals("void")
933                        ? "() { "
934                        : "() { return ";
935                in.putString(type + " " + afterCursor);
936                // position the cursor where the method name should be entered (before parens)
937                in.setCursorPosition(in.getCursorBuffer().cursor - afterCursor.length());
938                in.flush();
939            }
940
941            private FixResult reject(JShellTool repl, String messageKey) {
942                return new FixResult(Collections.emptyList(), repl.messageFormat(messageKey));
943            }
944
945            @Override
946            public FixResult compute(JShellTool repl, String code, int cursor) {
947                final String codeToCursor = code.substring(0, cursor);
948                final String type;
949                final CompletionInfo ci = repl.analysis.analyzeCompletion(codeToCursor);
950                if (!ci.remaining().isEmpty()) {
951                    return reject(repl, "jshell.console.exprstmt");
952                }
953                switch (ci.completeness()) {
954                    case COMPLETE:
955                    case COMPLETE_WITH_SEMI:
956                    case CONSIDERED_INCOMPLETE:
957                        break;
958                    case EMPTY:
959                        return reject(repl, "jshell.console.empty");
960                    case DEFINITELY_INCOMPLETE:
961                    case UNKNOWN:
962                    default:
963                        return reject(repl, "jshell.console.erroneous");
964                }
965                List<Snippet> snl = repl.analysis.sourceToSnippets(ci.source());
966                if (snl.size() != 1) {
967                    return reject(repl, "jshell.console.erroneous");
968                }
969                Snippet sn = snl.get(0);
970                switch (sn.kind()) {
971                    case EXPRESSION:
972                        type = ((ExpressionSnippet) sn).typeName();
973                        break;
974                    case STATEMENT:
975                        type = "void";
976                        break;
977                    case VAR:
978                        if (sn.subKind() != SubKind.TEMP_VAR_EXPRESSION_SUBKIND) {
979                            // only valid var is an expression turned into a temp var
980                            return reject(repl, "jshell.console.exprstmt");
981                        }
982                        type = ((VarSnippet) sn).typeName();
983                        break;
984                    case IMPORT:
985                    case METHOD:
986                    case TYPE_DECL:
987                        return reject(repl, "jshell.console.exprstmt");
988                    case ERRONEOUS:
989                    default:
990                        return reject(repl, "jshell.console.erroneous");
991                }
992                List<Fix> fixes = new ArrayList<>();
993                fixes.add(new Fix() {
994                    @Override
995                    public String displayName() {
996                        return repl.messageFormat("jshell.console.create.method");
997                    }
998
999                    @Override
1000                    public void perform(ConsoleReader in) throws IOException {
1001                        performToMethod(in, type, codeToCursor);
1002                    }
1003                });
1004                int idx = type.lastIndexOf(".");
1005                if (idx > 0) {
1006                    String stype = type.substring(idx + 1);
1007                    QualifiedNames res = repl.analysis.listQualifiedNames(stype, stype.length());
1008                    if (res.isUpToDate() && res.getNames().contains(type)
1009                            && !res.isResolvable()) {
1010                        fixes.add(new Fix() {
1011                            @Override
1012                            public String displayName() {
1013                                return "import: " + type + ". " +
1014                                        repl.messageFormat("jshell.console.create.method");
1015                            }
1016
1017                            @Override
1018                            public void perform(ConsoleReader in) throws IOException {
1019                                repl.processCompleteSource("import " + type + ";");
1020                                in.println("Imported: " + type);
1021                                performToMethod(in, stype, codeToCursor);
1022                            }
1023                        });
1024                    }
1025                }
1026                return new FixResult(fixes, null);
1027            }
1028        },
1029        new FixComputer('i', true) { //compute "Add import" Fixes:
1030            @Override
1031            public FixResult compute(JShellTool repl, String code, int cursor) {
1032                QualifiedNames res = repl.analysis.listQualifiedNames(code, cursor);
1033                List<Fix> fixes = new ArrayList<>();
1034                for (String fqn : res.getNames()) {
1035                    fixes.add(new Fix() {
1036                        @Override
1037                        public String displayName() {
1038                            return "import: " + fqn;
1039                        }
1040
1041                        @Override
1042                        public void perform(ConsoleReader in) throws IOException {
1043                            repl.processCompleteSource("import " + fqn + ";");
1044                            in.println("Imported: " + fqn);
1045                            in.redrawLine();
1046                        }
1047                    });
1048                }
1049                if (res.isResolvable()) {
1050                    return new FixResult(Collections.emptyList(),
1051                            repl.messageFormat("jshell.console.resolvable"));
1052                } else {
1053                    String error = "";
1054                    if (fixes.isEmpty()) {
1055                        error = repl.messageFormat("jshell.console.no.candidate");
1056                    }
1057                    if (!res.isUpToDate()) {
1058                        error += repl.messageFormat("jshell.console.incomplete");
1059                    }
1060                    return new FixResult(fixes, error);
1061                }
1062            }
1063        }
1064    };
1065
1066    private static final class JShellUnixTerminal extends NoInterruptUnixTerminal implements SuspendableTerminal {
1067
1068        private final StopDetectingInputStream input;
1069
1070        public JShellUnixTerminal(StopDetectingInputStream input) throws Exception {
1071            this.input = input;
1072        }
1073
1074        @Override
1075        public boolean isRaw() {
1076            try {
1077                return getSettings().get("-a").contains("-icanon");
1078            } catch (IOException | InterruptedException ex) {
1079                return false;
1080            }
1081        }
1082
1083        @Override
1084        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
1085            return input.setInputStream(super.wrapInIfNeeded(in));
1086        }
1087
1088        @Override
1089        public void disableInterruptCharacter() {
1090        }
1091
1092        @Override
1093        public void enableInterruptCharacter() {
1094        }
1095
1096        @Override
1097        public void suspend() {
1098            try {
1099                getSettings().restore();
1100                super.disableInterruptCharacter();
1101            } catch (Exception ex) {
1102                throw new IllegalStateException(ex);
1103            }
1104        }
1105
1106        @Override
1107        public void resume() {
1108            try {
1109                init();
1110            } catch (Exception ex) {
1111                throw new IllegalStateException(ex);
1112            }
1113        }
1114
1115    }
1116
1117    private static final class JShellWindowsTerminal extends WindowsTerminal implements SuspendableTerminal {
1118
1119        private final StopDetectingInputStream input;
1120
1121        public JShellWindowsTerminal(StopDetectingInputStream input) throws Exception {
1122            this.input = input;
1123        }
1124
1125        @Override
1126        public void init() throws Exception {
1127            super.init();
1128            setAnsiSupported(false);
1129        }
1130
1131        @Override
1132        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
1133            return input.setInputStream(super.wrapInIfNeeded(in));
1134        }
1135
1136        @Override
1137        public void suspend() {
1138            try {
1139                restore();
1140                setConsoleMode(getConsoleMode() & ~ConsoleMode.ENABLE_PROCESSED_INPUT.code);
1141            } catch (Exception ex) {
1142                throw new IllegalStateException(ex);
1143            }
1144        }
1145
1146        @Override
1147        public void resume() {
1148            try {
1149                restore();
1150                init();
1151            } catch (Exception ex) {
1152                throw new IllegalStateException(ex);
1153            }
1154        }
1155
1156        @Override
1157        public boolean isRaw() {
1158            return (getConsoleMode() & ConsoleMode.ENABLE_LINE_INPUT.code) == 0;
1159        }
1160
1161    }
1162
1163    private static final class TestTerminal extends TerminalSupport {
1164
1165        private final StopDetectingInputStream input;
1166        private final int height;
1167
1168        public TestTerminal(StopDetectingInputStream input) throws Exception {
1169            super(true);
1170            setAnsiSupported(false);
1171            setEchoEnabled(false);
1172            this.input = input;
1173            int h = DEFAULT_HEIGHT;
1174            try {
1175                String hp = System.getProperty("test.terminal.height");
1176                if (hp != null && !hp.isEmpty()) {
1177                    h = Integer.parseInt(hp);
1178                }
1179            } catch (Throwable ex) {
1180                // ignore
1181            }
1182            this.height = h;
1183        }
1184
1185        @Override
1186        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
1187            return input.setInputStream(super.wrapInIfNeeded(in));
1188        }
1189
1190        @Override
1191        public int getHeight() {
1192            return height;
1193        }
1194
1195    }
1196
1197    private interface SuspendableTerminal {
1198        public void suspend();
1199        public void resume();
1200        public boolean isRaw();
1201    }
1202
1203    private static final class CheckCompletionKeyMap extends KeyMap {
1204
1205        private final KeyMap del;
1206        private final List<CompletionTask> completionTODO;
1207
1208        public CheckCompletionKeyMap(KeyMap del, List<CompletionTask> completionTODO) {
1209            super(del.getName(), del.isViKeyMap());
1210            this.del = del;
1211            this.completionTODO = completionTODO;
1212        }
1213
1214        @Override
1215        public void bind(CharSequence keySeq, Object function) {
1216            del.bind(keySeq, function);
1217        }
1218
1219        @Override
1220        public void bindIfNotBound(CharSequence keySeq, Object function) {
1221            del.bindIfNotBound(keySeq, function);
1222        }
1223
1224        @Override
1225        public void from(KeyMap other) {
1226            del.from(other);
1227        }
1228
1229        @Override
1230        public Object getAnotherKey() {
1231            return del.getAnotherKey();
1232        }
1233
1234        @Override
1235        public Object getBound(CharSequence keySeq) {
1236            Object res = del.getBound(keySeq);
1237
1238            if (res != Operation.COMPLETE) {
1239                completionTODO.clear();
1240            }
1241
1242            return res;
1243        }
1244
1245        @Override
1246        public void setBlinkMatchingParen(boolean on) {
1247            del.setBlinkMatchingParen(on);
1248        }
1249
1250        @Override
1251        public String toString() {
1252            return "check: " + del.toString();
1253        }
1254    }
1255    }
1256