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