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