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