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