JShellTool.java revision 3441:6d69114ec506
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 26package jdk.internal.jshell.tool; 27 28import java.io.BufferedWriter; 29import java.io.ByteArrayInputStream; 30import java.io.File; 31import java.io.FileNotFoundException; 32import java.io.FileReader; 33import java.io.IOException; 34import java.io.InputStream; 35import java.io.PrintStream; 36import java.io.Reader; 37import java.io.StringReader; 38import java.nio.charset.Charset; 39import java.nio.file.AccessDeniedException; 40import java.nio.file.FileSystems; 41import java.nio.file.Files; 42import java.nio.file.NoSuchFileException; 43import java.nio.file.Path; 44import java.nio.file.Paths; 45import java.text.MessageFormat; 46import java.util.ArrayList; 47import java.util.Arrays; 48import java.util.Collections; 49import java.util.Iterator; 50import java.util.LinkedHashMap; 51import java.util.LinkedHashSet; 52import java.util.List; 53import java.util.Locale; 54import java.util.Map; 55import java.util.Map.Entry; 56import java.util.Scanner; 57import java.util.Set; 58import java.util.function.Consumer; 59import java.util.function.Predicate; 60import java.util.prefs.Preferences; 61import java.util.regex.Matcher; 62import java.util.regex.Pattern; 63import java.util.stream.Collectors; 64import java.util.stream.Stream; 65import java.util.stream.StreamSupport; 66 67import jdk.internal.jshell.debug.InternalDebugControl; 68import jdk.internal.jshell.tool.IOContext.InputInterruptedException; 69import jdk.jshell.DeclarationSnippet; 70import jdk.jshell.Diag; 71import jdk.jshell.EvalException; 72import jdk.jshell.ExpressionSnippet; 73import jdk.jshell.ImportSnippet; 74import jdk.jshell.JShell; 75import jdk.jshell.JShell.Subscription; 76import jdk.jshell.MethodSnippet; 77import jdk.jshell.PersistentSnippet; 78import jdk.jshell.Snippet; 79import jdk.jshell.Snippet.Status; 80import jdk.jshell.SnippetEvent; 81import jdk.jshell.SourceCodeAnalysis; 82import jdk.jshell.SourceCodeAnalysis.CompletionInfo; 83import jdk.jshell.SourceCodeAnalysis.Suggestion; 84import jdk.jshell.TypeDeclSnippet; 85import jdk.jshell.UnresolvedReferenceException; 86import jdk.jshell.VarSnippet; 87 88import static java.nio.file.StandardOpenOption.CREATE; 89import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; 90import static java.nio.file.StandardOpenOption.WRITE; 91import java.util.MissingResourceException; 92import java.util.Optional; 93import java.util.ResourceBundle; 94import java.util.Spliterators; 95import java.util.function.Function; 96import java.util.function.Supplier; 97import jdk.internal.jshell.tool.Feedback.FormatAction; 98import jdk.internal.jshell.tool.Feedback.FormatCase; 99import jdk.internal.jshell.tool.Feedback.FormatErrors; 100import jdk.internal.jshell.tool.Feedback.FormatResolve; 101import jdk.internal.jshell.tool.Feedback.FormatUnresolved; 102import jdk.internal.jshell.tool.Feedback.FormatWhen; 103import static java.util.stream.Collectors.toList; 104import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND; 105import static java.util.stream.Collectors.toMap; 106import static jdk.internal.jshell.debug.InternalDebugControl.DBG_COMPA; 107import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP; 108import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT; 109import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR; 110import 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 */ 116public 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 Suggestion(alternative, false)); 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 Suggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : ""), false)) 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 Suggestion(root.toString(), false)) 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<List<? extends Snippet>> snippetsSupplier) { 977 return (prefix, cursor, anchor) -> { 978 anchor[0] = 0; 979 return snippetsSupplier.get() .stream() 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 Suggestion(k, false)) 985 .collect(Collectors.toList()); 986 }; 987 } 988 989 private CompletionProvider snippetKeywordCompletion(Supplier<List<? 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 List<Snippet> allSnippets() { 1024 return state.snippets(); 1025 } 1026 1027 List<PersistentSnippet> dropableSnippets() { 1028 return state.snippets().stream() 1029 .filter(sn -> state.status(sn).isActive && sn instanceof PersistentSnippet) 1030 .map(sn -> (PersistentSnippet) sn) 1031 .collect(toList()); 1032 } 1033 1034 List<VarSnippet> allVarSnippets() { 1035 return state.snippets().stream() 1036 .filter(sn -> sn.kind() == Snippet.Kind.VAR) 1037 .map(sn -> (VarSnippet) sn) 1038 .collect(toList()); 1039 } 1040 1041 List<MethodSnippet> allMethodSnippets() { 1042 return state.snippets().stream() 1043 .filter(sn -> sn.kind() == Snippet.Kind.METHOD) 1044 .map(sn -> (MethodSnippet) sn) 1045 .collect(toList()); 1046 } 1047 1048 List<TypeDeclSnippet> allTypeSnippets() { 1049 return state.snippets().stream() 1050 .filter(sn -> sn.kind() == Snippet.Kind.TYPE_DECL) 1051 .map(sn -> (TypeDeclSnippet) sn) 1052 .collect(toList()); 1053 } 1054 1055 // Table of commands -- with command forms, argument kinds, helpKey message, implementation, ... 1056 1057 { 1058 registerCommand(new Command("/list", 1059 arg -> cmdList(arg), 1060 snippetKeywordCompletion(this::allSnippets))); 1061 registerCommand(new Command("/edit", 1062 arg -> cmdEdit(arg), 1063 snippetCompletion(this::allSnippets))); 1064 registerCommand(new Command("/drop", 1065 arg -> cmdDrop(arg), 1066 snippetCompletion(this::dropableSnippets), 1067 CommandKind.REPLAY)); 1068 registerCommand(new Command("/save", 1069 arg -> cmdSave(arg), 1070 saveCompletion())); 1071 registerCommand(new Command("/open", 1072 arg -> cmdOpen(arg), 1073 FILE_COMPLETION_PROVIDER)); 1074 registerCommand(new Command("/vars", 1075 arg -> cmdVars(arg), 1076 snippetKeywordCompletion(this::allVarSnippets))); 1077 registerCommand(new Command("/methods", 1078 arg -> cmdMethods(arg), 1079 snippetKeywordCompletion(this::allMethodSnippets))); 1080 registerCommand(new Command("/types", 1081 arg -> cmdTypes(arg), 1082 snippetKeywordCompletion(this::allTypeSnippets))); 1083 registerCommand(new Command("/imports", 1084 arg -> cmdImports(), 1085 EMPTY_COMPLETION_PROVIDER)); 1086 registerCommand(new Command("/exit", 1087 arg -> cmdExit(), 1088 EMPTY_COMPLETION_PROVIDER)); 1089 registerCommand(new Command("/reset", 1090 arg -> cmdReset(), 1091 EMPTY_COMPLETION_PROVIDER)); 1092 registerCommand(new Command("/reload", 1093 arg -> cmdReload(arg), 1094 reloadCompletion())); 1095 registerCommand(new Command("/classpath", 1096 arg -> cmdClasspath(arg), 1097 classPathCompletion(), 1098 CommandKind.REPLAY)); 1099 registerCommand(new Command("/history", 1100 arg -> cmdHistory(), 1101 EMPTY_COMPLETION_PROVIDER)); 1102 registerCommand(new Command("/debug", 1103 arg -> cmdDebug(arg), 1104 EMPTY_COMPLETION_PROVIDER, 1105 CommandKind.HIDDEN)); 1106 registerCommand(new Command("/help", 1107 arg -> cmdHelp(arg), 1108 EMPTY_COMPLETION_PROVIDER)); 1109 registerCommand(new Command("/set", 1110 arg -> cmdSet(arg), 1111 new FixedCompletionProvider(SET_SUBCOMMANDS))); 1112 registerCommand(new Command("/retain", 1113 arg -> cmdRetain(arg), 1114 new FixedCompletionProvider(RETAIN_SUBCOMMANDS))); 1115 registerCommand(new Command("/?", 1116 "help.quest", 1117 arg -> cmdHelp(arg), 1118 EMPTY_COMPLETION_PROVIDER, 1119 CommandKind.NORMAL)); 1120 registerCommand(new Command("/!", 1121 "help.bang", 1122 arg -> cmdUseHistoryEntry(-1), 1123 EMPTY_COMPLETION_PROVIDER, 1124 CommandKind.NORMAL)); 1125 1126 // Documentation pseudo-commands 1127 registerCommand(new Command("/<id>", 1128 "help.id", 1129 CommandKind.HELP_ONLY)); 1130 registerCommand(new Command("/-<n>", 1131 "help.previous", 1132 CommandKind.HELP_ONLY)); 1133 registerCommand(new Command("intro", 1134 "help.intro", 1135 CommandKind.HELP_SUBJECT)); 1136 registerCommand(new Command("shortcuts", 1137 "help.shortcuts", 1138 CommandKind.HELP_SUBJECT)); 1139 } 1140 1141 public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) { 1142 String prefix = code.substring(0, cursor); 1143 int space = prefix.indexOf(' '); 1144 Stream<Suggestion> result; 1145 1146 if (space == (-1)) { 1147 result = commands.values() 1148 .stream() 1149 .distinct() 1150 .filter(cmd -> cmd.kind.shouldSuggestCompletions) 1151 .map(cmd -> cmd.command) 1152 .filter(key -> key.startsWith(prefix)) 1153 .map(key -> new Suggestion(key + " ", false)); 1154 anchor[0] = 0; 1155 } else { 1156 String arg = prefix.substring(space + 1); 1157 String cmd = prefix.substring(0, space); 1158 Command[] candidates = findCommand(cmd, c -> true); 1159 if (candidates.length == 1) { 1160 result = candidates[0].completions.completionSuggestions(arg, cursor - space, anchor).stream(); 1161 anchor[0] += space + 1; 1162 } else { 1163 result = Stream.empty(); 1164 } 1165 } 1166 1167 return result.sorted((s1, s2) -> s1.continuation.compareTo(s2.continuation)) 1168 .collect(Collectors.toList()); 1169 } 1170 1171 public String commandDocumentation(String code, int cursor) { 1172 code = code.substring(0, cursor); 1173 int space = code.indexOf(' '); 1174 1175 if (space != (-1)) { 1176 String cmd = code.substring(0, space); 1177 Command command = commands.get(cmd); 1178 if (command != null) { 1179 return getResourceString(command.helpKey + ".summary"); 1180 } 1181 } 1182 1183 return null; 1184 } 1185 1186 // --- Command implementations --- 1187 1188 private static final String[] SET_SUBCOMMANDS = new String[]{ 1189 "format", "truncation", "feedback", "mode", "prompt", "editor", "start"}; 1190 1191 private static final String[] RETAIN_SUBCOMMANDS = new String[]{ 1192 "feedback", "mode", "editor", "start"}; 1193 1194 final boolean cmdSet(String arg) { 1195 String cmd = "/set"; 1196 ArgTokenizer at = new ArgTokenizer(cmd, arg.trim()); 1197 String which = subCommand(cmd, at, SET_SUBCOMMANDS); 1198 if (which == null) { 1199 return false; 1200 } 1201 switch (which) { 1202 case "format": 1203 return feedback.setFormat(this, at); 1204 case "truncation": 1205 return feedback.setTruncation(this, at); 1206 case "feedback": 1207 return feedback.setFeedback(this, at); 1208 case "mode": 1209 return feedback.setMode(this, at); 1210 case "prompt": 1211 return feedback.setPrompt(this, at); 1212 case "editor": 1213 return setEditor(at, true); 1214 case "start": 1215 return setStart(cmd, at, true); 1216 default: 1217 errormsg("jshell.err.arg", cmd, at.val()); 1218 return false; 1219 } 1220 } 1221 1222 final boolean cmdRetain(String arg) { 1223 String cmd = "/retain"; 1224 ArgTokenizer at = new ArgTokenizer(cmd, arg.trim()); 1225 String which = subCommand(cmd, at, RETAIN_SUBCOMMANDS); 1226 if (which == null) { 1227 return false; 1228 } 1229 switch (which) { 1230 case "feedback": { 1231 String fb = feedback.retainFeedback(this, at); 1232 if (fb != null) { 1233 // If a feedback mode has been set now, or in the past, retain it 1234 prefs.put(FEEDBACK_KEY, fb); 1235 return true; 1236 } 1237 return false; 1238 } 1239 case "mode": 1240 String retained = feedback.retainMode(this, at); 1241 if (retained != null) { 1242 // Retain this mode and all previously retained modes 1243 prefs.put(MODE_KEY, retained); 1244 return true; 1245 } 1246 return false; 1247 case "editor": 1248 if (!setEditor(at, false)) { 1249 return false; 1250 } 1251 // retain editor setting 1252 prefs.put(EDITOR_KEY, (editor == null) 1253 ? "" 1254 : String.join(RECORD_SEPARATOR, editor)); 1255 return true; 1256 case "start": { 1257 if (!setStart(cmd, at, false)) { 1258 return false; 1259 } 1260 // retain startup setting 1261 prefs.put(STARTUP_KEY, startup); 1262 return true; 1263 } 1264 default: 1265 errormsg("jshell.err.arg", cmd, at.val()); 1266 return false; 1267 } 1268 } 1269 1270 // Print the help doc for the specified sub-command 1271 boolean printSubCommandHelp(String cmd, ArgTokenizer at, String helpPrefix, String[] subs) { 1272 String which = subCommand(cmd, at, subs); 1273 if (which == null) { 1274 return false; 1275 } 1276 hardrb(helpPrefix + which); 1277 return true; 1278 } 1279 1280 // Find which, if any, sub-command matches 1281 String subCommand(String cmd, ArgTokenizer at, String[] subs) { 1282 String[] matches = at.next(subs); 1283 if (matches == null) { 1284 // No sub-command was given 1285 errormsg("jshell.err.sub.arg", cmd); 1286 return null; 1287 } 1288 if (matches.length == 0) { 1289 // There are no matching sub-commands 1290 errormsg("jshell.err.arg", cmd, at.val()); 1291 fluffmsg("jshell.msg.use.one.of", Arrays.stream(subs) 1292 .collect(Collectors.joining(", ")) 1293 ); 1294 return null; 1295 } 1296 if (matches.length > 1) { 1297 // More than one sub-command matches the initial characters provided 1298 errormsg("jshell.err.sub.ambiguous", cmd, at.val()); 1299 fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches) 1300 .collect(Collectors.joining(", ")) 1301 ); 1302 return null; 1303 } 1304 return matches[0]; 1305 } 1306 1307 // The sub-command: /set editor <editor-command-line>> 1308 boolean setEditor(ArgTokenizer at, boolean argsRequired) { 1309 at.allowedOptions("-default"); 1310 String prog = at.next(); 1311 List<String> ed = new ArrayList<>(); 1312 while (at.val() != null) { 1313 ed.add(at.val()); 1314 at.nextToken(); 1315 } 1316 if (!checkOptionsAndRemainingInput(at)) { 1317 return false; 1318 } 1319 boolean defaultOption = at.hasOption("-default"); 1320 if (prog != null) { 1321 if (defaultOption) { 1322 errormsg("jshell.err.default.option.or.program", at.whole()); 1323 return false; 1324 } 1325 editor = ed.toArray(new String[ed.size()]); 1326 fluffmsg("jshell.msg.set.editor.set", prog); 1327 } else if (defaultOption) { 1328 editor = null; 1329 } else if (argsRequired) { 1330 errormsg("jshell.err.set.editor.arg"); 1331 return false; 1332 } 1333 return true; 1334 } 1335 1336 // The sub-command: /set start <start-file> 1337 boolean setStart(String cmd, ArgTokenizer at, boolean argsRequired) { 1338 at.allowedOptions("-default", "-none"); 1339 String fn = at.next(); 1340 if (!checkOptionsAndRemainingInput(at)) { 1341 return false; 1342 } 1343 int argCount = at.optionCount() + ((fn != null) ? 1 : 0); 1344 if (argCount > 1 || argsRequired && argCount == 0) { 1345 errormsg("jshell.err.option.or.filename", at.whole()); 1346 return false; 1347 } 1348 if (fn != null) { 1349 String init = readFile(fn, cmd + " start"); 1350 if (init == null) { 1351 return false; 1352 } else { 1353 startup = init; 1354 return true; 1355 } 1356 } else if (at.hasOption("-default")) { 1357 startup = DEFAULT_STARTUP; 1358 } else if (at.hasOption("-none")) { 1359 startup = ""; 1360 } 1361 return true; 1362 } 1363 1364 boolean cmdClasspath(String arg) { 1365 if (arg.isEmpty()) { 1366 errormsg("jshell.err.classpath.arg"); 1367 return false; 1368 } else { 1369 state.addToClasspath(toPathResolvingUserHome(arg).toString()); 1370 fluffmsg("jshell.msg.classpath", arg); 1371 return true; 1372 } 1373 } 1374 1375 boolean cmdDebug(String arg) { 1376 if (arg.isEmpty()) { 1377 debug = !debug; 1378 InternalDebugControl.setDebugFlags(state, debug ? DBG_GEN : 0); 1379 fluff("Debugging %s", debug ? "on" : "off"); 1380 } else { 1381 int flags = 0; 1382 for (char ch : arg.toCharArray()) { 1383 switch (ch) { 1384 case '0': 1385 flags = 0; 1386 debug = false; 1387 fluff("Debugging off"); 1388 break; 1389 case 'r': 1390 debug = true; 1391 fluff("REPL tool debugging on"); 1392 break; 1393 case 'g': 1394 flags |= DBG_GEN; 1395 fluff("General debugging on"); 1396 break; 1397 case 'f': 1398 flags |= DBG_FMGR; 1399 fluff("File manager debugging on"); 1400 break; 1401 case 'c': 1402 flags |= DBG_COMPA; 1403 fluff("Completion analysis debugging on"); 1404 break; 1405 case 'd': 1406 flags |= DBG_DEP; 1407 fluff("Dependency debugging on"); 1408 break; 1409 case 'e': 1410 flags |= DBG_EVNT; 1411 fluff("Event debugging on"); 1412 break; 1413 default: 1414 hard("Unknown debugging option: %c", ch); 1415 fluff("Use: 0 r g f c d"); 1416 return false; 1417 } 1418 } 1419 InternalDebugControl.setDebugFlags(state, flags); 1420 } 1421 return true; 1422 } 1423 1424 private boolean cmdExit() { 1425 regenerateOnDeath = false; 1426 live = false; 1427 if (!replayableHistory.isEmpty()) { 1428 // Prevent history overflow by calculating what will fit, starting 1429 // with must recent 1430 int sepLen = RECORD_SEPARATOR.length(); 1431 int length = 0; 1432 int first = replayableHistory.size(); 1433 while(length < Preferences.MAX_VALUE_LENGTH && --first >= 0) { 1434 length += replayableHistory.get(first).length() + sepLen; 1435 } 1436 String hist = replayableHistory 1437 .subList(first + 1, replayableHistory.size()) 1438 .stream() 1439 .reduce( (a, b) -> a + RECORD_SEPARATOR + b) 1440 .get(); 1441 prefs.put(REPLAY_RESTORE_KEY, hist); 1442 } 1443 fluffmsg("jshell.msg.goodbye"); 1444 return true; 1445 } 1446 1447 boolean cmdHelp(String arg) { 1448 ArgTokenizer at = new ArgTokenizer("/help", arg); 1449 String subject = at.next(); 1450 if (subject != null) { 1451 Command[] matches = commands.values().stream() 1452 .filter(c -> c.command.startsWith(subject)) 1453 .toArray(size -> new Command[size]); 1454 at.mark(); 1455 String sub = at.next(); 1456 if (sub != null && matches.length == 1) { 1457 String cmd = matches[0].command; 1458 switch (cmd) { 1459 case "/set": 1460 at.rewind(); 1461 return printSubCommandHelp(cmd, at, "help.set.", SET_SUBCOMMANDS); 1462 case "/retain": 1463 at.rewind(); 1464 return printSubCommandHelp(cmd, at, "help.retain.", RETAIN_SUBCOMMANDS); 1465 } 1466 } 1467 if (matches.length > 0) { 1468 for (Command c : matches) { 1469 hard(""); 1470 hard("%s", c.command); 1471 hard(""); 1472 hardrb(c.helpKey); 1473 } 1474 return true; 1475 } else { 1476 errormsg("jshell.err.help.arg", arg); 1477 } 1478 } 1479 hardmsg("jshell.msg.help.begin"); 1480 hardPairs(commands.values().stream() 1481 .filter(cmd -> cmd.kind.showInHelp), 1482 cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"), 1483 cmd -> getResourceString(cmd.helpKey + ".summary") 1484 ); 1485 hardmsg("jshell.msg.help.subject"); 1486 hardPairs(commands.values().stream() 1487 .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT), 1488 cmd -> cmd.command, 1489 cmd -> getResourceString(cmd.helpKey + ".summary") 1490 ); 1491 return true; 1492 } 1493 1494 private boolean cmdHistory() { 1495 cmdout.println(); 1496 for (String s : input.currentSessionHistory()) { 1497 // No number prefix, confusing with snippet ids 1498 cmdout.printf("%s\n", s); 1499 } 1500 return true; 1501 } 1502 1503 /** 1504 * Avoid parameterized varargs possible heap pollution warning. 1505 */ 1506 private interface SnippetPredicate<T extends Snippet> extends Predicate<T> { } 1507 1508 /** 1509 * Apply filters to a stream until one that is non-empty is found. 1510 * Adapted from Stuart Marks 1511 * 1512 * @param supplier Supply the Snippet stream to filter 1513 * @param filters Filters to attempt 1514 * @return The non-empty filtered Stream, or null 1515 */ 1516 @SafeVarargs 1517 private static <T extends Snippet> Stream<T> nonEmptyStream(Supplier<Stream<T>> supplier, 1518 SnippetPredicate<T>... filters) { 1519 for (SnippetPredicate<T> filt : filters) { 1520 Iterator<T> iterator = supplier.get().filter(filt).iterator(); 1521 if (iterator.hasNext()) { 1522 return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); 1523 } 1524 } 1525 return null; 1526 } 1527 1528 private boolean inStartUp(Snippet sn) { 1529 return mapSnippet.get(sn).space == startNamespace; 1530 } 1531 1532 private boolean isActive(Snippet sn) { 1533 return state.status(sn).isActive; 1534 } 1535 1536 private boolean mainActive(Snippet sn) { 1537 return !inStartUp(sn) && isActive(sn); 1538 } 1539 1540 private boolean matchingDeclaration(Snippet sn, String name) { 1541 return sn instanceof DeclarationSnippet 1542 && ((DeclarationSnippet) sn).name().equals(name); 1543 } 1544 1545 /** 1546 * Convert user arguments to a Stream of snippets referenced by those 1547 * arguments (or lack of arguments). 1548 * 1549 * @param snippets the base list of possible snippets 1550 * @param defFilter the filter to apply to the arguments if no argument 1551 * @param rawargs the user's argument to the command, maybe be the empty 1552 * string 1553 * @return a Stream of referenced snippets or null if no matches are found 1554 */ 1555 private <T extends Snippet> Stream<T> argsOptionsToSnippets(List<T> snippets, 1556 Predicate<Snippet> defFilter, String rawargs, String cmd) { 1557 ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim()); 1558 at.allowedOptions("-all", "-start"); 1559 List<String> args = new ArrayList<>(); 1560 String s; 1561 while ((s = at.next()) != null) { 1562 args.add(s); 1563 } 1564 if (!checkOptionsAndRemainingInput(at)) { 1565 return null; 1566 } 1567 if (at.optionCount() > 0 && args.size() > 0) { 1568 errormsg("jshell.err.may.not.specify.options.and.snippets", at.whole()); 1569 return null; 1570 } 1571 if (at.optionCount() > 1) { 1572 errormsg("jshell.err.conflicting.options", at.whole()); 1573 return null; 1574 } 1575 if (at.hasOption("-all")) { 1576 // all snippets including start-up, failed, and overwritten 1577 return snippets.stream(); 1578 } 1579 if (at.hasOption("-start")) { 1580 // start-up snippets 1581 return snippets.stream() 1582 .filter(this::inStartUp); 1583 } 1584 if (args.isEmpty()) { 1585 // Default is all active user snippets 1586 return snippets.stream() 1587 .filter(defFilter); 1588 } 1589 return argsToSnippets(snippets, args); 1590 } 1591 1592 /** 1593 * Convert user arguments to a Stream of snippets referenced by those 1594 * arguments. 1595 * 1596 * @param snippets the base list of possible snippets 1597 * @param args the user's argument to the command, maybe be the empty list 1598 * @return a Stream of referenced snippets or null if no matches to specific 1599 * arg 1600 */ 1601 private <T extends Snippet> Stream<T> argsToSnippets(List<T> snippets, 1602 List<String> args) { 1603 Stream<T> result = null; 1604 for (String arg : args) { 1605 // Find the best match 1606 Stream<T> st = layeredSnippetSearch(snippets, arg); 1607 if (st == null) { 1608 Stream<Snippet> est = layeredSnippetSearch(state.snippets(), arg); 1609 if (est == null) { 1610 errormsg("jshell.err.no.such.snippets", arg); 1611 } else { 1612 errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command", 1613 arg, est.findFirst().get().source()); 1614 } 1615 return null; 1616 } 1617 if (result == null) { 1618 result = st; 1619 } else { 1620 result = Stream.concat(result, st); 1621 } 1622 } 1623 return result; 1624 } 1625 1626 private <T extends Snippet> Stream<T> layeredSnippetSearch(List<T> snippets, String arg) { 1627 return nonEmptyStream( 1628 // the stream supplier 1629 () -> snippets.stream(), 1630 // look for active user declarations matching the name 1631 sn -> isActive(sn) && matchingDeclaration(sn, arg), 1632 // else, look for any declarations matching the name 1633 sn -> matchingDeclaration(sn, arg), 1634 // else, look for an id of this name 1635 sn -> sn.id().equals(arg) 1636 ); 1637 } 1638 1639 private boolean cmdDrop(String rawargs) { 1640 ArgTokenizer at = new ArgTokenizer("/drop", rawargs.trim()); 1641 at.allowedOptions(); 1642 List<String> args = new ArrayList<>(); 1643 String s; 1644 while ((s = at.next()) != null) { 1645 args.add(s); 1646 } 1647 if (!checkOptionsAndRemainingInput(at)) { 1648 return false; 1649 } 1650 if (args.isEmpty()) { 1651 errormsg("jshell.err.drop.arg"); 1652 return false; 1653 } 1654 Stream<PersistentSnippet> stream = argsToSnippets(dropableSnippets(), args); 1655 if (stream == null) { 1656 // Snippet not found. Error already printed 1657 fluffmsg("jshell.msg.see.classes.etc"); 1658 return false; 1659 } 1660 List<PersistentSnippet> snippets = stream.collect(toList()); 1661 if (snippets.size() > args.size()) { 1662 // One of the args references more thean one snippet 1663 errormsg("jshell.err.drop.ambiguous"); 1664 fluffmsg("jshell.msg.use.one.of", snippets.stream() 1665 .map(sn -> String.format("\n/drop %-5s : %s", sn.id(), sn.source().replace("\n", "\n "))) 1666 .collect(Collectors.joining(", ")) 1667 ); 1668 return false; 1669 } 1670 snippets.stream() 1671 .forEach(sn -> state.drop(sn).forEach(this::handleEvent)); 1672 return true; 1673 } 1674 1675 private boolean cmdEdit(String arg) { 1676 Stream<Snippet> stream = argsOptionsToSnippets(state.snippets(), 1677 this::mainActive, arg, "/edit"); 1678 if (stream == null) { 1679 return false; 1680 } 1681 Set<String> srcSet = new LinkedHashSet<>(); 1682 stream.forEachOrdered(sn -> { 1683 String src = sn.source(); 1684 switch (sn.subKind()) { 1685 case VAR_VALUE_SUBKIND: 1686 break; 1687 case ASSIGNMENT_SUBKIND: 1688 case OTHER_EXPRESSION_SUBKIND: 1689 case TEMP_VAR_EXPRESSION_SUBKIND: 1690 if (!src.endsWith(";")) { 1691 src = src + ";"; 1692 } 1693 srcSet.add(src); 1694 break; 1695 default: 1696 srcSet.add(src); 1697 break; 1698 } 1699 }); 1700 StringBuilder sb = new StringBuilder(); 1701 for (String s : srcSet) { 1702 sb.append(s); 1703 sb.append('\n'); 1704 } 1705 String src = sb.toString(); 1706 Consumer<String> saveHandler = new SaveHandler(src, srcSet); 1707 Consumer<String> errorHandler = s -> hard("Edit Error: %s", s); 1708 if (editor == null) { 1709 try { 1710 EditPad.edit(errorHandler, src, saveHandler); 1711 } catch (RuntimeException ex) { 1712 errormsg("jshell.err.cant.launch.editor", ex); 1713 fluffmsg("jshell.msg.try.set.editor"); 1714 return false; 1715 } 1716 } else { 1717 ExternalEditor.edit(editor, errorHandler, src, saveHandler, input); 1718 } 1719 return true; 1720 } 1721 //where 1722 // receives editor requests to save 1723 private class SaveHandler implements Consumer<String> { 1724 1725 String src; 1726 Set<String> currSrcs; 1727 1728 SaveHandler(String src, Set<String> ss) { 1729 this.src = src; 1730 this.currSrcs = ss; 1731 } 1732 1733 @Override 1734 public void accept(String s) { 1735 if (!s.equals(src)) { // quick check first 1736 src = s; 1737 try { 1738 Set<String> nextSrcs = new LinkedHashSet<>(); 1739 boolean failed = false; 1740 while (true) { 1741 CompletionInfo an = analysis.analyzeCompletion(s); 1742 if (!an.completeness.isComplete) { 1743 break; 1744 } 1745 String tsrc = trimNewlines(an.source); 1746 if (!failed && !currSrcs.contains(tsrc)) { 1747 failed = processCompleteSource(tsrc); 1748 } 1749 nextSrcs.add(tsrc); 1750 if (an.remaining.isEmpty()) { 1751 break; 1752 } 1753 s = an.remaining; 1754 } 1755 currSrcs = nextSrcs; 1756 } catch (IllegalStateException ex) { 1757 hardmsg("jshell.msg.resetting"); 1758 resetState(); 1759 currSrcs = new LinkedHashSet<>(); // re-process everything 1760 } 1761 } 1762 } 1763 1764 private String trimNewlines(String s) { 1765 int b = 0; 1766 while (b < s.length() && s.charAt(b) == '\n') { 1767 ++b; 1768 } 1769 int e = s.length() -1; 1770 while (e >= 0 && s.charAt(e) == '\n') { 1771 --e; 1772 } 1773 return s.substring(b, e + 1); 1774 } 1775 } 1776 1777 private boolean cmdList(String arg) { 1778 if (arg.length() >= 2 && "-history".startsWith(arg)) { 1779 return cmdHistory(); 1780 } 1781 Stream<Snippet> stream = argsOptionsToSnippets(state.snippets(), 1782 this::mainActive, arg, "/list"); 1783 if (stream == null) { 1784 return false; 1785 } 1786 1787 // prevent double newline on empty list 1788 boolean[] hasOutput = new boolean[1]; 1789 stream.forEachOrdered(sn -> { 1790 if (!hasOutput[0]) { 1791 cmdout.println(); 1792 hasOutput[0] = true; 1793 } 1794 cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); 1795 }); 1796 return true; 1797 } 1798 1799 private boolean cmdOpen(String filename) { 1800 return runFile(filename, "/open"); 1801 } 1802 1803 private boolean runFile(String filename, String context) { 1804 if (!filename.isEmpty()) { 1805 try { 1806 run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString())); 1807 return true; 1808 } catch (FileNotFoundException e) { 1809 errormsg("jshell.err.file.not.found", context, filename, e.getMessage()); 1810 } catch (Exception e) { 1811 errormsg("jshell.err.file.exception", context, filename, e); 1812 } 1813 } else { 1814 errormsg("jshell.err.file.filename", context); 1815 } 1816 return false; 1817 } 1818 1819 /** 1820 * Read an external file. Error messages accessed via keyPrefix 1821 * 1822 * @param filename file to access or null 1823 * @param context printable non-natural language context for errors 1824 * @return contents of file as string 1825 */ 1826 String readFile(String filename, String context) { 1827 if (filename != null) { 1828 try { 1829 byte[] encoded = Files.readAllBytes(Paths.get(filename)); 1830 return new String(encoded); 1831 } catch (AccessDeniedException e) { 1832 errormsg("jshell.err.file.not.accessible", context, filename, e.getMessage()); 1833 } catch (NoSuchFileException e) { 1834 errormsg("jshell.err.file.not.found", context, filename); 1835 } catch (Exception e) { 1836 errormsg("jshell.err.file.exception", context, filename, e); 1837 } 1838 } else { 1839 errormsg("jshell.err.file.filename", context); 1840 } 1841 return null; 1842 1843 } 1844 1845 private boolean cmdReset() { 1846 live = false; 1847 fluffmsg("jshell.msg.resetting.state"); 1848 return true; 1849 } 1850 1851 private boolean cmdReload(String rawargs) { 1852 ArgTokenizer at = new ArgTokenizer("/reload", rawargs.trim()); 1853 at.allowedOptions("-restore", "-quiet"); 1854 if (!checkOptionsAndRemainingInput(at)) { 1855 return false; 1856 } 1857 Iterable<String> history; 1858 if (at.hasOption("-restore")) { 1859 if (replayableHistoryPrevious == null) { 1860 errormsg("jshell.err.reload.no.previous"); 1861 return false; 1862 } 1863 history = replayableHistoryPrevious; 1864 fluffmsg("jshell.err.reload.restarting.previous.state"); 1865 } else { 1866 history = replayableHistory; 1867 fluffmsg("jshell.err.reload.restarting.state"); 1868 } 1869 boolean echo = !at.hasOption("-quiet"); 1870 resetState(); 1871 run(new ReloadIOContext(history, 1872 echo ? cmdout : null)); 1873 return true; 1874 } 1875 1876 private boolean cmdSave(String rawargs) { 1877 ArgTokenizer at = new ArgTokenizer("/save", rawargs.trim()); 1878 at.allowedOptions("-all", "-start", "-history"); 1879 String filename = at.next(); 1880 if (filename == null) { 1881 errormsg("jshell.err.file.filename", "/save"); 1882 return false; 1883 } 1884 if (!checkOptionsAndRemainingInput(at)) { 1885 return false; 1886 } 1887 if (at.optionCount() > 1) { 1888 errormsg("jshell.err.conflicting.options", at.whole()); 1889 return false; 1890 } 1891 try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename), 1892 Charset.defaultCharset(), 1893 CREATE, TRUNCATE_EXISTING, WRITE)) { 1894 if (at.hasOption("-history")) { 1895 for (String s : input.currentSessionHistory()) { 1896 writer.write(s); 1897 writer.write("\n"); 1898 } 1899 } else if (at.hasOption("-start")) { 1900 writer.append(startup); 1901 } else { 1902 List<Snippet> sns = at.hasOption("-all") 1903 ? state.snippets() 1904 : state.snippets().stream().filter(this::mainActive).collect(toList()); 1905 for (Snippet sn : sns) { 1906 writer.write(sn.source()); 1907 writer.write("\n"); 1908 } 1909 } 1910 } catch (FileNotFoundException e) { 1911 errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage()); 1912 return false; 1913 } catch (Exception e) { 1914 errormsg("jshell.err.file.exception", "/save", filename, e); 1915 return false; 1916 } 1917 return true; 1918 } 1919 1920 private boolean cmdVars(String arg) { 1921 Stream<VarSnippet> stream = argsOptionsToSnippets(allVarSnippets(), 1922 this::isActive, arg, "/vars"); 1923 if (stream == null) { 1924 return false; 1925 } 1926 stream.forEachOrdered(vk -> 1927 { 1928 String val = state.status(vk) == Status.VALID 1929 ? state.varValue(vk) 1930 : "jshell.msg.vars.not.active"; 1931 hard(" %s %s = %s", vk.typeName(), vk.name(), val); 1932 }); 1933 return true; 1934 } 1935 1936 private boolean cmdMethods(String arg) { 1937 Stream<MethodSnippet> stream = argsOptionsToSnippets(allMethodSnippets(), 1938 this::isActive, arg, "/methods"); 1939 if (stream == null) { 1940 return false; 1941 } 1942 stream.forEachOrdered(mk 1943 -> hard(" %s %s", mk.name(), mk.signature()) 1944 ); 1945 return true; 1946 } 1947 1948 private boolean cmdTypes(String arg) { 1949 Stream<TypeDeclSnippet> stream = argsOptionsToSnippets(allTypeSnippets(), 1950 this::isActive, arg, "/types"); 1951 if (stream == null) { 1952 return false; 1953 } 1954 stream.forEachOrdered(ck 1955 -> { 1956 String kind; 1957 switch (ck.subKind()) { 1958 case INTERFACE_SUBKIND: 1959 kind = "interface"; 1960 break; 1961 case CLASS_SUBKIND: 1962 kind = "class"; 1963 break; 1964 case ENUM_SUBKIND: 1965 kind = "enum"; 1966 break; 1967 case ANNOTATION_TYPE_SUBKIND: 1968 kind = "@interface"; 1969 break; 1970 default: 1971 assert false : "Wrong kind" + ck.subKind(); 1972 kind = "class"; 1973 break; 1974 } 1975 hard(" %s %s", kind, ck.name()); 1976 }); 1977 return true; 1978 } 1979 1980 private boolean cmdImports() { 1981 state.imports().forEach(ik -> { 1982 hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname()); 1983 }); 1984 return true; 1985 } 1986 1987 private boolean cmdUseHistoryEntry(int index) { 1988 List<Snippet> keys = state.snippets(); 1989 if (index < 0) 1990 index += keys.size(); 1991 else 1992 index--; 1993 if (index >= 0 && index < keys.size()) { 1994 rerunSnippet(keys.get(index)); 1995 } else { 1996 errormsg("jshell.err.out.of.range"); 1997 return false; 1998 } 1999 return true; 2000 } 2001 2002 boolean checkOptionsAndRemainingInput(ArgTokenizer at) { 2003 String junk = at.remainder(); 2004 if (!junk.isEmpty()) { 2005 errormsg("jshell.err.unexpected.at.end", junk, at.whole()); 2006 return false; 2007 } else { 2008 String bad = at.badOptions(); 2009 if (!bad.isEmpty()) { 2010 errormsg("jshell.err.unknown.option", bad, at.whole()); 2011 return false; 2012 } 2013 } 2014 return true; 2015 } 2016 2017 private boolean rerunHistoryEntryById(String id) { 2018 Optional<Snippet> snippet = state.snippets().stream() 2019 .filter(s -> s.id().equals(id)) 2020 .findFirst(); 2021 return snippet.map(s -> { 2022 rerunSnippet(s); 2023 return true; 2024 }).orElse(false); 2025 } 2026 2027 private void rerunSnippet(Snippet snippet) { 2028 String source = snippet.source(); 2029 cmdout.printf("%s\n", source); 2030 input.replaceLastHistoryEntry(source); 2031 processSourceCatchingReset(source); 2032 } 2033 2034 /** 2035 * Filter diagnostics for only errors (no warnings, ...) 2036 * @param diagnostics input list 2037 * @return filtered list 2038 */ 2039 List<Diag> errorsOnly(List<Diag> diagnostics) { 2040 return diagnostics.stream() 2041 .filter(d -> d.isError()) 2042 .collect(toList()); 2043 } 2044 2045 void displayDiagnostics(String source, Diag diag, List<String> toDisplay) { 2046 for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize 2047 if (!line.trim().startsWith("location:")) { 2048 toDisplay.add(line); 2049 } 2050 } 2051 2052 int pstart = (int) diag.getStartPosition(); 2053 int pend = (int) diag.getEndPosition(); 2054 Matcher m = LINEBREAK.matcher(source); 2055 int pstartl = 0; 2056 int pendl = -2; 2057 while (m.find(pstartl)) { 2058 pendl = m.start(); 2059 if (pendl >= pstart) { 2060 break; 2061 } else { 2062 pstartl = m.end(); 2063 } 2064 } 2065 if (pendl < pstart) { 2066 pendl = source.length(); 2067 } 2068 toDisplay.add(source.substring(pstartl, pendl)); 2069 2070 StringBuilder sb = new StringBuilder(); 2071 int start = pstart - pstartl; 2072 for (int i = 0; i < start; ++i) { 2073 sb.append(' '); 2074 } 2075 sb.append('^'); 2076 boolean multiline = pend > pendl; 2077 int end = (multiline ? pendl : pend) - pstartl - 1; 2078 if (end > start) { 2079 for (int i = start + 1; i < end; ++i) { 2080 sb.append('-'); 2081 } 2082 if (multiline) { 2083 sb.append("-..."); 2084 } else { 2085 sb.append('^'); 2086 } 2087 } 2088 toDisplay.add(sb.toString()); 2089 2090 debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this); 2091 debug("Code: %s", diag.getCode()); 2092 debug("Pos: %d (%d - %d)", diag.getPosition(), 2093 diag.getStartPosition(), diag.getEndPosition()); 2094 } 2095 2096 private String processSource(String srcInput) throws IllegalStateException { 2097 while (true) { 2098 CompletionInfo an = analysis.analyzeCompletion(srcInput); 2099 if (!an.completeness.isComplete) { 2100 return an.remaining; 2101 } 2102 boolean failed = processCompleteSource(an.source); 2103 if (failed || an.remaining.isEmpty()) { 2104 return ""; 2105 } 2106 srcInput = an.remaining; 2107 } 2108 } 2109 //where 2110 private boolean processCompleteSource(String source) throws IllegalStateException { 2111 debug("Compiling: %s", source); 2112 boolean failed = false; 2113 boolean isActive = false; 2114 List<SnippetEvent> events = state.eval(source); 2115 for (SnippetEvent e : events) { 2116 // Report the event, recording failure 2117 failed |= handleEvent(e); 2118 2119 // If any main snippet is active, this should be replayable 2120 // also ignore var value queries 2121 isActive |= e.causeSnippet() == null && 2122 e.status().isActive && 2123 e.snippet().subKind() != VAR_VALUE_SUBKIND; 2124 } 2125 // If this is an active snippet and it didn't cause the backend to die, 2126 // add it to the replayable history 2127 if (isActive && live) { 2128 addToReplayHistory(source); 2129 } 2130 2131 return failed; 2132 } 2133 2134 // Handle incoming snippet events -- return true on failure 2135 private boolean handleEvent(SnippetEvent ste) { 2136 Snippet sn = ste.snippet(); 2137 if (sn == null) { 2138 debug("Event with null key: %s", ste); 2139 return false; 2140 } 2141 List<Diag> diagnostics = state.diagnostics(sn); 2142 String source = sn.source(); 2143 if (ste.causeSnippet() == null) { 2144 // main event 2145 for (Diag d : diagnostics) { 2146 hardmsg(d.isError()? "jshell.msg.error" : "jshell.msg.warning"); 2147 List<String> disp = new ArrayList<>(); 2148 displayDiagnostics(source, d, disp); 2149 disp.stream() 2150 .forEach(l -> hard("%s", l)); 2151 } 2152 2153 if (ste.status() != Status.REJECTED) { 2154 if (ste.exception() != null) { 2155 if (ste.exception() instanceof EvalException) { 2156 printEvalException((EvalException) ste.exception()); 2157 return true; 2158 } else if (ste.exception() instanceof UnresolvedReferenceException) { 2159 printUnresolvedException((UnresolvedReferenceException) ste.exception()); 2160 } else { 2161 hard("Unexpected execution exception: %s", ste.exception()); 2162 return true; 2163 } 2164 } else { 2165 new DisplayEvent(ste, false, ste.value(), diagnostics).displayDeclarationAndValue(); 2166 } 2167 } else { 2168 if (diagnostics.isEmpty()) { 2169 errormsg("jshell.err.failed"); 2170 } 2171 return true; 2172 } 2173 } else { 2174 // Update 2175 if (sn instanceof DeclarationSnippet) { 2176 List<Diag> other = errorsOnly(diagnostics); 2177 2178 // display update information 2179 new DisplayEvent(ste, true, ste.value(), other).displayDeclarationAndValue(); 2180 } 2181 } 2182 return false; 2183 } 2184 //where 2185 void printStackTrace(StackTraceElement[] stes) { 2186 for (StackTraceElement ste : stes) { 2187 StringBuilder sb = new StringBuilder(); 2188 String cn = ste.getClassName(); 2189 if (!cn.isEmpty()) { 2190 int dot = cn.lastIndexOf('.'); 2191 if (dot > 0) { 2192 sb.append(cn.substring(dot + 1)); 2193 } else { 2194 sb.append(cn); 2195 } 2196 sb.append("."); 2197 } 2198 if (!ste.getMethodName().isEmpty()) { 2199 sb.append(ste.getMethodName()); 2200 sb.append(" "); 2201 } 2202 String fileName = ste.getFileName(); 2203 int lineNumber = ste.getLineNumber(); 2204 String loc = ste.isNativeMethod() 2205 ? getResourceString("jshell.msg.native.method") 2206 : fileName == null 2207 ? getResourceString("jshell.msg.unknown.source") 2208 : lineNumber >= 0 2209 ? fileName + ":" + lineNumber 2210 : fileName; 2211 hard(" at %s(%s)", sb, loc); 2212 2213 } 2214 } 2215 //where 2216 void printUnresolvedException(UnresolvedReferenceException ex) { 2217 DeclarationSnippet corralled = ex.getSnippet(); 2218 List<Diag> otherErrors = errorsOnly(state.diagnostics(corralled)); 2219 new DisplayEvent(corralled, state.status(corralled), FormatAction.USED, true, null, otherErrors) 2220 .displayDeclarationAndValue(); 2221 } 2222 //where 2223 void printEvalException(EvalException ex) { 2224 if (ex.getMessage() == null) { 2225 hard("%s thrown", ex.getExceptionClassName()); 2226 } else { 2227 hard("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage()); 2228 } 2229 printStackTrace(ex.getStackTrace()); 2230 } 2231 2232 private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) { 2233 FormatAction act; 2234 switch (status) { 2235 case VALID: 2236 case RECOVERABLE_DEFINED: 2237 case RECOVERABLE_NOT_DEFINED: 2238 if (previousStatus.isActive) { 2239 act = isSignatureChange 2240 ? FormatAction.REPLACED 2241 : FormatAction.MODIFIED; 2242 } else { 2243 act = FormatAction.ADDED; 2244 } 2245 break; 2246 case OVERWRITTEN: 2247 act = FormatAction.OVERWROTE; 2248 break; 2249 case DROPPED: 2250 act = FormatAction.DROPPED; 2251 break; 2252 case REJECTED: 2253 case NONEXISTENT: 2254 default: 2255 // Should not occur 2256 error("Unexpected status: " + previousStatus.toString() + "=>" + status.toString()); 2257 act = FormatAction.DROPPED; 2258 } 2259 return act; 2260 } 2261 2262 class DisplayEvent { 2263 private final Snippet sn; 2264 private final FormatAction action; 2265 private final boolean update; 2266 private final String value; 2267 private final List<String> errorLines; 2268 private final FormatResolve resolution; 2269 private final String unresolved; 2270 private final FormatUnresolved unrcnt; 2271 private final FormatErrors errcnt; 2272 2273 DisplayEvent(SnippetEvent ste, boolean update, String value, List<Diag> errors) { 2274 this(ste.snippet(), ste.status(), toAction(ste.status(), ste.previousStatus(), ste.isSignatureChange()), update, value, errors); 2275 } 2276 2277 DisplayEvent(Snippet sn, Status status, FormatAction action, boolean update, String value, List<Diag> errors) { 2278 this.sn = sn; 2279 this.action = action; 2280 this.update = update; 2281 this.value = value; 2282 this.errorLines = new ArrayList<>(); 2283 for (Diag d : errors) { 2284 displayDiagnostics(sn.source(), d, errorLines); 2285 } 2286 int unresolvedCount; 2287 if (sn instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) { 2288 resolution = (status == Status.RECOVERABLE_NOT_DEFINED) 2289 ? FormatResolve.NOTDEFINED 2290 : FormatResolve.DEFINED; 2291 unresolved = unresolved((DeclarationSnippet) sn); 2292 unresolvedCount = state.unresolvedDependencies((DeclarationSnippet) sn).size(); 2293 } else { 2294 resolution = FormatResolve.OK; 2295 unresolved = ""; 2296 unresolvedCount = 0; 2297 } 2298 unrcnt = unresolvedCount == 0 2299 ? FormatUnresolved.UNRESOLVED0 2300 : unresolvedCount == 1 2301 ? FormatUnresolved.UNRESOLVED1 2302 : FormatUnresolved.UNRESOLVED2; 2303 errcnt = errors.isEmpty() 2304 ? FormatErrors.ERROR0 2305 : errors.size() == 1 2306 ? FormatErrors.ERROR1 2307 : FormatErrors.ERROR2; 2308 } 2309 2310 private String unresolved(DeclarationSnippet key) { 2311 List<String> unr = state.unresolvedDependencies(key); 2312 StringBuilder sb = new StringBuilder(); 2313 int fromLast = unr.size(); 2314 if (fromLast > 0) { 2315 sb.append(" "); 2316 } 2317 for (String u : unr) { 2318 --fromLast; 2319 sb.append(u); 2320 switch (fromLast) { 2321 // No suffix 2322 case 0: 2323 break; 2324 case 1: 2325 sb.append(", and "); 2326 break; 2327 default: 2328 sb.append(", "); 2329 break; 2330 } 2331 } 2332 return sb.toString(); 2333 } 2334 2335 private void custom(FormatCase fcase, String name) { 2336 custom(fcase, name, null); 2337 } 2338 2339 private void custom(FormatCase fcase, String name, String type) { 2340 String display = feedback.format(fcase, action, (update ? FormatWhen.UPDATE : FormatWhen.PRIMARY), 2341 resolution, unrcnt, errcnt, 2342 name, type, value, unresolved, errorLines); 2343 if (interactive()) { 2344 cmdout.print(display); 2345 } 2346 } 2347 2348 @SuppressWarnings("fallthrough") 2349 private void displayDeclarationAndValue() { 2350 switch (sn.subKind()) { 2351 case CLASS_SUBKIND: 2352 custom(FormatCase.CLASS, ((TypeDeclSnippet) sn).name()); 2353 break; 2354 case INTERFACE_SUBKIND: 2355 custom(FormatCase.INTERFACE, ((TypeDeclSnippet) sn).name()); 2356 break; 2357 case ENUM_SUBKIND: 2358 custom(FormatCase.ENUM, ((TypeDeclSnippet) sn).name()); 2359 break; 2360 case ANNOTATION_TYPE_SUBKIND: 2361 custom(FormatCase.ANNOTATION, ((TypeDeclSnippet) sn).name()); 2362 break; 2363 case METHOD_SUBKIND: 2364 custom(FormatCase.METHOD, ((MethodSnippet) sn).name(), ((MethodSnippet) sn).parameterTypes()); 2365 break; 2366 case VAR_DECLARATION_SUBKIND: { 2367 VarSnippet vk = (VarSnippet) sn; 2368 custom(FormatCase.VARDECL, vk.name(), vk.typeName()); 2369 break; 2370 } 2371 case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: { 2372 VarSnippet vk = (VarSnippet) sn; 2373 custom(FormatCase.VARINIT, vk.name(), vk.typeName()); 2374 break; 2375 } 2376 case TEMP_VAR_EXPRESSION_SUBKIND: { 2377 VarSnippet vk = (VarSnippet) sn; 2378 custom(FormatCase.EXPRESSION, vk.name(), vk.typeName()); 2379 break; 2380 } 2381 case OTHER_EXPRESSION_SUBKIND: 2382 error("Unexpected expression form -- value is: %s", (value)); 2383 break; 2384 case VAR_VALUE_SUBKIND: { 2385 ExpressionSnippet ek = (ExpressionSnippet) sn; 2386 custom(FormatCase.VARVALUE, ek.name(), ek.typeName()); 2387 break; 2388 } 2389 case ASSIGNMENT_SUBKIND: { 2390 ExpressionSnippet ek = (ExpressionSnippet) sn; 2391 custom(FormatCase.ASSIGNMENT, ek.name(), ek.typeName()); 2392 break; 2393 } 2394 case SINGLE_TYPE_IMPORT_SUBKIND: 2395 case TYPE_IMPORT_ON_DEMAND_SUBKIND: 2396 case SINGLE_STATIC_IMPORT_SUBKIND: 2397 case STATIC_IMPORT_ON_DEMAND_SUBKIND: 2398 custom(FormatCase.IMPORT, ((ImportSnippet) sn).name()); 2399 break; 2400 case STATEMENT_SUBKIND: 2401 custom(FormatCase.STATEMENT, null); 2402 break; 2403 } 2404 } 2405 } 2406 2407 /** The current version number as a string. 2408 */ 2409 String version() { 2410 return version("release"); // mm.nn.oo[-milestone] 2411 } 2412 2413 /** The current full version number as a string. 2414 */ 2415 String fullVersion() { 2416 return version("full"); // mm.mm.oo[-milestone]-build 2417 } 2418 2419 private String version(String key) { 2420 if (versionRB == null) { 2421 try { 2422 versionRB = ResourceBundle.getBundle(VERSION_RB_NAME, locale); 2423 } catch (MissingResourceException e) { 2424 return "(version info not available)"; 2425 } 2426 } 2427 try { 2428 return versionRB.getString(key); 2429 } 2430 catch (MissingResourceException e) { 2431 return "(version info not available)"; 2432 } 2433 } 2434 2435 class NameSpace { 2436 final String spaceName; 2437 final String prefix; 2438 private int nextNum; 2439 2440 NameSpace(String spaceName, String prefix) { 2441 this.spaceName = spaceName; 2442 this.prefix = prefix; 2443 this.nextNum = 1; 2444 } 2445 2446 String tid(Snippet sn) { 2447 String tid = prefix + nextNum++; 2448 mapSnippet.put(sn, new SnippetInfo(sn, this, tid)); 2449 return tid; 2450 } 2451 2452 String tidNext() { 2453 return prefix + nextNum; 2454 } 2455 } 2456 2457 static class SnippetInfo { 2458 final Snippet snippet; 2459 final NameSpace space; 2460 final String tid; 2461 2462 SnippetInfo(Snippet snippet, NameSpace space, String tid) { 2463 this.snippet = snippet; 2464 this.space = space; 2465 this.tid = tid; 2466 } 2467 } 2468} 2469 2470abstract class NonInteractiveIOContext extends IOContext { 2471 2472 @Override 2473 public boolean interactiveOutput() { 2474 return false; 2475 } 2476 2477 @Override 2478 public Iterable<String> currentSessionHistory() { 2479 return Collections.emptyList(); 2480 } 2481 2482 @Override 2483 public boolean terminalEditorRunning() { 2484 return false; 2485 } 2486 2487 @Override 2488 public void suspend() { 2489 } 2490 2491 @Override 2492 public void resume() { 2493 } 2494 2495 @Override 2496 public void beforeUserCode() { 2497 } 2498 2499 @Override 2500 public void afterUserCode() { 2501 } 2502 2503 @Override 2504 public void replaceLastHistoryEntry(String source) { 2505 } 2506} 2507 2508class ScannerIOContext extends NonInteractiveIOContext { 2509 private final Scanner scannerIn; 2510 2511 ScannerIOContext(Scanner scannerIn) { 2512 this.scannerIn = scannerIn; 2513 } 2514 2515 @Override 2516 public String readLine(String prompt, String prefix) { 2517 if (scannerIn.hasNextLine()) { 2518 return scannerIn.nextLine(); 2519 } else { 2520 return null; 2521 } 2522 } 2523 2524 @Override 2525 public void close() { 2526 scannerIn.close(); 2527 } 2528} 2529 2530class FileScannerIOContext extends ScannerIOContext { 2531 2532 FileScannerIOContext(String fn) throws FileNotFoundException { 2533 this(new FileReader(fn)); 2534 } 2535 2536 FileScannerIOContext(Reader rdr) throws FileNotFoundException { 2537 super(new Scanner(rdr)); 2538 } 2539} 2540 2541class ReloadIOContext extends NonInteractiveIOContext { 2542 private final Iterator<String> it; 2543 private final PrintStream echoStream; 2544 2545 ReloadIOContext(Iterable<String> history, PrintStream echoStream) { 2546 this.it = history.iterator(); 2547 this.echoStream = echoStream; 2548 } 2549 2550 @Override 2551 public String readLine(String prompt, String prefix) { 2552 String s = it.hasNext() 2553 ? it.next() 2554 : null; 2555 if (echoStream != null && s != null) { 2556 String p = "-: "; 2557 String p2 = "\n "; 2558 echoStream.printf("%s%s\n", p, s.replace("\n", p2)); 2559 } 2560 return s; 2561 } 2562 2563 @Override 2564 public void close() { 2565 } 2566} 2567