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