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