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