ConsoleIOContext.java revision 3982:162b521af7bb
1/*
2 * Copyright (c) 2015, 2016, 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.io.UncheckedIOException;
37import java.util.ArrayList;
38import java.util.Arrays;
39import java.util.Collection;
40import java.util.Collections;
41import java.util.HashMap;
42import java.util.Iterator;
43import java.util.List;
44import java.util.Locale;
45import java.util.Map;
46import java.util.Objects;
47import java.util.Optional;
48import java.util.concurrent.atomic.AtomicBoolean;
49import java.util.function.Function;
50import java.util.stream.Collectors;
51import java.util.stream.Stream;
52
53import jdk.internal.shellsupport.doc.JavadocFormatter;
54import jdk.internal.jline.NoInterruptUnixTerminal;
55import jdk.internal.jline.Terminal;
56import jdk.internal.jline.TerminalFactory;
57import jdk.internal.jline.TerminalSupport;
58import jdk.internal.jline.WindowsTerminal;
59import jdk.internal.jline.console.ConsoleReader;
60import jdk.internal.jline.console.KeyMap;
61import jdk.internal.jline.console.Operation;
62import jdk.internal.jline.console.UserInterruptException;
63import jdk.internal.jline.console.completer.Completer;
64import jdk.internal.jline.console.history.History;
65import jdk.internal.jline.console.history.MemoryHistory;
66import jdk.internal.jline.extra.EditingHistory;
67import jdk.internal.jshell.tool.StopDetectingInputStream.State;
68import jdk.internal.misc.Signal;
69import jdk.internal.misc.Signal.Handler;
70
71class ConsoleIOContext extends IOContext {
72
73    private static final String HISTORY_LINE_PREFIX = "HISTORY_LINE_";
74
75    final JShellTool repl;
76    final StopDetectingInputStream input;
77    final ConsoleReader in;
78    final EditingHistory history;
79    final MemoryHistory userInputHistory = new MemoryHistory();
80
81    String prefix = "";
82
83    ConsoleIOContext(JShellTool repl, InputStream cmdin, PrintStream cmdout) throws Exception {
84        this.repl = repl;
85        this.input = new StopDetectingInputStream(() -> repl.state.stop(), ex -> repl.hard("Error on input: %s", ex));
86        Terminal term;
87        if (System.getProperty("test.jdk") != null) {
88            term = new TestTerminal(input);
89        } else if (System.getProperty("os.name").toLowerCase(Locale.US).contains(TerminalFactory.WINDOWS)) {
90            term = new JShellWindowsTerminal(input);
91        } else {
92            term = new JShellUnixTerminal(input);
93        }
94        term.init();
95        AtomicBoolean allowSmart = new AtomicBoolean();
96        in = new ConsoleReader(cmdin, cmdout, term) {
97            @Override public KeyMap getKeys() {
98                return new CheckCompletionKeyMap(super.getKeys(), allowSmart);
99            }
100        };
101        in.setExpandEvents(false);
102        in.setHandleUserInterrupt(true);
103        List<String> persistenHistory = Stream.of(repl.prefs.keys())
104                                              .filter(key -> key.startsWith(HISTORY_LINE_PREFIX))
105                                              .sorted()
106                                              .map(key -> repl.prefs.get(key))
107                                              .collect(Collectors.toList());
108        in.setHistory(history = new EditingHistory(in, persistenHistory) {
109            @Override protected boolean isComplete(CharSequence input) {
110                return repl.analysis.analyzeCompletion(input.toString()).completeness().isComplete();
111            }
112        });
113        in.setBellEnabled(true);
114        in.setCopyPasteDetection(true);
115        in.addCompleter(new Completer() {
116            @Override public int complete(String test, int cursor, List<CharSequence> result) {
117                int[] anchor = new int[] {-1};
118                List<Suggestion> suggestions;
119                if (prefix.isEmpty() && test.trim().startsWith("/")) {
120                    suggestions = repl.commandCompletionSuggestions(test, cursor, anchor);
121                } else {
122                    int prefixLength = prefix.length();
123                    suggestions = repl.analysis.completionSuggestions(prefix + test, cursor + prefixLength, anchor);
124                    anchor[0] -= prefixLength;
125                }
126                boolean smart = allowSmart.get() &&
127                                suggestions.stream()
128                                           .anyMatch(Suggestion::matchesType);
129
130                allowSmart.set(!allowSmart.get());
131
132                suggestions.stream()
133                           .filter(s -> !smart || s.matchesType())
134                           .map(Suggestion::continuation)
135                           .forEach(result::add);
136
137                boolean onlySmart = suggestions.stream()
138                                               .allMatch(Suggestion::matchesType);
139
140                if (smart && !onlySmart) {
141                    Optional<String> prefix =
142                            suggestions.stream()
143                                       .map(Suggestion::continuation)
144                                       .reduce(ConsoleIOContext::commonPrefix);
145
146                    String prefixStr = prefix.orElse("").substring(cursor - anchor[0]);
147                    try {
148                        in.putString(prefixStr);
149                        cursor += prefixStr.length();
150                    } catch (IOException ex) {
151                        throw new IllegalStateException(ex);
152                    }
153                    result.add(repl.messageFormat("jshell.console.see.more"));
154                    return cursor; //anchor should not be used.
155                }
156
157                if (result.isEmpty()) {
158                    try {
159                        //provide "empty completion" feedback
160                        //XXX: this only works correctly when there is only one Completer:
161                        in.beep();
162                    } catch (IOException ex) {
163                        throw new UncheckedIOException(ex);
164                    }
165                }
166
167                return anchor[0];
168            }
169        });
170        bind(DOCUMENTATION_SHORTCUT, (Runnable) () -> documentation(repl));
171        for (FixComputer computer : FIX_COMPUTERS) {
172            for (String shortcuts : SHORTCUT_FIXES) {
173                bind(shortcuts + computer.shortcut, (Runnable) () -> fixes(computer));
174            }
175        }
176        try {
177            Signal.handle(new Signal("CONT"), new Handler() {
178                @Override public void handle(Signal sig) {
179                    try {
180                        in.getTerminal().reset();
181                        in.redrawLine();
182                        in.flush();
183                    } catch (Exception ex) {
184                        ex.printStackTrace();
185                    }
186                }
187            });
188        } catch (IllegalArgumentException ignored) {
189            //the CONT signal does not exist on this platform
190        }
191    }
192
193    @Override
194    public String readLine(String prompt, String prefix) throws IOException, InputInterruptedException {
195        this.prefix = prefix;
196        try {
197            return in.readLine(prompt);
198        } catch (UserInterruptException ex) {
199            throw (InputInterruptedException) new InputInterruptedException().initCause(ex);
200        }
201    }
202
203    @Override
204    public boolean interactiveOutput() {
205        return true;
206    }
207
208    @Override
209    public Iterable<String> currentSessionHistory() {
210        return history.currentSessionEntries();
211    }
212
213    @Override
214    public void close() throws IOException {
215        //save history:
216        for (String key : repl.prefs.keys()) {
217            if (key.startsWith(HISTORY_LINE_PREFIX)) {
218                repl.prefs.remove(key);
219            }
220        }
221        Collection<? extends String> savedHistory = history.save();
222        if (!savedHistory.isEmpty()) {
223            int len = (int) Math.ceil(Math.log10(savedHistory.size()+1));
224            String format = HISTORY_LINE_PREFIX + "%0" + len + "d";
225            int index = 0;
226            for (String historyLine : savedHistory) {
227                repl.prefs.put(String.format(format, index++), historyLine);
228            }
229        }
230        repl.prefs.flush();
231        in.shutdown();
232        try {
233            in.getTerminal().restore();
234        } catch (Exception ex) {
235            throw new IOException(ex);
236        }
237        input.shutdown();
238    }
239
240    private void bind(String shortcut, Object action) {
241        KeyMap km = in.getKeys();
242        for (int i = 0; i < shortcut.length(); i++) {
243            Object value = km.getBound(Character.toString(shortcut.charAt(i)));
244            if (value instanceof KeyMap) {
245                km = (KeyMap) value;
246            } else {
247                km.bind(shortcut.substring(i), action);
248            }
249        }
250    }
251
252    private static final String DOCUMENTATION_SHORTCUT = "\033\133\132"; //Shift-TAB
253    private static final String[] SHORTCUT_FIXES = {
254        "\033\015", //Alt-Enter (Linux)
255        "\033\012", //Alt-Enter (Linux)
256        "\033\133\061\067\176", //F6/Alt-F1 (Mac)
257        "\u001BO3P" //Alt-F1 (Linux)
258    };
259
260    private String lastDocumentationBuffer;
261    private int lastDocumentationCursor = (-1);
262
263    private void documentation(JShellTool repl) {
264        String buffer = in.getCursorBuffer().buffer.toString();
265        int cursor = in.getCursorBuffer().cursor;
266        boolean firstInvocation = !buffer.equals(lastDocumentationBuffer) || cursor != lastDocumentationCursor;
267        lastDocumentationBuffer = buffer;
268        lastDocumentationCursor = cursor;
269        List<String> doc;
270        String seeMore;
271        Terminal term = in.getTerminal();
272        if (prefix.isEmpty() && buffer.trim().startsWith("/")) {
273            doc = Arrays.asList(repl.commandDocumentation(buffer, cursor, firstInvocation));
274            seeMore = "jshell.console.see.help";
275        } else {
276            JavadocFormatter formatter = new JavadocFormatter(term.getWidth(),
277                                                              term.isAnsiSupported());
278            Function<Documentation, String> convertor;
279            if (firstInvocation) {
280                convertor = Documentation::signature;
281            } else {
282                convertor = d -> formatter.formatJavadoc(d.signature(), d.javadoc()) +
283                                 (d.javadoc() == null ? repl.messageFormat("jshell.console.no.javadoc")
284                                                      : "");
285            }
286            doc = repl.analysis.documentation(prefix + buffer, cursor + prefix.length(), !firstInvocation)
287                               .stream()
288                               .map(convertor)
289                               .collect(Collectors.toList());
290            seeMore = "jshell.console.see.javadoc";
291        }
292
293        try {
294            if (doc != null && !doc.isEmpty()) {
295                if (firstInvocation) {
296                    in.println();
297                    in.println(doc.stream().collect(Collectors.joining("\n")));
298                    in.println(repl.messageFormat(seeMore));
299                    in.redrawLine();
300                    in.flush();
301                } else {
302                    in.println();
303
304                    int height = term.getHeight();
305                    String lastNote = "";
306
307                    PRINT_DOC: for (Iterator<String> docIt = doc.iterator(); docIt.hasNext(); ) {
308                        String currentDoc = docIt.next();
309                        String[] lines = currentDoc.split("\n");
310                        int firstLine = 0;
311
312                        PRINT_PAGE: while (true) {
313                            in.print(lastNote.replaceAll(".", " ") + ConsoleReader.RESET_LINE);
314
315                            int toPrint = height - 1;
316
317                            while (toPrint > 0 && firstLine < lines.length) {
318                                in.println(lines[firstLine++]);
319                                toPrint--;
320                            }
321
322                            if (firstLine >= lines.length) {
323                                break;
324                            }
325
326                            lastNote = repl.getResourceString("jshell.console.see.next.page");
327                            in.print(lastNote + ConsoleReader.RESET_LINE);
328                            in.flush();
329
330                            while (true) {
331                                int r = in.readCharacter();
332
333                                switch (r) {
334                                    case ' ': continue PRINT_PAGE;
335                                    case 'q':
336                                    case 3:
337                                        break PRINT_DOC;
338                                    default:
339                                        in.beep();
340                                        break;
341                                }
342                            }
343                        }
344
345                        if (docIt.hasNext()) {
346                            lastNote = repl.getResourceString("jshell.console.see.next.javadoc");
347                            in.print(lastNote + ConsoleReader.RESET_LINE);
348                            in.flush();
349
350                            while (true) {
351                                int r = in.readCharacter();
352
353                                switch (r) {
354                                    case ' ': continue PRINT_DOC;
355                                    case 'q':
356                                    case 3:
357                                        break PRINT_DOC;
358                                    default:
359                                        in.beep();
360                                        break;
361                                }
362                            }
363                        }
364                    }
365                    //clear the "press space" line:
366                    in.getCursorBuffer().buffer.replace(0, buffer.length(), lastNote);
367                    in.getCursorBuffer().cursor = 0;
368                    in.killLine();
369                    in.getCursorBuffer().buffer.append(buffer);
370                    in.getCursorBuffer().cursor = cursor;
371                    in.redrawLine();
372                    in.flush();
373                }
374            } else {
375                in.beep();
376            }
377        } catch (IOException ex) {
378            throw new IllegalStateException(ex);
379        }
380    }
381
382    private static String commonPrefix(String str1, String str2) {
383        for (int i = 0; i < str2.length(); i++) {
384            if (!str1.startsWith(str2.substring(0, i + 1))) {
385                return str2.substring(0, i);
386            }
387        }
388
389        return str2;
390    }
391
392    @Override
393    public boolean terminalEditorRunning() {
394        Terminal terminal = in.getTerminal();
395        if (terminal instanceof SuspendableTerminal)
396            return ((SuspendableTerminal) terminal).isRaw();
397        return false;
398    }
399
400    @Override
401    public void suspend() {
402        Terminal terminal = in.getTerminal();
403        if (terminal instanceof SuspendableTerminal)
404            ((SuspendableTerminal) terminal).suspend();
405    }
406
407    @Override
408    public void resume() {
409        Terminal terminal = in.getTerminal();
410        if (terminal instanceof SuspendableTerminal)
411            ((SuspendableTerminal) terminal).resume();
412    }
413
414    @Override
415    public void beforeUserCode() {
416        synchronized (this) {
417            inputBytes = null;
418        }
419        input.setState(State.BUFFER);
420    }
421
422    @Override
423    public void afterUserCode() {
424        input.setState(State.WAIT);
425    }
426
427    @Override
428    public void replaceLastHistoryEntry(String source) {
429        history.fullHistoryReplace(source);
430    }
431
432    //compute possible options/Fixes based on the selected FixComputer, present them to the user,
433    //and perform the selected one:
434    private void fixes(FixComputer computer) {
435        String input = prefix + in.getCursorBuffer().toString();
436        int cursor = prefix.length() + in.getCursorBuffer().cursor;
437        FixResult candidates = computer.compute(repl, input, cursor);
438
439        try {
440            final boolean printError = candidates.error != null && !candidates.error.isEmpty();
441            if (printError) {
442                in.println(candidates.error);
443            }
444            if (candidates.fixes.isEmpty()) {
445                in.beep();
446                if (printError) {
447                    in.redrawLine();
448                    in.flush();
449                }
450            } else if (candidates.fixes.size() == 1 && !computer.showMenu) {
451                if (printError) {
452                    in.redrawLine();
453                    in.flush();
454                }
455                candidates.fixes.get(0).perform(in);
456            } else {
457                List<Fix> fixes = new ArrayList<>(candidates.fixes);
458                fixes.add(0, new Fix() {
459                    @Override
460                    public String displayName() {
461                        return repl.messageFormat("jshell.console.do.nothing");
462                    }
463
464                    @Override
465                    public void perform(ConsoleReader in) throws IOException {
466                        in.redrawLine();
467                    }
468                });
469
470                Map<Character, Fix> char2Fix = new HashMap<>();
471                in.println();
472                for (int i = 0; i < fixes.size(); i++) {
473                    Fix fix = fixes.get(i);
474                    char2Fix.put((char) ('0' + i), fix);
475                    in.println("" + i + ": " + fixes.get(i).displayName());
476                }
477                in.print(repl.messageFormat("jshell.console.choice"));
478                in.flush();
479                int read;
480
481                read = in.readCharacter();
482
483                Fix fix = char2Fix.get((char) read);
484
485                if (fix == null) {
486                    in.beep();
487                    fix = fixes.get(0);
488                }
489
490                in.println();
491
492                fix.perform(in);
493
494                in.flush();
495            }
496        } catch (IOException ex) {
497            ex.printStackTrace();
498        }
499    }
500
501    private byte[] inputBytes;
502    private int inputBytesPointer;
503
504    @Override
505    public synchronized int readUserInput() throws IOException {
506        while (inputBytes == null || inputBytes.length <= inputBytesPointer) {
507            boolean prevHandleUserInterrupt = in.getHandleUserInterrupt();
508            History prevHistory = in.getHistory();
509
510            try {
511                input.setState(State.WAIT);
512                in.setHandleUserInterrupt(true);
513                in.setHistory(userInputHistory);
514                inputBytes = (in.readLine("") + System.getProperty("line.separator")).getBytes();
515                inputBytesPointer = 0;
516            } catch (UserInterruptException ex) {
517                throw new InterruptedIOException();
518            } finally {
519                in.setHistory(prevHistory);
520                in.setHandleUserInterrupt(prevHandleUserInterrupt);
521                input.setState(State.BUFFER);
522            }
523        }
524        return inputBytes[inputBytesPointer++];
525    }
526
527    /**
528     * A possible action which the user can choose to perform.
529     */
530    public interface Fix {
531        /**
532         * A name that should be shown to the user.
533         */
534        public String displayName();
535        /**
536         * Perform the given action.
537         */
538        public void perform(ConsoleReader in) throws IOException;
539    }
540
541    /**
542     * A factory for {@link Fix}es.
543     */
544    public abstract static class FixComputer {
545        private final char shortcut;
546        private final boolean showMenu;
547
548        /**
549         * Construct a new FixComputer. {@code shortcut} defines the key which should trigger this FixComputer.
550         * If {@code showMenu} is {@code false}, and this computer returns exactly one {@code Fix},
551         * no options will be show to the user, and the given {@code Fix} will be performed.
552         */
553        public FixComputer(char shortcut, boolean showMenu) {
554            this.shortcut = shortcut;
555            this.showMenu = showMenu;
556        }
557
558        /**
559         * Compute possible actions for the given code.
560         */
561        public abstract FixResult compute(JShellTool repl, String code, int cursor);
562    }
563
564    /**
565     * A list of {@code Fix}es with a possible error that should be shown to the user.
566     */
567    public static class FixResult {
568        public final List<Fix> fixes;
569        public final String error;
570
571        public FixResult(List<Fix> fixes, String error) {
572            this.fixes = fixes;
573            this.error = error;
574        }
575    }
576
577    private static final FixComputer[] FIX_COMPUTERS = new FixComputer[] {
578        new FixComputer('v', false) { //compute "Introduce variable" Fix:
579            private void performToVar(ConsoleReader in, String type) throws IOException {
580                in.redrawLine();
581                in.setCursorPosition(0);
582                in.putString(type + "  = ");
583                in.setCursorPosition(in.getCursorBuffer().cursor - 3);
584                in.flush();
585            }
586
587            @Override
588            public FixResult compute(JShellTool repl, String code, int cursor) {
589                String type = repl.analysis.analyzeType(code, cursor);
590                if (type == null) {
591                    return new FixResult(Collections.emptyList(), null);
592                }
593                List<Fix> fixes = new ArrayList<>();
594                fixes.add(new Fix() {
595                    @Override
596                    public String displayName() {
597                        return repl.messageFormat("jshell.console.create.variable");
598                    }
599
600                    @Override
601                    public void perform(ConsoleReader in) throws IOException {
602                        performToVar(in, type);
603                    }
604                });
605                int idx = type.lastIndexOf(".");
606                if (idx > 0) {
607                    String stype = type.substring(idx + 1);
608                    QualifiedNames res = repl.analysis.listQualifiedNames(stype, stype.length());
609                    if (res.isUpToDate() && res.getNames().contains(type)
610                            && !res.isResolvable()) {
611                        fixes.add(new Fix() {
612                            @Override
613                            public String displayName() {
614                                return "import: " + type + ". " +
615                                        repl.messageFormat("jshell.console.create.variable");
616                            }
617
618                            @Override
619                            public void perform(ConsoleReader in) throws IOException {
620                                repl.state.eval("import " + type + ";");
621                                in.println("Imported: " + type);
622                                performToVar(in, stype);
623                            }
624                        });
625                    }
626                }
627                return new FixResult(fixes, null);
628            }
629        },
630        new FixComputer('i', true) { //compute "Add import" Fixes:
631            @Override
632            public FixResult compute(JShellTool repl, String code, int cursor) {
633                QualifiedNames res = repl.analysis.listQualifiedNames(code, cursor);
634                List<Fix> fixes = new ArrayList<>();
635                for (String fqn : res.getNames()) {
636                    fixes.add(new Fix() {
637                        @Override
638                        public String displayName() {
639                            return "import: " + fqn;
640                        }
641
642                        @Override
643                        public void perform(ConsoleReader in) throws IOException {
644                            repl.state.eval("import " + fqn + ";");
645                            in.println("Imported: " + fqn);
646                            in.redrawLine();
647                        }
648                    });
649                }
650                if (res.isResolvable()) {
651                    return new FixResult(Collections.emptyList(),
652                            repl.messageFormat("jshell.console.resolvable"));
653                } else {
654                    String error = "";
655                    if (fixes.isEmpty()) {
656                        error = repl.messageFormat("jshell.console.no.candidate");
657                    }
658                    if (!res.isUpToDate()) {
659                        error += repl.messageFormat("jshell.console.incomplete");
660                    }
661                    return new FixResult(fixes, error);
662                }
663            }
664        }
665    };
666
667    private static final class JShellUnixTerminal extends NoInterruptUnixTerminal implements SuspendableTerminal {
668
669        private final StopDetectingInputStream input;
670
671        public JShellUnixTerminal(StopDetectingInputStream input) throws Exception {
672            this.input = input;
673        }
674
675        public boolean isRaw() {
676            try {
677                return getSettings().get("-a").contains("-icanon");
678            } catch (IOException | InterruptedException ex) {
679                return false;
680            }
681        }
682
683        @Override
684        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
685            return input.setInputStream(super.wrapInIfNeeded(in));
686        }
687
688        @Override
689        public void disableInterruptCharacter() {
690        }
691
692        @Override
693        public void enableInterruptCharacter() {
694        }
695
696        @Override
697        public void suspend() {
698            try {
699                getSettings().restore();
700                super.disableInterruptCharacter();
701            } catch (Exception ex) {
702                throw new IllegalStateException(ex);
703            }
704        }
705
706        @Override
707        public void resume() {
708            try {
709                init();
710            } catch (Exception ex) {
711                throw new IllegalStateException(ex);
712            }
713        }
714
715    }
716
717    private static final class JShellWindowsTerminal extends WindowsTerminal implements SuspendableTerminal {
718
719        private final StopDetectingInputStream input;
720
721        public JShellWindowsTerminal(StopDetectingInputStream input) throws Exception {
722            this.input = input;
723        }
724
725        @Override
726        public void init() throws Exception {
727            super.init();
728            setAnsiSupported(false);
729        }
730
731        @Override
732        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
733            return input.setInputStream(super.wrapInIfNeeded(in));
734        }
735
736        @Override
737        public void suspend() {
738            try {
739                restore();
740                setConsoleMode(getConsoleMode() & ~ConsoleMode.ENABLE_PROCESSED_INPUT.code);
741            } catch (Exception ex) {
742                throw new IllegalStateException(ex);
743            }
744        }
745
746        @Override
747        public void resume() {
748            try {
749                restore();
750                init();
751            } catch (Exception ex) {
752                throw new IllegalStateException(ex);
753            }
754        }
755
756        @Override
757        public boolean isRaw() {
758            return (getConsoleMode() & ConsoleMode.ENABLE_LINE_INPUT.code) == 0;
759        }
760
761    }
762
763    private static final class TestTerminal extends TerminalSupport {
764
765        private final StopDetectingInputStream input;
766
767        public TestTerminal(StopDetectingInputStream input) throws Exception {
768            super(true);
769            setAnsiSupported(false);
770            setEchoEnabled(true);
771            this.input = input;
772        }
773
774        @Override
775        public InputStream wrapInIfNeeded(InputStream in) throws IOException {
776            return input.setInputStream(super.wrapInIfNeeded(in));
777        }
778
779    }
780
781    private interface SuspendableTerminal {
782        public void suspend();
783        public void resume();
784        public boolean isRaw();
785    }
786
787    private static final class CheckCompletionKeyMap extends KeyMap {
788
789        private final KeyMap del;
790        private final AtomicBoolean allowSmart;
791
792        public CheckCompletionKeyMap(KeyMap del, AtomicBoolean allowSmart) {
793            super(del.getName(), del.isViKeyMap());
794            this.del = del;
795            this.allowSmart = allowSmart;
796        }
797
798        @Override
799        public void bind(CharSequence keySeq, Object function) {
800            del.bind(keySeq, function);
801        }
802
803        @Override
804        public void bindIfNotBound(CharSequence keySeq, Object function) {
805            del.bindIfNotBound(keySeq, function);
806        }
807
808        @Override
809        public void from(KeyMap other) {
810            del.from(other);
811        }
812
813        @Override
814        public Object getAnotherKey() {
815            return del.getAnotherKey();
816        }
817
818        @Override
819        public Object getBound(CharSequence keySeq) {
820            Object res = del.getBound(keySeq);
821
822            if (res != Operation.COMPLETE) {
823                allowSmart.set(true);
824            }
825
826            return res;
827        }
828
829        @Override
830        public void setBlinkMatchingParen(boolean on) {
831            del.setBlinkMatchingParen(on);
832        }
833
834        @Override
835        public String toString() {
836            return "check: " + del.toString();
837        }
838    }
839}
840