1 /* 2 * Copyright (c) 2014, 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 26 package jdk.internal.jshell.tool; 27 28 import java.io.BufferedWriter; 29 import java.io.ByteArrayInputStream; 30 import java.io.File; 31 import java.io.FileNotFoundException; 32 import java.io.FileReader; 33 import java.io.IOException; 34 import java.io.InputStream; 35 import java.io.PrintStream; 36 import java.io.Reader; 37 import java.io.StringReader; 38 import java.nio.charset.Charset; 39 import java.nio.file.AccessDeniedException; 40 import java.nio.file.FileSystems; 41 import java.nio.file.Files; 42 import java.nio.file.NoSuchFileException; 43 import java.nio.file.Path; 44 import java.nio.file.Paths; 45 import java.text.MessageFormat; 46 import java.util.ArrayList; 47 import java.util.Arrays; 48 import java.util.Collections; 49 import java.util.Iterator; 50 import java.util.LinkedHashMap; 51 import java.util.LinkedHashSet; 52 import java.util.List; 53 import java.util.Locale; 54 import java.util.Map; 55 import java.util.Map.Entry; 56 import java.util.Scanner; 57 import java.util.Set; 58 import java.util.function.Consumer; 59 import java.util.function.Predicate; 60 import java.util.prefs.Preferences; 61 import java.util.regex.Matcher; 62 import java.util.regex.Pattern; 63 import java.util.stream.Collectors; 64 import java.util.stream.Stream; 65 import java.util.stream.StreamSupport; 66 67 import jdk.internal.jshell.debug.InternalDebugControl; 68 import jdk.internal.jshell.tool.IOContext.InputInterruptedException; 69 import jdk.jshell.DeclarationSnippet; 70 import jdk.jshell.Diag; 71 import jdk.jshell.EvalException; 72 import jdk.jshell.ExpressionSnippet; 73 import jdk.jshell.ImportSnippet; 74 import jdk.jshell.JShell; 75 import jdk.jshell.JShell.Subscription; 76 import jdk.jshell.MethodSnippet; 77 import jdk.jshell.PersistentSnippet; 78 import jdk.jshell.Snippet; 79 import jdk.jshell.Snippet.Status; 80 import jdk.jshell.SnippetEvent; 81 import jdk.jshell.SourceCodeAnalysis; 82 import jdk.jshell.SourceCodeAnalysis.CompletionInfo; 83 import jdk.jshell.SourceCodeAnalysis.Suggestion; 84 import jdk.jshell.TypeDeclSnippet; 85 import jdk.jshell.UnresolvedReferenceException; 86 import jdk.jshell.VarSnippet; 87 88 import static java.nio.file.StandardOpenOption.CREATE; 89 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; 90 import static java.nio.file.StandardOpenOption.WRITE; 91 import java.util.MissingResourceException; 92 import java.util.Optional; 93 import java.util.ResourceBundle; 94 import java.util.Spliterators; 95 import java.util.function.Function; 96 import java.util.function.Supplier; 97 import jdk.internal.jshell.tool.Feedback.FormatAction; 98 import jdk.internal.jshell.tool.Feedback.FormatCase; 99 import jdk.internal.jshell.tool.Feedback.FormatErrors; 100 import jdk.internal.jshell.tool.Feedback.FormatResolve; 101 import jdk.internal.jshell.tool.Feedback.FormatUnresolved; 102 import jdk.internal.jshell.tool.Feedback.FormatWhen; 103 import static java.util.stream.Collectors.toList; 104 import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND; 105 import static java.util.stream.Collectors.toMap; 106 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_COMPA; 107 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP; 108 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT; 109 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR; 110 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN; 111 112 /** 113 * Command line REPL tool for Java using the JShell API. 114 * @author Robert Field 115 */ 116 public class JShellTool implements MessageHandler { 117 118 private static final String LINE_SEP = System.getProperty("line.separator"); 119 private static final Pattern LINEBREAK = Pattern.compile("\\R"); 120 private static final String RECORD_SEPARATOR = "\u241E"; 121 private static final String RB_NAME_PREFIX = "jdk.internal.jshell.tool.resources"; 122 private static final String VERSION_RB_NAME = RB_NAME_PREFIX + ".version"; 123 private static final String L10N_RB_NAME = RB_NAME_PREFIX + ".l10n"; 124 125 final InputStream cmdin; 126 final PrintStream cmdout; 127 final PrintStream cmderr; 128 final PrintStream console; 129 final InputStream userin; 130 final PrintStream userout; 131 final PrintStream usererr; 132 final Preferences prefs; 133 final Locale locale; 134 135 final Feedback feedback = new Feedback(); 136 137 /** 138 * The constructor for the tool (used by tool launch via main and by test 139 * harnesses to capture ins and outs. 140 * @param cmdin command line input -- snippets and commands 141 * @param cmdout command line output, feedback including errors 142 * @param cmderr start-up errors and debugging info 143 * @param console console control interaction 144 * @param userin code execution input (not yet functional) 145 * @param userout code execution output -- System.out.printf("hi") 146 * @param usererr code execution error stream -- System.err.printf("Oops") 147 * @param prefs preferences to use 148 * @param locale locale to use 149 */ 150 public JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr, 151 PrintStream console, 152 InputStream userin, PrintStream userout, PrintStream usererr, 153 Preferences prefs, Locale locale) { 154 this.cmdin = cmdin; 155 this.cmdout = cmdout; 156 this.cmderr = cmderr; 157 this.console = console; 158 this.userin = userin; 159 this.userout = userout; 160 this.usererr = usererr; 161 this.prefs = prefs; 162 this.locale = locale; 163 } 164 165 private ResourceBundle versionRB = null; 166 private ResourceBundle outputRB = null; 167 168 private IOContext input = null; 169 private boolean regenerateOnDeath = true; 170 private boolean live = false; 171 private boolean feedbackInitialized = false; 172 private String commandLineFeedbackMode = null; 173 private List<String> remoteVMOptions = new ArrayList<>(); 174 175 SourceCodeAnalysis analysis; 176 JShell state = null; 177 Subscription shutdownSubscription = null; 178 179 private boolean debug = false; 180 public boolean testPrompt = false; 181 private String cmdlineClasspath = null; 182 private String startup = null; 183 private String[] editor = null; 184 185 // Commands and snippets which should be replayed 186 private List<String> replayableHistory; 187 private List<String> replayableHistoryPrevious; 188 189 static final String STARTUP_KEY = "STARTUP"; 190 static final String EDITOR_KEY = "EDITOR"; 191 static final String FEEDBACK_KEY = "FEEDBACK"; 192 static final String MODE_KEY = "MODE"; 193 static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE"; 194 195 static final String DEFAULT_STARTUP = 196 "\n" + 197 "import java.util.*;\n" + 198 "import java.io.*;\n" + 199 "import java.math.*;\n" + 200 "import java.net.*;\n" + 201 "import java.util.concurrent.*;\n" + 202 "import java.util.prefs.*;\n" + 203 "import java.util.regex.*;\n" + 204 "void printf(String format, Object... args) { System.out.printf(format, args); }\n"; 205 206 // Tool id (tid) mapping: the three name spaces 207 NameSpace mainNamespace; 208 NameSpace startNamespace; 209 NameSpace errorNamespace; 210 211 // Tool id (tid) mapping: the current name spaces 212 NameSpace currentNameSpace; 213 214 Map<Snippet,SnippetInfo> mapSnippet; 215 216 /** 217 * Is the input/output currently interactive 218 * 219 * @return true if console 220 */ 221 boolean interactive() { 222 return input != null && input.interactiveOutput(); 223 } 224 225 void debug(String format, Object... args) { 226 if (debug) { 227 cmderr.printf(format + "\n", args); 228 } 229 } 230 231 /** 232 * Base output for command output -- no pre- or post-fix 233 * 234 * @param printf format 235 * @param printf args 236 */ 237 void rawout(String format, Object... args) { 238 cmdout.printf(format, args); 239 } 240 241 /** 242 * Must show command output 243 * 244 * @param format printf format 245 * @param args printf args 246 */ 247 void hard(String format, Object... args) { 248 rawout(feedback.getPre() + format + feedback.getPost(), args); 249 } 250 251 /** 252 * Error command output 253 * 254 * @param format printf format 255 * @param args printf args 256 */ 257 void error(String format, Object... args) { 258 rawout(feedback.getErrorPre() + format + feedback.getErrorPost(), args); 259 } 260 261 /** 262 * Optional output 263 * 264 * @param format printf format 265 * @param args printf args 266 */ 267 @Override 268 public void fluff(String format, Object... args) { 269 if (feedback.shouldDisplayCommandFluff() && interactive()) { 270 hard(format, args); 271 } 272 } 273 274 /** 275 * Optional output -- with embedded per- and post-fix 276 * 277 * @param format printf format 278 * @param args printf args 279 */ 280 void fluffRaw(String format, Object... args) { 281 if (feedback.shouldDisplayCommandFluff() && interactive()) { 282 rawout(format, args); 283 } 284 } 285 286 /** 287 * Print using resource bundle look-up and adding prefix and postfix 288 * 289 * @param key the resource key 290 */ 291 String getResourceString(String key) { 292 if (outputRB == null) { 293 try { 294 outputRB = ResourceBundle.getBundle(L10N_RB_NAME, locale); 295 } catch (MissingResourceException mre) { 296 error("Cannot find ResourceBundle: %s for locale: %s", L10N_RB_NAME, locale); 297 return ""; 298 } 299 } 300 String s; 301 try { 302 s = outputRB.getString(key); 303 } catch (MissingResourceException mre) { 304 error("Missing resource: %s in %s", key, L10N_RB_NAME); 305 return ""; 306 } 307 return s; 308 } 309 310 /** 311 * Add prefixing to embedded newlines in a string, leading with the normal 312 * prefix 313 * 314 * @param s the string to prefix 315 */ 316 String prefix(String s) { 317 return prefix(s, feedback.getPre()); 318 } 319 320 /** 321 * Add prefixing to embedded newlines in a string 322 * 323 * @param s the string to prefix 324 * @param leading the string to prepend 325 */ 326 String prefix(String s, String leading) { 327 if (s == null || s.isEmpty()) { 328 return ""; 329 } 330 return leading 331 + s.substring(0, s.length() - 1).replaceAll("\\R", System.getProperty("line.separator") + feedback.getPre()) 332 + s.substring(s.length() - 1, s.length()); 333 } 334 335 /** 336 * Print using resource bundle look-up and adding prefix and postfix 337 * 338 * @param key the resource key 339 */ 340 void hardrb(String key) { 341 String s = prefix(getResourceString(key)); 342 cmdout.println(s); 343 } 344 345 /** 346 * Format using resource bundle look-up using MessageFormat 347 * 348 * @param key the resource key 349 * @param args 350 */ 351 String messageFormat(String key, Object... args) { 352 String rs = getResourceString(key); 353 return MessageFormat.format(rs, args); 354 } 355 356 /** 357 * Print using resource bundle look-up, MessageFormat, and add prefix and 358 * postfix 359 * 360 * @param key the resource key 361 * @param args 362 */ 363 void hardmsg(String key, Object... args) { 364 cmdout.println(prefix(messageFormat(key, args))); 365 } 366 367 /** 368 * Print error using resource bundle look-up, MessageFormat, and add prefix 369 * and postfix 370 * 371 * @param key the resource key 372 * @param args 373 */ 374 @Override 375 public void errormsg(String key, Object... args) { 376 if (isRunningInteractive()) { 377 cmdout.println(prefix(messageFormat(key, args), feedback.getErrorPre())); 378 } else { 379 startmsg(key, args); 380 } 381 } 382 383 /** 384 * Print command-line error using resource bundle look-up, MessageFormat 385 * 386 * @param key the resource key 387 * @param args 388 */ 389 void startmsg(String key, Object... args) { 390 cmderr.println(prefix(messageFormat(key, args), "")); 391 } 392 393 /** 394 * Print (fluff) using resource bundle look-up, MessageFormat, and add 395 * prefix and postfix 396 * 397 * @param key the resource key 398 * @param args 399 */ 400 @Override 401 public void fluffmsg(String key, Object... args) { 402 if (feedback.shouldDisplayCommandFluff() && interactive()) { 403 hardmsg(key, args); 404 } 405 } 406 407 <T> void hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b) { 408 Map<String, String> a2b = stream.collect(toMap(a, b, 409 (m1, m2) -> m1, 410 () -> new LinkedHashMap<>())); 411 int aLen = 0; 412 for (String av : a2b.keySet()) { 413 aLen = Math.max(aLen, av.length()); 414 } 415 String format = " %-" + aLen + "s -- %s"; 416 String indentedNewLine = LINE_SEP + feedback.getPre() 417 + String.format(" %-" + (aLen + 4) + "s", ""); 418 for (Entry<String, String> e : a2b.entrySet()) { 419 hard(format, e.getKey(), e.getValue().replaceAll("\n", indentedNewLine)); 420 } 421 } 422 423 /** 424 * Trim whitespace off end of string 425 * 426 * @param s 427 * @return 428 */ 429 static String trimEnd(String s) { 430 int last = s.length() - 1; 431 int i = last; 432 while (i >= 0 && Character.isWhitespace(s.charAt(i))) { 433 --i; 434 } 435 if (i != last) { 436 return s.substring(0, i + 1); 437 } else { 438 return s; 439 } 440 } 441 442 /** 443 * Normal start entry point 444 * @param args 445 * @throws Exception 446 */ 447 public static void main(String[] args) throws Exception { 448 new JShellTool(System.in, System.out, System.err, System.out, 449 new ByteArrayInputStream(new byte[0]), System.out, System.err, 450 Preferences.userRoot().node("tool/JShell"), 451 Locale.getDefault()) 452 .start(args); 453 } 454 455 public void start(String[] args) throws Exception { 456 List<String> loadList = processCommandArgs(args); 457 if (loadList == null) { 458 // Abort 459 return; 460 } 461 try (IOContext in = new ConsoleIOContext(this, cmdin, console)) { 462 start(in, loadList); 463 } 464 } 465 466 private void start(IOContext in, List<String> loadList) { 467 // If startup hasn't been set by command line, set from retained/default 468 if (startup == null) { 469 startup = prefs.get(STARTUP_KEY, null); 470 if (startup == null) { 471 startup = DEFAULT_STARTUP; 472 } 473 } 474 475 // Read retained editor setting (if any) 476 String editorString = prefs.get(EDITOR_KEY, ""); 477 if (editorString == null || editorString.isEmpty()) { 478 editor = null; 479 } else { 480 editor = editorString.split(RECORD_SEPARATOR); 481 } 482 483 resetState(); // Initialize 484 485 // Read replay history from last jshell session into previous history 486 String prevReplay = prefs.get(REPLAY_RESTORE_KEY, null); 487 if (prevReplay != null) { 488 replayableHistoryPrevious = Arrays.asList(prevReplay.split(RECORD_SEPARATOR)); 489 } 490 491 for (String loadFile : loadList) { 492 runFile(loadFile, "jshell"); 493 } 494 495 if (regenerateOnDeath) { 496 hardmsg("jshell.msg.welcome", version()); 497 } 498 499 try { 500 while (regenerateOnDeath) { 501 if (!live) { 502 resetState(); 503 } 504 run(in); 505 } 506 } finally { 507 closeState(); 508 } 509 } 510 511 /** 512 * Process the command line arguments. 513 * Set options. 514 * @param args the command line arguments 515 * @return the list of files to be loaded 516 */ 517 private List<String> processCommandArgs(String[] args) { 518 List<String> loadList = new ArrayList<>(); 519 Iterator<String> ai = Arrays.asList(args).iterator(); 520 while (ai.hasNext()) { 521 String arg = ai.next(); 522 if (arg.startsWith("-")) { 523 switch (arg) { 524 case "-classpath": 525 case "-cp": 526 if (cmdlineClasspath != null) { 527 startmsg("jshell.err.opt.classpath.conflict"); 528 return null; 529 } 530 if (ai.hasNext()) { 531 cmdlineClasspath = ai.next(); 532 } else { 533 startmsg("jshell.err.opt.classpath.arg"); 534 return null; 535 } 536 break; 537 case "-help": 538 printUsage(); 539 return null; 540 case "-version": 541 cmdout.printf("jshell %s\n", version()); 542 return null; 543 case "-fullversion": 544 cmdout.printf("jshell %s\n", fullVersion()); 545 return null; 546 case "-feedback": 547 if (ai.hasNext()) { 548 commandLineFeedbackMode = ai.next(); 549 } else { 550 startmsg("jshell.err.opt.feedback.arg"); 551 return null; 552 } 553 break; 554 case "-q": 555 commandLineFeedbackMode = "concise"; 556 break; 557 case "-qq": 558 commandLineFeedbackMode = "silent"; 559 break; 560 case "-v": 561 commandLineFeedbackMode = "verbose"; 562 break; 563 case "-startup": 564 if (startup != null) { 565 startmsg("jshell.err.opt.startup.one"); 566 return null; 567 } 568 startup = readFile(ai.hasNext()? ai.next() : null, "-startup"); 569 if (startup == null) { 570 return null; 571 } 572 break; 573 case "-nostartup": 574 if (startup != null) { 575 startmsg("jshell.err.opt.startup.one"); 576 return null; 577 } 578 startup = ""; 579 break; 580 default: 581 if (arg.startsWith("-R")) { 582 remoteVMOptions.add(arg.substring(2)); 583 break; 584 } 585 startmsg("jshell.err.opt.unknown", arg); 586 printUsage(); 587 return null; 588 } 589 } else { 590 loadList.add(arg); 591 } 592 } 593 return loadList; 594 } 595 596 private void printUsage() { 597 cmdout.print(getResourceString("help.usage")); 598 } 599 600 /** 601 * Message handler to use during initial start-up. 602 */ 603 private class InitMessageHandler implements MessageHandler { 604 605 @Override 606 public void fluff(String format, Object... args) { 607 //ignore 608 } 609 610 @Override 611 public void fluffmsg(String messageKey, Object... args) { 612 //ignore 613 } 614 615 @Override 616 public void errormsg(String messageKey, Object... args) { 617 startmsg(messageKey, args); 618 } 619 } 620 621 private void resetState() { 622 closeState(); 623 624 // Initialize tool id mapping 625 mainNamespace = new NameSpace("main", ""); 626 startNamespace = new NameSpace("start", "s"); 627 errorNamespace = new NameSpace("error", "e"); 628 mapSnippet = new LinkedHashMap<>(); 629 currentNameSpace = startNamespace; 630 631 // Reset the replayable history, saving the old for restore 632 replayableHistoryPrevious = replayableHistory; 633 replayableHistory = new ArrayList<>(); 634 635 state = JShell.builder() 636 .in(userin) 637 .out(userout) 638 .err(usererr) 639 .tempVariableNameGenerator(()-> "$" + currentNameSpace.tidNext()) 640 .idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive()) 641 ? currentNameSpace.tid(sn) 642 : errorNamespace.tid(sn)) 643 .remoteVMOptions(remoteVMOptions.toArray(new String[remoteVMOptions.size()])) 644 .build(); 645 shutdownSubscription = state.onShutdown((JShell deadState) -> { 646 if (deadState == state) { 647 hardmsg("jshell.msg.terminated"); 648 live = false; 649 } 650 }); 651 analysis = state.sourceCodeAnalysis(); 652 live = true; 653 if (!feedbackInitialized) { 654 // One time per run feedback initialization 655 feedbackInitialized = true; 656 initFeedback(); 657 } 658 659 if (cmdlineClasspath != null) { 660 state.addToClasspath(cmdlineClasspath); 661 } 662 663 startUpRun(startup); 664 currentNameSpace = mainNamespace; 665 } 666 667 private boolean isRunningInteractive() { 668 return currentNameSpace != null && currentNameSpace == mainNamespace; 669 } 670 671 //where -- one-time per run initialization of feedback modes 672 private void initFeedback() { 673 // No fluff, no prefix, for init failures 674 MessageHandler initmh = new InitMessageHandler(); 675 // Execute the feedback initialization code in the resource file 676 startUpRun(getResourceString("startup.feedback")); 677 // These predefined modes are read-only 678 feedback.markModesReadOnly(); 679 // Restore user defined modes retained on previous run with /retain mode 680 String encoded = prefs.get(MODE_KEY, null); 681 if (encoded != null && !encoded.isEmpty()) { 682 if (!feedback.restoreEncodedModes(initmh, encoded)) { 683 // Catastrophic corruption -- remove the retained modes 684 prefs.remove(MODE_KEY); 685 } 686 } 687 if (commandLineFeedbackMode != null) { 688 // The feedback mode to use was specified on the command line, use it 689 if (!feedback.setFeedback(initmh, new ArgTokenizer("-feedback", commandLineFeedbackMode))) { 690 regenerateOnDeath = false; 691 } 692 commandLineFeedbackMode = null; 693 } else { 694 String fb = prefs.get(FEEDBACK_KEY, null); 695 if (fb != null) { 696 // Restore the feedback mode to use that was retained 697 // on a previous run with /retain feedback 698 feedback.retainFeedback(initmh, new ArgTokenizer("/retain feedback", fb)); 699 } 700 } 701 } 702 703 //where 704 private void startUpRun(String start) { 705 try (IOContext suin = new FileScannerIOContext(new StringReader(start))) { 706 run(suin); 707 } catch (Exception ex) { 708 hardmsg("jshell.err.startup.unexpected.exception", ex); 709 ex.printStackTrace(cmdout); 710 } 711 } 712 713 private void closeState() { 714 live = false; 715 JShell oldState = state; 716 if (oldState != null) { 717 oldState.unsubscribe(shutdownSubscription); // No notification 718 oldState.close(); 719 } 720 } 721 722 /** 723 * Main loop 724 * @param in the line input/editing context 725 */ 726 private void run(IOContext in) { 727 IOContext oldInput = input; 728 input = in; 729 try { 730 String incomplete = ""; 731 while (live) { 732 String prompt; 733 if (isRunningInteractive()) { 734 prompt = testPrompt 735 ? incomplete.isEmpty() 736 ? "\u0005" //ENQ 737 : "\u0006" //ACK 738 : incomplete.isEmpty() 739 ? feedback.getPrompt(currentNameSpace.tidNext()) 740 : feedback.getContinuationPrompt(currentNameSpace.tidNext()) 741 ; 742 } else { 743 prompt = ""; 744 } 745 String raw; 746 try { 747 raw = in.readLine(prompt, incomplete); 748 } catch (InputInterruptedException ex) { 749 //input interrupted - clearing current state 750 incomplete = ""; 751 continue; 752 } 753 if (raw == null) { 754 //EOF 755 if (in.interactiveOutput()) { 756 // End after user ctrl-D 757 regenerateOnDeath = false; 758 } 759 break; 760 } 761 String trimmed = trimEnd(raw); 762 if (!trimmed.isEmpty()) { 763 String line = incomplete + trimmed; 764 765 // No commands in the middle of unprocessed source 766 if (incomplete.isEmpty() && line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*")) { 767 processCommand(line.trim()); 768 } else { 769 incomplete = processSourceCatchingReset(line); 770 } 771 } 772 } 773 } catch (IOException ex) { 774 errormsg("jshell.err.unexpected.exception", ex); 775 } finally { 776 input = oldInput; 777 } 778 } 779 780 private void addToReplayHistory(String s) { 781 if (isRunningInteractive()) { 782 replayableHistory.add(s); 783 } 784 } 785 786 private String processSourceCatchingReset(String src) { 787 try { 788 input.beforeUserCode(); 789 return processSource(src); 790 } catch (IllegalStateException ex) { 791 hard("Resetting..."); 792 live = false; // Make double sure 793 return ""; 794 } finally { 795 input.afterUserCode(); 796 } 797 } 798 799 private void processCommand(String cmd) { 800 if (cmd.startsWith("/-")) { 801 try { 802 //handle "/-[number]" 803 cmdUseHistoryEntry(Integer.parseInt(cmd.substring(1))); 804 return ; 805 } catch (NumberFormatException ex) { 806 //ignore 807 } 808 } 809 String arg = ""; 810 int idx = cmd.indexOf(' '); 811 if (idx > 0) { 812 arg = cmd.substring(idx + 1).trim(); 813 cmd = cmd.substring(0, idx); 814 } 815 Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand); 816 switch (candidates.length) { 817 case 0: 818 if (!rerunHistoryEntryById(cmd.substring(1))) { 819 errormsg("jshell.err.no.such.command.or.snippet.id", cmd); 820 fluffmsg("jshell.msg.help.for.help"); 821 } break; 822 case 1: 823 Command command = candidates[0]; 824 // If comand was successful and is of a replayable kind, add it the replayable history 825 if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) { 826 addToReplayHistory((command.command + " " + arg).trim()); 827 } break; 828 default: 829 errormsg("jshell.err.command.ambiguous", cmd, 830 Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", "))); 831 fluffmsg("jshell.msg.help.for.help"); 832 break; 833 } 834 } 835 836 private Command[] findCommand(String cmd, Predicate<Command> filter) { 837 Command exact = commands.get(cmd); 838 if (exact != null) 839 return new Command[] {exact}; 840 841 return commands.values() 842 .stream() 843 .filter(filter) 844 .filter(command -> command.command.startsWith(cmd)) 845 .toArray(size -> new Command[size]); 846 } 847 848 private static Path toPathResolvingUserHome(String pathString) { 849 if (pathString.replace(File.separatorChar, '/').startsWith("~/")) 850 return Paths.get(System.getProperty("user.home"), pathString.substring(2)); 851 else 852 return Paths.get(pathString); 853 } 854 855 static final class Command { 856 public final String command; 857 public final String helpKey; 858 public final Function<String,Boolean> run; 859 public final CompletionProvider completions; 860 public final CommandKind kind; 861 862 // NORMAL Commands 863 public Command(String command, Function<String,Boolean> run, CompletionProvider completions) { 864 this(command, run, completions, CommandKind.NORMAL); 865 } 866 867 // Special kinds of Commands 868 public Command(String command, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) { 869 this(command, "help." + command.substring(1), 870 run, completions, kind); 871 } 872 873 // Documentation pseudo-commands 874 public Command(String command, String helpKey, CommandKind kind) { 875 this(command, helpKey, 876 arg -> { throw new IllegalStateException(); }, 877 EMPTY_COMPLETION_PROVIDER, 878 kind); 879 } 880 881 public Command(String command, String helpKey, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) { 882 this.command = command; 883 this.helpKey = helpKey; 884 this.run = run; 885 this.completions = completions; 886 this.kind = kind; 887 } 888 889 } 890 891 interface CompletionProvider { 892 List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor); 893 } 894 895 enum CommandKind { 896 NORMAL(true, true, true), 897 REPLAY(true, true, true), 898 HIDDEN(true, false, false), 899 HELP_ONLY(false, true, false), 900 HELP_SUBJECT(false, false, false); 901 902 final boolean isRealCommand; 903 final boolean showInHelp; 904 final boolean shouldSuggestCompletions; 905 private CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions) { 906 this.isRealCommand = isRealCommand; 907 this.showInHelp = showInHelp; 908 this.shouldSuggestCompletions = shouldSuggestCompletions; 909 } 910 } 911 912 static final class FixedCompletionProvider implements CompletionProvider { 913 914 private final String[] alternatives; 915 916 public FixedCompletionProvider(String... alternatives) { 917 this.alternatives = alternatives; 918 } 919 920 @Override 921 public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) { 922 List<Suggestion> result = new ArrayList<>(); 923 924 for (String alternative : alternatives) { 925 if (alternative.startsWith(input)) { 926 result.add(new ArgSuggestion(alternative)); 927 } 928 } 929 930 anchor[0] = 0; 931 932 return result; 933 } 934 935 } 936 937 private static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider(); 938 private static final CompletionProvider KEYWORD_COMPLETION_PROVIDER = new FixedCompletionProvider("-all ", "-start ", "-history "); 939 private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-restore", "-quiet"); 940 private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true); 941 private final Map<String, Command> commands = new LinkedHashMap<>(); 942 private void registerCommand(Command cmd) { 943 commands.put(cmd.command, cmd); 944 } 945 private static CompletionProvider fileCompletions(Predicate<Path> accept) { 946 return (code, cursor, anchor) -> { 947 int lastSlash = code.lastIndexOf('/'); 948 String path = code.substring(0, lastSlash + 1); 949 String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code; 950 Path current = toPathResolvingUserHome(path); 951 List<Suggestion> result = new ArrayList<>(); 952 try (Stream<Path> dir = Files.list(current)) { 953 dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix)) 954 .map(f -> new ArgSuggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : ""))) 955 .forEach(result::add); 956 } catch (IOException ex) { 957 //ignore... 958 } 959 if (path.isEmpty()) { 960 StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false) 961 .filter(root -> accept.test(root) && root.toString().startsWith(prefix)) 962 .map(root -> new ArgSuggestion(root.toString())) 963 .forEach(result::add); 964 } 965 anchor[0] = path.length(); 966 return result; 967 }; 968 } 969 970 private static CompletionProvider classPathCompletion() { 971 return fileCompletions(p -> Files.isDirectory(p) || 972 p.getFileName().toString().endsWith(".zip") || 973 p.getFileName().toString().endsWith(".jar")); 974 } 975 976 private CompletionProvider snippetCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) { 977 return (prefix, cursor, anchor) -> { 978 anchor[0] = 0; 979 return snippetsSupplier.get() 980 .flatMap(k -> (k instanceof DeclarationSnippet) 981 ? Stream.of(String.valueOf(k.id()), ((DeclarationSnippet) k).name()) 982 : Stream.of(String.valueOf(k.id()))) 983 .filter(k -> k.startsWith(prefix)) 984 .map(k -> new ArgSuggestion(k)) 985 .collect(Collectors.toList()); 986 }; 987 } 988 989 private CompletionProvider snippetKeywordCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) { 990 return (code, cursor, anchor) -> { 991 List<Suggestion> result = new ArrayList<>(); 992 result.addAll(KEYWORD_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor)); 993 result.addAll(snippetCompletion(snippetsSupplier).completionSuggestions(code, cursor, anchor)); 994 return result; 995 }; 996 } 997 998 private static CompletionProvider saveCompletion() { 999 return (code, cursor, anchor) -> { 1000 List<Suggestion> result = new ArrayList<>(); 1001 int space = code.indexOf(' '); 1002 if (space == (-1)) { 1003 result.addAll(KEYWORD_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor)); 1004 } 1005 result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor)); 1006 anchor[0] += space + 1; 1007 return result; 1008 }; 1009 } 1010 1011 private static CompletionProvider reloadCompletion() { 1012 return (code, cursor, anchor) -> { 1013 List<Suggestion> result = new ArrayList<>(); 1014 int pastSpace = code.indexOf(' ') + 1; // zero if no space 1015 result.addAll(RELOAD_OPTIONS_COMPLETION_PROVIDER.completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor)); 1016 anchor[0] += pastSpace; 1017 return result; 1018 }; 1019 } 1020 1021 // Snippet lists 1022 1023 Stream<Snippet> allSnippets() { 1024 return state.snippets(); 1025 } 1026 1027 Stream<PersistentSnippet> dropableSnippets() { 1028 return state.snippets() 1029 .filter(sn -> state.status(sn).isActive() && sn instanceof PersistentSnippet) 1030 .map(sn -> (PersistentSnippet) sn); 1031 } 1032 1033 Stream<VarSnippet> allVarSnippets() { 1034 return state.snippets() 1035 .filter(sn -> sn.kind() == Snippet.Kind.VAR) 1036 .map(sn -> (VarSnippet) sn); 1037 } 1038 1039 Stream<MethodSnippet> allMethodSnippets() { 1040 return state.snippets() 1041 .filter(sn -> sn.kind() == Snippet.Kind.METHOD) 1042 .map(sn -> (MethodSnippet) sn); 1043 } 1044 1045 Stream<TypeDeclSnippet> allTypeSnippets() { 1046 return state.snippets() 1047 .filter(sn -> sn.kind() == Snippet.Kind.TYPE_DECL) 1048 .map(sn -> (TypeDeclSnippet) sn); 1049 } 1050 1051 // Table of commands -- with command forms, argument kinds, helpKey message, implementation, ... 1052 1053 { 1054 registerCommand(new Command("/list", 1055 arg -> cmdList(arg), 1056 snippetKeywordCompletion(this::allSnippets))); 1057 registerCommand(new Command("/edit", 1058 arg -> cmdEdit(arg), 1059 snippetCompletion(this::allSnippets))); 1060 registerCommand(new Command("/drop", 1061 arg -> cmdDrop(arg), 1062 snippetCompletion(this::dropableSnippets), 1063 CommandKind.REPLAY)); 1064 registerCommand(new Command("/save", 1065 arg -> cmdSave(arg), 1066 saveCompletion())); 1067 registerCommand(new Command("/open", 1068 arg -> cmdOpen(arg), 1069 FILE_COMPLETION_PROVIDER)); 1070 registerCommand(new Command("/vars", 1071 arg -> cmdVars(arg), 1072 snippetKeywordCompletion(this::allVarSnippets))); 1073 registerCommand(new Command("/methods", 1074 arg -> cmdMethods(arg), 1075 snippetKeywordCompletion(this::allMethodSnippets))); 1076 registerCommand(new Command("/types", 1077 arg -> cmdTypes(arg), 1078 snippetKeywordCompletion(this::allTypeSnippets))); 1079 registerCommand(new Command("/imports", 1080 arg -> cmdImports(), 1081 EMPTY_COMPLETION_PROVIDER)); 1082 registerCommand(new Command("/exit", 1083 arg -> cmdExit(), 1084 EMPTY_COMPLETION_PROVIDER)); 1085 registerCommand(new Command("/reset", 1086 arg -> cmdReset(), 1087 EMPTY_COMPLETION_PROVIDER)); 1088 registerCommand(new Command("/reload", 1089 arg -> cmdReload(arg), 1090 reloadCompletion())); 1091 registerCommand(new Command("/classpath", 1092 arg -> cmdClasspath(arg), 1093 classPathCompletion(), 1094 CommandKind.REPLAY)); 1095 registerCommand(new Command("/history", 1096 arg -> cmdHistory(), 1097 EMPTY_COMPLETION_PROVIDER)); 1098 registerCommand(new Command("/debug", 1099 arg -> cmdDebug(arg), 1100 EMPTY_COMPLETION_PROVIDER, 1101 CommandKind.HIDDEN)); 1102 registerCommand(new Command("/help", 1103 arg -> cmdHelp(arg), 1104 EMPTY_COMPLETION_PROVIDER)); 1105 registerCommand(new Command("/set", 1106 arg -> cmdSet(arg), 1107 new FixedCompletionProvider(SET_SUBCOMMANDS))); 1108 registerCommand(new Command("/retain", 1109 arg -> cmdRetain(arg), 1110 new FixedCompletionProvider(RETAIN_SUBCOMMANDS))); 1111 registerCommand(new Command("/?", 1112 "help.quest", 1113 arg -> cmdHelp(arg), 1114 EMPTY_COMPLETION_PROVIDER, 1115 CommandKind.NORMAL)); 1116 registerCommand(new Command("/!", 1117 "help.bang", 1118 arg -> cmdUseHistoryEntry(-1), 1119 EMPTY_COMPLETION_PROVIDER, 1120 CommandKind.NORMAL)); 1121 1122 // Documentation pseudo-commands 1123 registerCommand(new Command("/<id>", 1124 "help.id", 1125 CommandKind.HELP_ONLY)); 1126 registerCommand(new Command("/-<n>", 1127 "help.previous", 1128 CommandKind.HELP_ONLY)); 1129 registerCommand(new Command("intro", 1130 "help.intro", 1131 CommandKind.HELP_SUBJECT)); 1132 registerCommand(new Command("shortcuts", 1133 "help.shortcuts", 1134 CommandKind.HELP_SUBJECT)); 1135 } 1136 1137 public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) { 1138 String prefix = code.substring(0, cursor); 1139 int space = prefix.indexOf(' '); 1140 Stream<Suggestion> result; 1141 1142 if (space == (-1)) { 1143 result = commands.values() 1144 .stream() 1145 .distinct() 1146 .filter(cmd -> cmd.kind.shouldSuggestCompletions) 1147 .map(cmd -> cmd.command) 1148 .filter(key -> key.startsWith(prefix)) 1149 .map(key -> new ArgSuggestion(key + " ")); 1150 anchor[0] = 0; 1151 } else { 1152 String arg = prefix.substring(space + 1); 1153 String cmd = prefix.substring(0, space); 1154 Command[] candidates = findCommand(cmd, c -> true); 1155 if (candidates.length == 1) { 1156 result = candidates[0].completions.completionSuggestions(arg, cursor - space, anchor).stream(); 1157 anchor[0] += space + 1; 1158 } else { 1159 result = Stream.empty(); 1160 } 1161 } 1162 1163 return result.sorted((s1, s2) -> s1.continuation().compareTo(s2.continuation())) 1164 .collect(Collectors.toList()); 1165 } 1166 1167 public String commandDocumentation(String code, int cursor) { 1168 code = code.substring(0, cursor); 1169 int space = code.indexOf(' '); 1170 1171 if (space != (-1)) { 1172 String cmd = code.substring(0, space); 1173 Command command = commands.get(cmd); 1174 if (command != null) { 1175 return getResourceString(command.helpKey + ".summary"); 1176 } 1177 } 1178 1179 return null; 1180 } 1181 1182 // --- Command implementations --- 1183 1184 private static final String[] SET_SUBCOMMANDS = new String[]{ 1185 "format", "truncation", "feedback", "mode", "prompt", "editor", "start"}; 1186 1187 private static final String[] RETAIN_SUBCOMMANDS = new String[]{ 1188 "feedback", "mode", "editor", "start"}; 1189 1190 final boolean cmdSet(String arg) { 1191 String cmd = "/set"; 1192 ArgTokenizer at = new ArgTokenizer(cmd, arg.trim()); 1193 String which = subCommand(cmd, at, SET_SUBCOMMANDS); 1194 if (which == null) { 1195 return false; 1196 } 1197 switch (which) { 1198 case "format": 1199 return feedback.setFormat(this, at); 1200 case "truncation": 1201 return feedback.setTruncation(this, at); 1202 case "feedback": 1203 return feedback.setFeedback(this, at); 1204 case "mode": 1205 return feedback.setMode(this, at); 1206 case "prompt": 1207 return feedback.setPrompt(this, at); 1208 case "editor": 1209 return setEditor(at, true); 1210 case "start": 1211 return setStart(cmd, at, true); 1212 default: 1213 errormsg("jshell.err.arg", cmd, at.val()); 1214 return false; 1215 } 1216 } 1217 1218 final boolean cmdRetain(String arg) { 1219 String cmd = "/retain"; 1220 ArgTokenizer at = new ArgTokenizer(cmd, arg.trim()); 1221 String which = subCommand(cmd, at, RETAIN_SUBCOMMANDS); 1222 if (which == null) { 1223 return false; 1224 } 1225 switch (which) { 1226 case "feedback": { 1227 String fb = feedback.retainFeedback(this, at); 1228 if (fb != null) { 1229 // If a feedback mode has been set now, or in the past, retain it 1230 prefs.put(FEEDBACK_KEY, fb); 1231 return true; 1232 } 1233 return false; 1234 } 1235 case "mode": 1236 String retained = feedback.retainMode(this, at); 1237 if (retained != null) { 1238 // Retain this mode and all previously retained modes 1239 prefs.put(MODE_KEY, retained); 1240 return true; 1241 } 1242 return false; 1243 case "editor": 1244 if (!setEditor(at, false)) { 1245 return false; 1246 } 1247 // retain editor setting 1248 prefs.put(EDITOR_KEY, (editor == null) 1249 ? "" 1250 : String.join(RECORD_SEPARATOR, editor)); 1251 return true; 1252 case "start": { 1253 if (!setStart(cmd, at, false)) { 1254 return false; 1255 } 1256 // retain startup setting 1257 prefs.put(STARTUP_KEY, startup); 1258 return true; 1259 } 1260 default: 1261 errormsg("jshell.err.arg", cmd, at.val()); 1262 return false; 1263 } 1264 } 1265 1266 // Print the help doc for the specified sub-command 1267 boolean printSubCommandHelp(String cmd, ArgTokenizer at, String helpPrefix, String[] subs) { 1268 String which = subCommand(cmd, at, subs); 1269 if (which == null) { 1270 return false; 1271 } 1272 hardrb(helpPrefix + which); 1273 return true; 1274 } 1275 1276 // Find which, if any, sub-command matches 1277 String subCommand(String cmd, ArgTokenizer at, String[] subs) { 1278 String[] matches = at.next(subs); 1279 if (matches == null) { 1280 // No sub-command was given 1281 errormsg("jshell.err.sub.arg", cmd); 1282 return null; 1283 } 1284 if (matches.length == 0) { 1285 // There are no matching sub-commands 1286 errormsg("jshell.err.arg", cmd, at.val()); 1287 fluffmsg("jshell.msg.use.one.of", Arrays.stream(subs) 1288 .collect(Collectors.joining(", ")) 1289 ); 1290 return null; 1291 } 1292 if (matches.length > 1) { 1293 // More than one sub-command matches the initial characters provided 1294 errormsg("jshell.err.sub.ambiguous", cmd, at.val()); 1295 fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches) 1296 .collect(Collectors.joining(", ")) 1297 ); 1298 return null; 1299 } 1300 return matches[0]; 1301 } 1302 1303 // The sub-command: /set editor <editor-command-line>> 1304 boolean setEditor(ArgTokenizer at, boolean argsRequired) { 1305 at.allowedOptions("-default"); 1306 String prog = at.next(); 1307 List<String> ed = new ArrayList<>(); 1308 while (at.val() != null) { 1309 ed.add(at.val()); 1310 at.nextToken(); 1311 } 1312 if (!checkOptionsAndRemainingInput(at)) { 1313 return false; 1314 } 1315 boolean defaultOption = at.hasOption("-default"); 1316 if (prog != null) { 1317 if (defaultOption) { 1318 errormsg("jshell.err.default.option.or.program", at.whole()); 1319 return false; 1320 } 1321 editor = ed.toArray(new String[ed.size()]); 1322 fluffmsg("jshell.msg.set.editor.set", prog); 1323 } else if (defaultOption) { 1324 editor = null; 1325 } else if (argsRequired) { 1326 errormsg("jshell.err.set.editor.arg"); 1327 return false; 1328 } 1329 return true; 1330 } 1331 1332 // The sub-command: /set start <start-file> 1333 boolean setStart(String cmd, ArgTokenizer at, boolean argsRequired) { 1334 at.allowedOptions("-default", "-none"); 1335 String fn = at.next(); 1336 if (!checkOptionsAndRemainingInput(at)) { 1337 return false; 1338 } 1339 int argCount = at.optionCount() + ((fn != null) ? 1 : 0); 1340 if (argCount > 1 || argsRequired && argCount == 0) { 1341 errormsg("jshell.err.option.or.filename", at.whole()); 1342 return false; 1343 } 1344 if (fn != null) { 1345 String init = readFile(fn, cmd + " start"); 1346 if (init == null) { 1347 return false; 1348 } else { 1349 startup = init; 1350 return true; 1351 } 1352 } else if (at.hasOption("-default")) { 1353 startup = DEFAULT_STARTUP; 1354 } else if (at.hasOption("-none")) { 1355 startup = ""; 1356 } 1357 return true; 1358 } 1359 1360 boolean cmdClasspath(String arg) { 1361 if (arg.isEmpty()) { 1362 errormsg("jshell.err.classpath.arg"); 1363 return false; 1364 } else { 1365 state.addToClasspath(toPathResolvingUserHome(arg).toString()); 1366 fluffmsg("jshell.msg.classpath", arg); 1367 return true; 1368 } 1369 } 1370 1371 boolean cmdDebug(String arg) { 1372 if (arg.isEmpty()) { 1373 debug = !debug; 1374 InternalDebugControl.setDebugFlags(state, debug ? DBG_GEN : 0); 1375 fluff("Debugging %s", debug ? "on" : "off"); 1376 } else { 1377 int flags = 0; 1378 for (char ch : arg.toCharArray()) { 1379 switch (ch) { 1380 case '0': 1381 flags = 0; 1382 debug = false; 1383 fluff("Debugging off"); 1384 break; 1385 case 'r': 1386 debug = true; 1387 fluff("REPL tool debugging on"); 1388 break; 1389 case 'g': 1390 flags |= DBG_GEN; 1391 fluff("General debugging on"); 1392 break; 1393 case 'f': 1394 flags |= DBG_FMGR; 1395 fluff("File manager debugging on"); 1396 break; 1397 case 'c': 1398 flags |= DBG_COMPA; 1399 fluff("Completion analysis debugging on"); 1400 break; 1401 case 'd': 1402 flags |= DBG_DEP; 1403 fluff("Dependency debugging on"); 1404 break; 1405 case 'e': 1406 flags |= DBG_EVNT; 1407 fluff("Event debugging on"); 1408 break; 1409 default: 1410 hard("Unknown debugging option: %c", ch); 1411 fluff("Use: 0 r g f c d"); 1412 return false; 1413 } 1414 } 1415 InternalDebugControl.setDebugFlags(state, flags); 1416 } 1417 return true; 1418 } 1419 1420 private boolean cmdExit() { 1421 regenerateOnDeath = false; 1422 live = false; 1423 if (!replayableHistory.isEmpty()) { 1424 // Prevent history overflow by calculating what will fit, starting 1425 // with most recent 1426 int sepLen = RECORD_SEPARATOR.length(); 1427 int length = 0; 1428 int first = replayableHistory.size(); 1429 while(length < Preferences.MAX_VALUE_LENGTH && --first >= 0) { 1430 length += replayableHistory.get(first).length() + sepLen; 1431 } 1432 String hist = String.join(RECORD_SEPARATOR, 1433 replayableHistory.subList(first + 1, replayableHistory.size())); 1434 prefs.put(REPLAY_RESTORE_KEY, hist); 1435 } 1436 fluffmsg("jshell.msg.goodbye"); 1437 return true; 1438 } 1439 1440 boolean cmdHelp(String arg) { 1441 ArgTokenizer at = new ArgTokenizer("/help", arg); 1442 String subject = at.next(); 1443 if (subject != null) { 1444 Command[] matches = commands.values().stream() 1445 .filter(c -> c.command.startsWith(subject)) 1446 .toArray(size -> new Command[size]); 1447 at.mark(); 1448 String sub = at.next(); 1449 if (sub != null && matches.length == 1) { 1450 String cmd = matches[0].command; 1451 switch (cmd) { 1452 case "/set": 1453 at.rewind(); 1454 return printSubCommandHelp(cmd, at, "help.set.", SET_SUBCOMMANDS); 1455 case "/retain": 1456 at.rewind(); 1457 return printSubCommandHelp(cmd, at, "help.retain.", RETAIN_SUBCOMMANDS); 1458 } 1459 } 1460 if (matches.length > 0) { 1461 for (Command c : matches) { 1462 hard(""); 1463 hard("%s", c.command); 1464 hard(""); 1465 hardrb(c.helpKey); 1466 } 1467 return true; 1468 } else { 1469 errormsg("jshell.err.help.arg", arg); 1470 } 1471 } 1472 hardmsg("jshell.msg.help.begin"); 1473 hardPairs(commands.values().stream() 1474 .filter(cmd -> cmd.kind.showInHelp), 1475 cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"), 1476 cmd -> getResourceString(cmd.helpKey + ".summary") 1477 ); 1478 hardmsg("jshell.msg.help.subject"); 1479 hardPairs(commands.values().stream() 1480 .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT), 1481 cmd -> cmd.command, 1482 cmd -> getResourceString(cmd.helpKey + ".summary") 1483 ); 1484 return true; 1485 } 1486 1487 private boolean cmdHistory() { 1488 cmdout.println(); 1489 for (String s : input.currentSessionHistory()) { 1490 // No number prefix, confusing with snippet ids 1491 cmdout.printf("%s\n", s); 1492 } 1493 return true; 1494 } 1495 1496 /** 1497 * Avoid parameterized varargs possible heap pollution warning. 1498 */ 1499 private interface SnippetPredicate<T extends Snippet> extends Predicate<T> { } 1500 1501 /** 1502 * Apply filters to a stream until one that is non-empty is found. 1503 * Adapted from Stuart Marks 1504 * 1505 * @param supplier Supply the Snippet stream to filter 1506 * @param filters Filters to attempt 1507 * @return The non-empty filtered Stream, or null 1508 */ 1509 @SafeVarargs 1510 private static <T extends Snippet> Stream<T> nonEmptyStream(Supplier<Stream<T>> supplier, 1511 SnippetPredicate<T>... filters) { 1512 for (SnippetPredicate<T> filt : filters) { 1513 Iterator<T> iterator = supplier.get().filter(filt).iterator(); 1514 if (iterator.hasNext()) { 1515 return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); 1516 } 1517 } 1518 return null; 1519 } 1520 1521 private boolean inStartUp(Snippet sn) { 1522 return mapSnippet.get(sn).space == startNamespace; 1523 } 1524 1525 private boolean isActive(Snippet sn) { 1526 return state.status(sn).isActive(); 1527 } 1528 1529 private boolean mainActive(Snippet sn) { 1530 return !inStartUp(sn) && isActive(sn); 1531 } 1532 1533 private boolean matchingDeclaration(Snippet sn, String name) { 1534 return sn instanceof DeclarationSnippet 1535 && ((DeclarationSnippet) sn).name().equals(name); 1536 } 1537 1538 /** 1539 * Convert user arguments to a Stream of snippets referenced by those 1540 * arguments (or lack of arguments). 1541 * 1542 * @param snippets the base list of possible snippets 1543 * @param defFilter the filter to apply to the arguments if no argument 1544 * @param rawargs the user's argument to the command, maybe be the empty 1545 * string 1546 * @return a Stream of referenced snippets or null if no matches are found 1547 */ 1548 private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier, 1549 Predicate<Snippet> defFilter, String rawargs, String cmd) { 1550 ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim()); 1551 at.allowedOptions("-all", "-start"); 1552 List<String> args = new ArrayList<>(); 1553 String s; 1554 while ((s = at.next()) != null) { 1555 args.add(s); 1556 } 1557 if (!checkOptionsAndRemainingInput(at)) { 1558 return null; 1559 } 1560 if (at.optionCount() > 0 && args.size() > 0) { 1561 errormsg("jshell.err.may.not.specify.options.and.snippets", at.whole()); 1562 return null; 1563 } 1564 if (at.optionCount() > 1) { 1565 errormsg("jshell.err.conflicting.options", at.whole()); 1566 return null; 1567 } 1568 if (at.hasOption("-all")) { 1569 // all snippets including start-up, failed, and overwritten 1570 return snippetSupplier.get(); 1571 } 1572 if (at.hasOption("-start")) { 1573 // start-up snippets 1574 return snippetSupplier.get() 1575 .filter(this::inStartUp); 1576 } 1577 if (args.isEmpty()) { 1578 // Default is all active user snippets 1579 return snippetSupplier.get() 1580 .filter(defFilter); 1581 } 1582 return argsToSnippets(snippetSupplier, args); 1583 } 1584 1585 /** 1586 * Convert user arguments to a Stream of snippets referenced by those 1587 * arguments. 1588 * 1589 * @param snippetSupplier the base list of possible snippets 1590 * @param args the user's argument to the command, maybe be the empty list 1591 * @return a Stream of referenced snippets or null if no matches to specific 1592 * arg 1593 */ 1594 private <T extends Snippet> Stream<T> argsToSnippets(Supplier<Stream<T>> snippetSupplier, 1595 List<String> args) { 1596 Stream<T> result = null; 1597 for (String arg : args) { 1598 // Find the best match 1599 Stream<T> st = layeredSnippetSearch(snippetSupplier, arg); 1600 if (st == null) { 1601 Stream<Snippet> est = layeredSnippetSearch(state::snippets, arg); 1602 if (est == null) { 1603 errormsg("jshell.err.no.such.snippets", arg); 1604 } else { 1605 errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command", 1606 arg, est.findFirst().get().source()); 1607 } 1608 return null; 1609 } 1610 if (result == null) { 1611 result = st; 1612 } else { 1613 result = Stream.concat(result, st); 1614 } 1615 } 1616 return result; 1617 } 1618 1619 private <T extends Snippet> Stream<T> layeredSnippetSearch(Supplier<Stream<T>> snippetSupplier, String arg) { 1620 return nonEmptyStream( 1621 // the stream supplier 1622 snippetSupplier, 1623 // look for active user declarations matching the name 1624 sn -> isActive(sn) && matchingDeclaration(sn, arg), 1625 // else, look for any declarations matching the name 1626 sn -> matchingDeclaration(sn, arg), 1627 // else, look for an id of this name 1628 sn -> sn.id().equals(arg) 1629 ); 1630 } 1631 1632 private boolean cmdDrop(String rawargs) { 1633 ArgTokenizer at = new ArgTokenizer("/drop", rawargs.trim()); 1634 at.allowedOptions(); 1635 List<String> args = new ArrayList<>(); 1636 String s; 1637 while ((s = at.next()) != null) { 1638 args.add(s); 1639 } 1640 if (!checkOptionsAndRemainingInput(at)) { 1641 return false; 1642 } 1643 if (args.isEmpty()) { 1644 errormsg("jshell.err.drop.arg"); 1645 return false; 1646 } 1647 Stream<PersistentSnippet> stream = argsToSnippets(this::dropableSnippets, args); 1648 if (stream == null) { 1649 // Snippet not found. Error already printed 1650 fluffmsg("jshell.msg.see.classes.etc"); 1651 return false; 1652 } 1653 List<PersistentSnippet> snippets = stream.collect(toList()); 1654 if (snippets.size() > args.size()) { 1655 // One of the args references more thean one snippet 1656 errormsg("jshell.err.drop.ambiguous"); 1657 fluffmsg("jshell.msg.use.one.of", snippets.stream() 1658 .map(sn -> String.format("\n/drop %-5s : %s", sn.id(), sn.source().replace("\n", "\n "))) 1659 .collect(Collectors.joining(", ")) 1660 ); 1661 return false; 1662 } 1663 snippets.stream() 1664 .forEach(sn -> state.drop(sn).forEach(this::handleEvent)); 1665 return true; 1666 } 1667 1668 private boolean cmdEdit(String arg) { 1669 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, 1670 this::mainActive, arg, "/edit"); 1671 if (stream == null) { 1672 return false; 1673 } 1674 Set<String> srcSet = new LinkedHashSet<>(); 1675 stream.forEachOrdered(sn -> { 1676 String src = sn.source(); 1677 switch (sn.subKind()) { 1678 case VAR_VALUE_SUBKIND: 1679 break; 1680 case ASSIGNMENT_SUBKIND: 1681 case OTHER_EXPRESSION_SUBKIND: 1682 case TEMP_VAR_EXPRESSION_SUBKIND: 1683 if (!src.endsWith(";")) { 1684 src = src + ";"; 1685 } 1686 srcSet.add(src); 1687 break; 1688 default: 1689 srcSet.add(src); 1690 break; 1691 } 1692 }); 1693 StringBuilder sb = new StringBuilder(); 1694 for (String s : srcSet) { 1695 sb.append(s); 1696 sb.append('\n'); 1697 } 1698 String src = sb.toString(); 1699 Consumer<String> saveHandler = new SaveHandler(src, srcSet); 1700 Consumer<String> errorHandler = s -> hard("Edit Error: %s", s); 1701 if (editor == null) { 1702 try { 1703 EditPad.edit(errorHandler, src, saveHandler); 1704 } catch (RuntimeException ex) { 1705 errormsg("jshell.err.cant.launch.editor", ex); 1706 fluffmsg("jshell.msg.try.set.editor"); 1707 return false; 1708 } 1709 } else { 1710 ExternalEditor.edit(editor, errorHandler, src, saveHandler, input); 1711 } 1712 return true; 1713 } 1714 //where 1715 // receives editor requests to save 1716 private class SaveHandler implements Consumer<String> { 1717 1718 String src; 1719 Set<String> currSrcs; 1720 1721 SaveHandler(String src, Set<String> ss) { 1722 this.src = src; 1723 this.currSrcs = ss; 1724 } 1725 1726 @Override 1727 public void accept(String s) { 1728 if (!s.equals(src)) { // quick check first 1729 src = s; 1730 try { 1731 Set<String> nextSrcs = new LinkedHashSet<>(); 1732 boolean failed = false; 1733 while (true) { 1734 CompletionInfo an = analysis.analyzeCompletion(s); 1735 if (!an.completeness().isComplete()) { 1736 break; 1737 } 1738 String tsrc = trimNewlines(an.source()); 1739 if (!failed && !currSrcs.contains(tsrc)) { 1740 failed = processCompleteSource(tsrc); 1741 } 1742 nextSrcs.add(tsrc); 1743 if (an.remaining().isEmpty()) { 1744 break; 1745 } 1746 s = an.remaining(); 1747 } 1748 currSrcs = nextSrcs; 1749 } catch (IllegalStateException ex) { 1750 hardmsg("jshell.msg.resetting"); 1751 resetState(); 1752 currSrcs = new LinkedHashSet<>(); // re-process everything 1753 } 1754 } 1755 } 1756 1757 private String trimNewlines(String s) { 1758 int b = 0; 1759 while (b < s.length() && s.charAt(b) == '\n') { 1760 ++b; 1761 } 1762 int e = s.length() -1; 1763 while (e >= 0 && s.charAt(e) == '\n') { 1764 --e; 1765 } 1766 return s.substring(b, e + 1); 1767 } 1768 } 1769 1770 private boolean cmdList(String arg) { 1771 if (arg.length() >= 2 && "-history".startsWith(arg)) { 1772 return cmdHistory(); 1773 } 1774 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, 1775 this::mainActive, arg, "/list"); 1776 if (stream == null) { 1777 return false; 1778 } 1779 1780 // prevent double newline on empty list 1781 boolean[] hasOutput = new boolean[1]; 1782 stream.forEachOrdered(sn -> { 1783 if (!hasOutput[0]) { 1784 cmdout.println(); 1785 hasOutput[0] = true; 1786 } 1787 cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); 1788 }); 1789 return true; 1790 } 1791 1792 private boolean cmdOpen(String filename) { 1793 return runFile(filename, "/open"); 1794 } 1795 1796 private boolean runFile(String filename, String context) { 1797 if (!filename.isEmpty()) { 1798 try { 1799 run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString())); 1800 return true; 1801 } catch (FileNotFoundException e) { 1802 errormsg("jshell.err.file.not.found", context, filename, e.getMessage()); 1803 } catch (Exception e) { 1804 errormsg("jshell.err.file.exception", context, filename, e); 1805 } 1806 } else { 1807 errormsg("jshell.err.file.filename", context); 1808 } 1809 return false; 1810 } 1811 1812 /** 1813 * Read an external file. Error messages accessed via keyPrefix 1814 * 1815 * @param filename file to access or null 1816 * @param context printable non-natural language context for errors 1817 * @return contents of file as string 1818 */ 1819 String readFile(String filename, String context) { 1820 if (filename != null) { 1821 try { 1822 byte[] encoded = Files.readAllBytes(Paths.get(filename)); 1823 return new String(encoded); 1824 } catch (AccessDeniedException e) { 1825 errormsg("jshell.err.file.not.accessible", context, filename, e.getMessage()); 1826 } catch (NoSuchFileException e) { 1827 errormsg("jshell.err.file.not.found", context, filename); 1828 } catch (Exception e) { 1829 errormsg("jshell.err.file.exception", context, filename, e); 1830 } 1831 } else { 1832 errormsg("jshell.err.file.filename", context); 1833 } 1834 return null; 1835 1836 } 1837 1838 private boolean cmdReset() { 1839 live = false; 1840 fluffmsg("jshell.msg.resetting.state"); 1841 return true; 1842 } 1843 1844 private boolean cmdReload(String rawargs) { 1845 ArgTokenizer at = new ArgTokenizer("/reload", rawargs.trim()); 1846 at.allowedOptions("-restore", "-quiet"); 1847 if (!checkOptionsAndRemainingInput(at)) { 1848 return false; 1849 } 1850 Iterable<String> history; 1851 if (at.hasOption("-restore")) { 1852 if (replayableHistoryPrevious == null) { 1853 errormsg("jshell.err.reload.no.previous"); 1854 return false; 1855 } 1856 history = replayableHistoryPrevious; 1857 fluffmsg("jshell.err.reload.restarting.previous.state"); 1858 } else { 1859 history = replayableHistory; 1860 fluffmsg("jshell.err.reload.restarting.state"); 1861 } 1862 boolean echo = !at.hasOption("-quiet"); 1863 resetState(); 1864 run(new ReloadIOContext(history, 1865 echo ? cmdout : null)); 1866 return true; 1867 } 1868 1869 private boolean cmdSave(String rawargs) { 1870 ArgTokenizer at = new ArgTokenizer("/save", rawargs.trim()); 1871 at.allowedOptions("-all", "-start", "-history"); 1872 String filename = at.next(); 1873 if (filename == null) { 1874 errormsg("jshell.err.file.filename", "/save"); 1875 return false; 1876 } 1877 if (!checkOptionsAndRemainingInput(at)) { 1878 return false; 1879 } 1880 if (at.optionCount() > 1) { 1881 errormsg("jshell.err.conflicting.options", at.whole()); 1882 return false; 1883 } 1884 try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename), 1885 Charset.defaultCharset(), 1886 CREATE, TRUNCATE_EXISTING, WRITE)) { 1887 if (at.hasOption("-history")) { 1888 for (String s : input.currentSessionHistory()) { 1889 writer.write(s); 1890 writer.write("\n"); 1891 } 1892 } else if (at.hasOption("-start")) { 1893 writer.append(startup); 1894 } else { 1895 String sources = (at.hasOption("-all") 1896 ? state.snippets() 1897 : state.snippets().filter(this::mainActive)) 1898 .map(Snippet::source) 1899 .collect(Collectors.joining("\n")); 1900 writer.write(sources); 1901 } 1902 } catch (FileNotFoundException e) { 1903 errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage()); 1904 return false; 1905 } catch (Exception e) { 1906 errormsg("jshell.err.file.exception", "/save", filename, e); 1907 return false; 1908 } 1909 return true; 1910 } 1911 1912 private boolean cmdVars(String arg) { 1913 Stream<VarSnippet> stream = argsOptionsToSnippets(this::allVarSnippets, 1914 this::isActive, arg, "/vars"); 1915 if (stream == null) { 1916 return false; 1917 } 1918 stream.forEachOrdered(vk -> 1919 { 1920 String val = state.status(vk) == Status.VALID 1921 ? state.varValue(vk) 1922 : "jshell.msg.vars.not.active"; 1923 hard(" %s %s = %s", vk.typeName(), vk.name(), val); 1924 }); 1925 return true; 1926 } 1927 1928 private boolean cmdMethods(String arg) { 1929 Stream<MethodSnippet> stream = argsOptionsToSnippets(this::allMethodSnippets, 1930 this::isActive, arg, "/methods"); 1931 if (stream == null) { 1932 return false; 1933 } 1934 stream.forEachOrdered(mk 1935 -> hard(" %s %s", mk.name(), mk.signature()) 1936 ); 1937 return true; 1938 } 1939 1940 private boolean cmdTypes(String arg) { 1941 Stream<TypeDeclSnippet> stream = argsOptionsToSnippets(this::allTypeSnippets, 1942 this::isActive, arg, "/types"); 1943 if (stream == null) { 1944 return false; 1945 } 1946 stream.forEachOrdered(ck 1947 -> { 1948 String kind; 1949 switch (ck.subKind()) { 1950 case INTERFACE_SUBKIND: 1951 kind = "interface"; 1952 break; 1953 case CLASS_SUBKIND: 1954 kind = "class"; 1955 break; 1956 case ENUM_SUBKIND: 1957 kind = "enum"; 1958 break; 1959 case ANNOTATION_TYPE_SUBKIND: 1960 kind = "@interface"; 1961 break; 1962 default: 1963 assert false : "Wrong kind" + ck.subKind(); 1964 kind = "class"; 1965 break; 1966 } 1967 hard(" %s %s", kind, ck.name()); 1968 }); 1969 return true; 1970 } 1971 1972 private boolean cmdImports() { 1973 state.imports().forEach(ik -> { 1974 hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname()); 1975 }); 1976 return true; 1977 } 1978 1979 private boolean cmdUseHistoryEntry(int index) { 1980 List<Snippet> keys = state.snippets().collect(toList()); 1981 if (index < 0) 1982 index += keys.size(); 1983 else 1984 index--; 1985 if (index >= 0 && index < keys.size()) { 1986 rerunSnippet(keys.get(index)); 1987 } else { 1988 errormsg("jshell.err.out.of.range"); 1989 return false; 1990 } 1991 return true; 1992 } 1993 1994 boolean checkOptionsAndRemainingInput(ArgTokenizer at) { 1995 String junk = at.remainder(); 1996 if (!junk.isEmpty()) { 1997 errormsg("jshell.err.unexpected.at.end", junk, at.whole()); 1998 return false; 1999 } else { 2000 String bad = at.badOptions(); 2001 if (!bad.isEmpty()) { 2002 errormsg("jshell.err.unknown.option", bad, at.whole()); 2003 return false; 2004 } 2005 } 2006 return true; 2007 } 2008 2009 private boolean rerunHistoryEntryById(String id) { 2010 Optional<Snippet> snippet = state.snippets() 2011 .filter(s -> s.id().equals(id)) 2012 .findFirst(); 2013 return snippet.map(s -> { 2014 rerunSnippet(s); 2015 return true; 2016 }).orElse(false); 2017 } 2018 2019 private void rerunSnippet(Snippet snippet) { 2020 String source = snippet.source(); 2021 cmdout.printf("%s\n", source); 2022 input.replaceLastHistoryEntry(source); 2023 processSourceCatchingReset(source); 2024 } 2025 2026 /** 2027 * Filter diagnostics for only errors (no warnings, ...) 2028 * @param diagnostics input list 2029 * @return filtered list 2030 */ 2031 List<Diag> errorsOnly(List<Diag> diagnostics) { 2032 return diagnostics.stream() 2033 .filter(d -> d.isError()) 2034 .collect(toList()); 2035 } 2036 2037 void displayDiagnostics(String source, Diag diag, List<String> toDisplay) { 2038 for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize 2039 if (!line.trim().startsWith("location:")) { 2040 toDisplay.add(line); 2041 } 2042 } 2043 2044 int pstart = (int) diag.getStartPosition(); 2045 int pend = (int) diag.getEndPosition(); 2046 Matcher m = LINEBREAK.matcher(source); 2047 int pstartl = 0; 2048 int pendl = -2; 2049 while (m.find(pstartl)) { 2050 pendl = m.start(); 2051 if (pendl >= pstart) { 2052 break; 2053 } else { 2054 pstartl = m.end(); 2055 } 2056 } 2057 if (pendl < pstart) { 2058 pendl = source.length(); 2059 } 2060 toDisplay.add(source.substring(pstartl, pendl)); 2061 2062 StringBuilder sb = new StringBuilder(); 2063 int start = pstart - pstartl; 2064 for (int i = 0; i < start; ++i) { 2065 sb.append(' '); 2066 } 2067 sb.append('^'); 2068 boolean multiline = pend > pendl; 2069 int end = (multiline ? pendl : pend) - pstartl - 1; 2070 if (end > start) { 2071 for (int i = start + 1; i < end; ++i) { 2072 sb.append('-'); 2073 } 2074 if (multiline) { 2075 sb.append("-..."); 2076 } else { 2077 sb.append('^'); 2078 } 2079 } 2080 toDisplay.add(sb.toString()); 2081 2082 debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this); 2083 debug("Code: %s", diag.getCode()); 2084 debug("Pos: %d (%d - %d)", diag.getPosition(), 2085 diag.getStartPosition(), diag.getEndPosition()); 2086 } 2087 2088 private String processSource(String srcInput) throws IllegalStateException { 2089 while (true) { 2090 CompletionInfo an = analysis.analyzeCompletion(srcInput); 2091 if (!an.completeness().isComplete()) { 2092 return an.remaining(); 2093 } 2094 boolean failed = processCompleteSource(an.source()); 2095 if (failed || an.remaining().isEmpty()) { 2096 return ""; 2097 } 2098 srcInput = an.remaining(); 2099 } 2100 } 2101 //where 2102 private boolean processCompleteSource(String source) throws IllegalStateException { 2103 debug("Compiling: %s", source); 2104 boolean failed = false; 2105 boolean isActive = false; 2106 List<SnippetEvent> events = state.eval(source); 2107 for (SnippetEvent e : events) { 2108 // Report the event, recording failure 2109 failed |= handleEvent(e); 2110 2111 // If any main snippet is active, this should be replayable 2112 // also ignore var value queries 2113 isActive |= e.causeSnippet() == null && 2114 e.status().isActive() && 2115 e.snippet().subKind() != VAR_VALUE_SUBKIND; 2116 } 2117 // If this is an active snippet and it didn't cause the backend to die, 2118 // add it to the replayable history 2119 if (isActive && live) { 2120 addToReplayHistory(source); 2121 } 2122 2123 return failed; 2124 } 2125 2126 // Handle incoming snippet events -- return true on failure 2127 private boolean handleEvent(SnippetEvent ste) { 2128 Snippet sn = ste.snippet(); 2129 if (sn == null) { 2130 debug("Event with null key: %s", ste); 2131 return false; 2132 } 2133 List<Diag> diagnostics = state.diagnostics(sn).collect(toList()); 2134 String source = sn.source(); 2135 if (ste.causeSnippet() == null) { 2136 // main event 2137 for (Diag d : diagnostics) { 2138 hardmsg(d.isError()? "jshell.msg.error" : "jshell.msg.warning"); 2139 List<String> disp = new ArrayList<>(); 2140 displayDiagnostics(source, d, disp); 2141 disp.stream() 2142 .forEach(l -> hard("%s", l)); 2143 } 2144 2145 if (ste.status() != Status.REJECTED) { 2146 if (ste.exception() != null) { 2147 if (ste.exception() instanceof EvalException) { 2148 printEvalException((EvalException) ste.exception()); 2149 return true; 2150 } else if (ste.exception() instanceof UnresolvedReferenceException) { 2151 printUnresolvedException((UnresolvedReferenceException) ste.exception()); 2152 } else { 2153 hard("Unexpected execution exception: %s", ste.exception()); 2154 return true; 2155 } 2156 } else { 2157 new DisplayEvent(ste, false, ste.value(), diagnostics).displayDeclarationAndValue(); 2158 } 2159 } else { 2160 if (diagnostics.isEmpty()) { 2161 errormsg("jshell.err.failed"); 2162 } 2163 return true; 2164 } 2165 } else { 2166 // Update 2167 if (sn instanceof DeclarationSnippet) { 2168 List<Diag> other = errorsOnly(diagnostics); 2169 2170 // display update information 2171 new DisplayEvent(ste, true, ste.value(), other).displayDeclarationAndValue(); 2172 } 2173 } 2174 return false; 2175 } 2176 //where 2177 void printStackTrace(StackTraceElement[] stes) { 2178 for (StackTraceElement ste : stes) { 2179 StringBuilder sb = new StringBuilder(); 2180 String cn = ste.getClassName(); 2181 if (!cn.isEmpty()) { 2182 int dot = cn.lastIndexOf('.'); 2183 if (dot > 0) { 2184 sb.append(cn.substring(dot + 1)); 2185 } else { 2186 sb.append(cn); 2187 } 2188 sb.append("."); 2189 } 2190 if (!ste.getMethodName().isEmpty()) { 2191 sb.append(ste.getMethodName()); 2192 sb.append(" "); 2193 } 2194 String fileName = ste.getFileName(); 2195 int lineNumber = ste.getLineNumber(); 2196 String loc = ste.isNativeMethod() 2197 ? getResourceString("jshell.msg.native.method") 2198 : fileName == null 2199 ? getResourceString("jshell.msg.unknown.source") 2200 : lineNumber >= 0 2201 ? fileName + ":" + lineNumber 2202 : fileName; 2203 hard(" at %s(%s)", sb, loc); 2204 2205 } 2206 } 2207 //where 2208 void printUnresolvedException(UnresolvedReferenceException ex) { 2209 DeclarationSnippet corralled = ex.getSnippet(); 2210 List<Diag> otherErrors = errorsOnly(state.diagnostics(corralled).collect(toList())); 2211 new DisplayEvent(corralled, state.status(corralled), FormatAction.USED, true, null, otherErrors) 2212 .displayDeclarationAndValue(); 2213 } 2214 //where 2215 void printEvalException(EvalException ex) { 2216 if (ex.getMessage() == null) { 2217 hard("%s thrown", ex.getExceptionClassName()); 2218 } else { 2219 hard("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage()); 2220 } 2221 printStackTrace(ex.getStackTrace()); 2222 } 2223 2224 private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) { 2225 FormatAction act; 2226 switch (status) { 2227 case VALID: 2228 case RECOVERABLE_DEFINED: 2229 case RECOVERABLE_NOT_DEFINED: 2230 if (previousStatus.isActive()) { 2231 act = isSignatureChange 2232 ? FormatAction.REPLACED 2233 : FormatAction.MODIFIED; 2234 } else { 2235 act = FormatAction.ADDED; 2236 } 2237 break; 2238 case OVERWRITTEN: 2239 act = FormatAction.OVERWROTE; 2240 break; 2241 case DROPPED: 2242 act = FormatAction.DROPPED; 2243 break; 2244 case REJECTED: 2245 case NONEXISTENT: 2246 default: 2247 // Should not occur 2248 error("Unexpected status: " + previousStatus.toString() + "=>" + status.toString()); 2249 act = FormatAction.DROPPED; 2250 } 2251 return act; 2252 } 2253 2254 class DisplayEvent { 2255 private final Snippet sn; 2256 private final FormatAction action; 2257 private final boolean update; 2258 private final String value; 2259 private final List<String> errorLines; 2260 private final FormatResolve resolution; 2261 private final String unresolved; 2262 private final FormatUnresolved unrcnt; 2263 private final FormatErrors errcnt; 2264 2265 DisplayEvent(SnippetEvent ste, boolean update, String value, List<Diag> errors) { 2266 this(ste.snippet(), ste.status(), toAction(ste.status(), ste.previousStatus(), ste.isSignatureChange()), update, value, errors); 2267 } 2268 2269 DisplayEvent(Snippet sn, Status status, FormatAction action, boolean update, String value, List<Diag> errors) { 2270 this.sn = sn; 2271 this.action = action; 2272 this.update = update; 2273 this.value = value; 2274 this.errorLines = new ArrayList<>(); 2275 for (Diag d : errors) { 2276 displayDiagnostics(sn.source(), d, errorLines); 2277 } 2278 long unresolvedCount; 2279 if (sn instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) { 2280 resolution = (status == Status.RECOVERABLE_NOT_DEFINED) 2281 ? FormatResolve.NOTDEFINED 2282 : FormatResolve.DEFINED; 2283 unresolved = unresolved((DeclarationSnippet) sn); 2284 unresolvedCount = state.unresolvedDependencies((DeclarationSnippet) sn).count(); 2285 } else { 2286 resolution = FormatResolve.OK; 2287 unresolved = ""; 2288 unresolvedCount = 0; 2289 } 2290 unrcnt = unresolvedCount == 0 2291 ? FormatUnresolved.UNRESOLVED0 2292 : unresolvedCount == 1 2293 ? FormatUnresolved.UNRESOLVED1 2294 : FormatUnresolved.UNRESOLVED2; 2295 errcnt = errors.isEmpty() 2296 ? FormatErrors.ERROR0 2297 : errors.size() == 1 2298 ? FormatErrors.ERROR1 2299 : FormatErrors.ERROR2; 2300 } 2301 2302 private String unresolved(DeclarationSnippet key) { 2303 List<String> unr = state.unresolvedDependencies(key).collect(toList()); 2304 StringBuilder sb = new StringBuilder(); 2305 int fromLast = unr.size(); 2306 if (fromLast > 0) { 2307 sb.append(" "); 2308 } 2309 for (String u : unr) { 2310 --fromLast; 2311 sb.append(u); 2312 switch (fromLast) { 2313 // No suffix 2314 case 0: 2315 break; 2316 case 1: 2317 sb.append(", and "); 2318 break; 2319 default: 2320 sb.append(", "); 2321 break; 2322 } 2323 } 2324 return sb.toString(); 2325 } 2326 2327 private void custom(FormatCase fcase, String name) { 2328 custom(fcase, name, null); 2329 } 2330 2331 private void custom(FormatCase fcase, String name, String type) { 2332 String display = feedback.format(fcase, action, (update ? FormatWhen.UPDATE : FormatWhen.PRIMARY), 2333 resolution, unrcnt, errcnt, 2334 name, type, value, unresolved, errorLines); 2335 if (interactive()) { 2336 cmdout.print(display); 2337 } 2338 } 2339 2340 @SuppressWarnings("fallthrough") 2341 private void displayDeclarationAndValue() { 2342 switch (sn.subKind()) { 2343 case CLASS_SUBKIND: 2344 custom(FormatCase.CLASS, ((TypeDeclSnippet) sn).name()); 2345 break; 2346 case INTERFACE_SUBKIND: 2347 custom(FormatCase.INTERFACE, ((TypeDeclSnippet) sn).name()); 2348 break; 2349 case ENUM_SUBKIND: 2350 custom(FormatCase.ENUM, ((TypeDeclSnippet) sn).name()); 2351 break; 2352 case ANNOTATION_TYPE_SUBKIND: 2353 custom(FormatCase.ANNOTATION, ((TypeDeclSnippet) sn).name()); 2354 break; 2355 case METHOD_SUBKIND: 2356 custom(FormatCase.METHOD, ((MethodSnippet) sn).name(), ((MethodSnippet) sn).parameterTypes()); 2357 break; 2358 case VAR_DECLARATION_SUBKIND: { 2359 VarSnippet vk = (VarSnippet) sn; 2360 custom(FormatCase.VARDECL, vk.name(), vk.typeName()); 2361 break; 2362 } 2363 case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: { 2364 VarSnippet vk = (VarSnippet) sn; 2365 custom(FormatCase.VARINIT, vk.name(), vk.typeName()); 2366 break; 2367 } 2368 case TEMP_VAR_EXPRESSION_SUBKIND: { 2369 VarSnippet vk = (VarSnippet) sn; 2370 custom(FormatCase.EXPRESSION, vk.name(), vk.typeName()); 2371 break; 2372 } 2373 case OTHER_EXPRESSION_SUBKIND: 2374 error("Unexpected expression form -- value is: %s", (value)); 2375 break; 2376 case VAR_VALUE_SUBKIND: { 2377 ExpressionSnippet ek = (ExpressionSnippet) sn; 2378 custom(FormatCase.VARVALUE, ek.name(), ek.typeName()); 2379 break; 2380 } 2381 case ASSIGNMENT_SUBKIND: { 2382 ExpressionSnippet ek = (ExpressionSnippet) sn; 2383 custom(FormatCase.ASSIGNMENT, ek.name(), ek.typeName()); 2384 break; 2385 } 2386 case SINGLE_TYPE_IMPORT_SUBKIND: 2387 case TYPE_IMPORT_ON_DEMAND_SUBKIND: 2388 case SINGLE_STATIC_IMPORT_SUBKIND: 2389 case STATIC_IMPORT_ON_DEMAND_SUBKIND: 2390 custom(FormatCase.IMPORT, ((ImportSnippet) sn).name()); 2391 break; 2392 case STATEMENT_SUBKIND: 2393 custom(FormatCase.STATEMENT, null); 2394 break; 2395 } 2396 } 2397 } 2398 2399 /** The current version number as a string. 2400 */ 2401 String version() { 2402 return version("release"); // mm.nn.oo[-milestone] 2403 } 2404 2405 /** The current full version number as a string. 2406 */ 2407 String fullVersion() { 2408 return version("full"); // mm.mm.oo[-milestone]-build 2409 } 2410 2411 private String version(String key) { 2412 if (versionRB == null) { 2413 try { 2414 versionRB = ResourceBundle.getBundle(VERSION_RB_NAME, locale); 2415 } catch (MissingResourceException e) { 2416 return "(version info not available)"; 2417 } 2418 } 2419 try { 2420 return versionRB.getString(key); 2421 } 2422 catch (MissingResourceException e) { 2423 return "(version info not available)"; 2424 } 2425 } 2426 2427 class NameSpace { 2428 final String spaceName; 2429 final String prefix; 2430 private int nextNum; 2431 2432 NameSpace(String spaceName, String prefix) { 2433 this.spaceName = spaceName; 2434 this.prefix = prefix; 2435 this.nextNum = 1; 2436 } 2437 2438 String tid(Snippet sn) { 2439 String tid = prefix + nextNum++; 2440 mapSnippet.put(sn, new SnippetInfo(sn, this, tid)); 2441 return tid; 2442 } 2443 2444 String tidNext() { 2445 return prefix + nextNum; 2446 } 2447 } 2448 2449 static class SnippetInfo { 2450 final Snippet snippet; 2451 final NameSpace space; 2452 final String tid; 2453 2454 SnippetInfo(Snippet snippet, NameSpace space, String tid) { 2455 this.snippet = snippet; 2456 this.space = space; 2457 this.tid = tid; 2458 } 2459 } 2460 2461 private static class ArgSuggestion implements Suggestion { 2462 2463 private final String continuation; 2464 2465 /** 2466 * Create a {@code Suggestion} instance. 2467 * 2468 * @param continuation a candidate continuation of the user's input 2469 */ 2470 public ArgSuggestion(String continuation) { 2471 this.continuation = continuation; 2472 } 2473 2474 /** 2475 * The candidate continuation of the given user's input. 2476 * 2477 * @return the continuation string 2478 */ 2479 @Override 2480 public String continuation() { 2481 return continuation; 2482 } 2483 2484 /** 2485 * Indicates whether input continuation matches the target type and is thus 2486 * more likely to be the desired continuation. A matching continuation is 2487 * preferred. 2488 * 2489 * @return {@code false}, non-types analysis 2490 */ 2491 @Override 2492 public boolean matchesType() { 2493 return false; 2494 } 2495 } 2496 } 2497 2498 abstract class NonInteractiveIOContext extends IOContext { 2499 2500 @Override 2501 public boolean interactiveOutput() { 2502 return false; 2503 } 2504 2505 @Override 2506 public Iterable<String> currentSessionHistory() { 2507 return Collections.emptyList(); 2508 } 2509 2510 @Override 2511 public boolean terminalEditorRunning() { 2512 return false; 2513 } 2514 2515 @Override 2516 public void suspend() { 2517 } 2518 2519 @Override 2520 public void resume() { 2521 } 2522 2523 @Override 2524 public void beforeUserCode() { 2525 } 2526 2527 @Override 2528 public void afterUserCode() { 2529 } 2530 2531 @Override 2532 public void replaceLastHistoryEntry(String source) { 2533 } 2534 } 2535 2536 class ScannerIOContext extends NonInteractiveIOContext { 2537 private final Scanner scannerIn; 2538 2539 ScannerIOContext(Scanner scannerIn) { 2540 this.scannerIn = scannerIn; 2541 } 2542 2543 @Override 2544 public String readLine(String prompt, String prefix) { 2545 if (scannerIn.hasNextLine()) { 2546 return scannerIn.nextLine(); 2547 } else { 2548 return null; 2549 } 2550 } 2551 2552 @Override 2553 public void close() { 2554 scannerIn.close(); 2555 } 2556 } 2557 2558 class FileScannerIOContext extends ScannerIOContext { 2559 2560 FileScannerIOContext(String fn) throws FileNotFoundException { 2561 this(new FileReader(fn)); 2562 } 2563 2564 FileScannerIOContext(Reader rdr) throws FileNotFoundException { 2565 super(new Scanner(rdr)); 2566 } 2567 } 2568 2569 class ReloadIOContext extends NonInteractiveIOContext { 2570 private final Iterator<String> it; 2571 private final PrintStream echoStream; 2572 2573 ReloadIOContext(Iterable<String> history, PrintStream echoStream) { 2574 this.it = history.iterator(); 2575 this.echoStream = echoStream; 2576 } 2577 2578 @Override 2579 public String readLine(String prompt, String prefix) { 2580 String s = it.hasNext() 2581 ? it.next() 2582 : null; 2583 if (echoStream != null && s != null) { 2584 String p = "-: "; 2585 String p2 = "\n "; 2586 echoStream.printf("%s%s\n", p, s.replace("\n", p2)); 2587 } 2588 return s; 2589 } 2590 2591 @Override 2592 public void close() { 2593 } 2594 }