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