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