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