1 /* 2 * Copyright (c) 2014, 2020, 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.BufferedReader; 29 import java.io.BufferedWriter; 30 import java.io.EOFException; 31 import java.io.File; 32 import java.io.FileNotFoundException; 33 import java.io.FileReader; 34 import java.io.IOException; 35 import java.io.InputStream; 36 import java.io.InputStreamReader; 37 import java.io.PrintStream; 38 import java.io.Reader; 39 import java.io.StringReader; 40 import java.lang.module.ModuleDescriptor; 41 import java.lang.module.ModuleFinder; 42 import java.lang.module.ModuleReference; 43 import java.net.MalformedURLException; 44 import java.net.URISyntaxException; 45 import java.net.URL; 46 import java.nio.charset.Charset; 47 import java.nio.file.FileSystems; 48 import java.nio.file.Files; 49 import java.nio.file.InvalidPathException; 50 import java.nio.file.Path; 51 import java.nio.file.Paths; 52 import java.text.MessageFormat; 53 import java.util.ArrayList; 54 import java.util.Arrays; 55 import java.util.Collection; 56 import java.util.Collections; 57 import java.util.HashMap; 58 import java.util.HashSet; 59 import java.util.Iterator; 60 import java.util.LinkedHashMap; 61 import java.util.LinkedHashSet; 62 import java.util.List; 63 import java.util.Locale; 64 import java.util.Map; 65 import java.util.Map.Entry; 66 import java.util.Optional; 67 import java.util.Scanner; 68 import java.util.Set; 69 import java.util.function.Consumer; 70 import java.util.function.Predicate; 71 import java.util.prefs.Preferences; 72 import java.util.regex.Matcher; 73 import java.util.regex.Pattern; 74 import java.util.stream.Collectors; 75 import java.util.stream.Stream; 76 import java.util.stream.StreamSupport; 77 78 import jdk.internal.jshell.debug.InternalDebugControl; 79 import jdk.internal.jshell.tool.IOContext.InputInterruptedException; 80 import jdk.jshell.DeclarationSnippet; 81 import jdk.jshell.Diag; 82 import jdk.jshell.EvalException; 83 import jdk.jshell.ExpressionSnippet; 84 import jdk.jshell.ImportSnippet; 85 import jdk.jshell.JShell; 86 import jdk.jshell.JShell.Subscription; 87 import jdk.jshell.JShellException; 88 import jdk.jshell.MethodSnippet; 89 import jdk.jshell.Snippet; 90 import jdk.jshell.Snippet.Kind; 91 import jdk.jshell.Snippet.Status; 92 import jdk.jshell.SnippetEvent; 93 import jdk.jshell.SourceCodeAnalysis; 94 import jdk.jshell.SourceCodeAnalysis.CompletionInfo; 95 import jdk.jshell.SourceCodeAnalysis.Completeness; 96 import jdk.jshell.SourceCodeAnalysis.Suggestion; 97 import jdk.jshell.TypeDeclSnippet; 98 import jdk.jshell.UnresolvedReferenceException; 99 import jdk.jshell.VarSnippet; 100 101 import static java.nio.file.StandardOpenOption.CREATE; 102 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; 103 import static java.nio.file.StandardOpenOption.WRITE; 104 import java.util.AbstractMap.SimpleEntry; 105 import java.util.MissingResourceException; 106 import java.util.ResourceBundle; 107 import java.util.ServiceLoader; 108 import java.util.Spliterators; 109 import java.util.function.Function; 110 import java.util.function.Supplier; 111 import jdk.internal.joptsimple.*; 112 import jdk.internal.jshell.tool.Selector.FormatAction; 113 import jdk.internal.jshell.tool.Selector.FormatCase; 114 import jdk.internal.jshell.tool.Selector.FormatErrors; 115 import jdk.internal.jshell.tool.Selector.FormatResolve; 116 import jdk.internal.jshell.tool.Selector.FormatUnresolved; 117 import jdk.internal.jshell.tool.Selector.FormatWhen; 118 import jdk.internal.editor.spi.BuildInEditorProvider; 119 import jdk.internal.editor.external.ExternalEditor; 120 import static java.util.Arrays.asList; 121 import static java.util.Arrays.stream; 122 import static java.util.Collections.singletonList; 123 import static java.util.stream.Collectors.joining; 124 import static java.util.stream.Collectors.toList; 125 import static jdk.jshell.Snippet.SubKind.TEMP_VAR_EXPRESSION_SUBKIND; 126 import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND; 127 import static java.util.stream.Collectors.toMap; 128 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_COMPA; 129 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP; 130 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT; 131 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR; 132 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN; 133 import static jdk.internal.jshell.debug.InternalDebugControl.DBG_WRAP; 134 import static jdk.internal.jshell.tool.ContinuousCompletionProvider.STARTSWITH_MATCHER; 135 136 /** 137 * Command line REPL tool for Java using the JShell API. 138 * @author Robert Field 139 */ 140 public class JShellTool implements MessageHandler { 141 142 private static final Pattern LINEBREAK = Pattern.compile("\\R"); 143 private static final Pattern ID = Pattern.compile("[se]?\\d+([-\\s].*)?"); 144 private static final Pattern RERUN_ID = Pattern.compile("/" + ID.pattern()); 145 private static final Pattern RERUN_PREVIOUS = Pattern.compile("/\\-\\d+( .*)?"); 146 private static final Pattern SET_SUB = Pattern.compile("/?set .*"); 147 static final String RECORD_SEPARATOR = "\u241E"; 148 private static final String RB_NAME_PREFIX = "jdk.internal.jshell.tool.resources"; 149 private static final String VERSION_RB_NAME = RB_NAME_PREFIX + ".version"; 150 private static final String L10N_RB_NAME = RB_NAME_PREFIX + ".l10n"; 151 152 final InputStream cmdin; 153 final PrintStream cmdout; 154 final PrintStream cmderr; 155 final PrintStream console; 156 final InputStream userin; 157 final PrintStream userout; 158 final PrintStream usererr; 159 final PersistentStorage prefs; 160 final Map<String, String> envvars; 161 final Locale locale; 162 163 final Feedback feedback = new Feedback(); 164 165 /** 166 * The complete constructor for the tool (used by test harnesses). 167 * @param cmdin command line input -- snippets and commands 168 * @param cmdout command line output, feedback including errors 169 * @param cmderr start-up errors and debugging info 170 * @param console console control interaction 171 * @param userin code execution input, or null to use IOContext 172 * @param userout code execution output -- System.out.printf("hi") 173 * @param usererr code execution error stream -- System.err.printf("Oops") 174 * @param prefs persistence implementation to use 175 * @param envvars environment variable mapping to use 176 * @param locale locale to use 177 */ JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr, PrintStream console, InputStream userin, PrintStream userout, PrintStream usererr, PersistentStorage prefs, Map<String, String> envvars, Locale locale)178 JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr, 179 PrintStream console, 180 InputStream userin, PrintStream userout, PrintStream usererr, 181 PersistentStorage prefs, Map<String, String> envvars, Locale locale) { 182 this.cmdin = cmdin; 183 this.cmdout = cmdout; 184 this.cmderr = cmderr; 185 this.console = console; 186 this.userin = userin != null ? userin : new InputStream() { 187 @Override 188 public int read() throws IOException { 189 return input.readUserInput(); 190 } 191 }; 192 this.userout = userout; 193 this.usererr = usererr; 194 this.prefs = prefs; 195 this.envvars = envvars; 196 this.locale = locale; 197 } 198 199 private ResourceBundle versionRB = null; 200 private ResourceBundle outputRB = null; 201 202 private IOContext input = null; 203 private boolean regenerateOnDeath = true; 204 private boolean live = false; 205 private boolean interactiveModeBegun = false; 206 private Options options; 207 208 SourceCodeAnalysis analysis; 209 private JShell state = null; 210 Subscription shutdownSubscription = null; 211 212 static final EditorSetting BUILT_IN_EDITOR = new EditorSetting(null, false); 213 214 private boolean debug = false; 215 private int debugFlags = 0; 216 public boolean testPrompt = false; 217 private Startup startup = null; 218 private boolean isCurrentlyRunningStartup = false; 219 private String executionControlSpec = null; 220 private EditorSetting editor = BUILT_IN_EDITOR; 221 private int exitCode = 0; 222 223 private static final String[] EDITOR_ENV_VARS = new String[] { 224 "JSHELLEDITOR", "VISUAL", "EDITOR"}; 225 226 // Commands and snippets which can be replayed 227 private ReplayableHistory replayableHistory; 228 private ReplayableHistory replayableHistoryPrevious; 229 230 static final String STARTUP_KEY = "STARTUP"; 231 static final String EDITOR_KEY = "EDITOR"; 232 static final String MODE_KEY = "MODE"; 233 static final String MODE2_KEY = "MODE2"; 234 static final String FEEDBACK_KEY = "FEEDBACK"; 235 static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE"; 236 public static final String INDENT_KEY = "INDENT"; 237 238 static final Pattern BUILTIN_FILE_PATTERN = Pattern.compile("\\w+"); 239 static final String BUILTIN_FILE_PATH_FORMAT = "/jdk/jshell/tool/resources/%s.jsh"; 240 static final String INT_PREFIX = "int $$exit$$ = "; 241 242 static final int OUTPUT_WIDTH = 72; 243 static final int DEFAULT_INDENT = 4; 244 245 // match anything followed by whitespace 246 private static final Pattern OPTION_PRE_PATTERN = 247 Pattern.compile("\\s*(\\S+\\s+)*?"); 248 // match a (possibly incomplete) option flag with optional double-dash and/or internal dashes 249 private static final Pattern OPTION_PATTERN = 250 Pattern.compile(OPTION_PRE_PATTERN.pattern() + "(?<dd>-??)(?<flag>-([a-z][a-z\\-]*)?)"); 251 // match an option flag and a (possibly missing or incomplete) value 252 private static final Pattern OPTION_VALUE_PATTERN = 253 Pattern.compile(OPTION_PATTERN.pattern() + "\\s+(?<val>\\S*)"); 254 255 // Tool id (tid) mapping: the three name spaces 256 NameSpace mainNamespace; 257 NameSpace startNamespace; 258 NameSpace errorNamespace; 259 260 // Tool id (tid) mapping: the current name spaces 261 NameSpace currentNameSpace; 262 263 Map<Snippet, SnippetInfo> mapSnippet; 264 265 // Kinds of compiler/runtime init options 266 private enum OptionKind { 267 CLASS_PATH("--class-path", true), 268 MODULE_PATH("--module-path", true), 269 ADD_MODULES("--add-modules", false), 270 ADD_EXPORTS("--add-exports", false), 271 ENABLE_PREVIEW("--enable-preview", true), 272 SOURCE_RELEASE("-source", true, true, true, false, false), // virtual option, generated by --enable-preview 273 TO_COMPILER("-C", false, false, true, false, false), 274 TO_REMOTE_VM("-R", false, false, false, true, false),; 275 final String optionFlag; 276 final boolean onlyOne; 277 final boolean passFlag; 278 final boolean toCompiler; 279 final boolean toRemoteVm; 280 final boolean showOption; 281 OptionKind(String optionFlag, boolean onlyOne)282 private OptionKind(String optionFlag, boolean onlyOne) { 283 this(optionFlag, onlyOne, true, true, true, true); 284 } 285 OptionKind(String optionFlag, boolean onlyOne, boolean passFlag, boolean toCompiler, boolean toRemoteVm, boolean showOption)286 private OptionKind(String optionFlag, boolean onlyOne, boolean passFlag, boolean toCompiler, boolean toRemoteVm, boolean showOption) { 287 this.optionFlag = optionFlag; 288 this.onlyOne = onlyOne; 289 this.passFlag = passFlag; 290 this.toCompiler = toCompiler; 291 this.toRemoteVm = toRemoteVm; 292 this.showOption= showOption; 293 } 294 295 } 296 297 // compiler/runtime init option values 298 private static class Options { 299 300 private final Map<OptionKind, List<String>> optMap; 301 302 // New blank Options Options()303 Options() { 304 optMap = new HashMap<>(); 305 } 306 307 // Options as a copy Options(Options opts)308 private Options(Options opts) { 309 optMap = new HashMap<>(opts.optMap); 310 } 311 selectOptions(Predicate<Entry<OptionKind, List<String>>> pred)312 private String[] selectOptions(Predicate<Entry<OptionKind, List<String>>> pred) { 313 return optMap.entrySet().stream() 314 .filter(pred) 315 .flatMap(e -> e.getValue().stream()) 316 .toArray(String[]::new); 317 } 318 remoteVmOptions()319 String[] remoteVmOptions() { 320 return selectOptions(e -> e.getKey().toRemoteVm); 321 } 322 compilerOptions()323 String[] compilerOptions() { 324 return selectOptions(e -> e.getKey().toCompiler); 325 } 326 shownOptions()327 String[] shownOptions() { 328 return selectOptions(e -> e.getKey().showOption); 329 } 330 addAll(OptionKind kind, Collection<String> vals)331 void addAll(OptionKind kind, Collection<String> vals) { 332 optMap.computeIfAbsent(kind, k -> new ArrayList<>()) 333 .addAll(vals); 334 } 335 336 // return a new Options, with parameter options overriding receiver options override(Options newer)337 Options override(Options newer) { 338 Options result = new Options(this); 339 newer.optMap.entrySet().stream() 340 .forEach(e -> { 341 if (e.getKey().onlyOne) { 342 // Only one allowed, override last 343 result.optMap.put(e.getKey(), e.getValue()); 344 } else { 345 // Additive 346 result.addAll(e.getKey(), e.getValue()); 347 } 348 }); 349 return result; 350 } 351 } 352 353 // base option parsing of /env, /reload, and /reset and command-line options 354 private class OptionParserBase { 355 356 final OptionParser parser = new OptionParser(); 357 private final OptionSpec<String> argClassPath = parser.accepts("class-path").withRequiredArg(); 358 private final OptionSpec<String> argModulePath = parser.accepts("module-path").withRequiredArg(); 359 private final OptionSpec<String> argAddModules = parser.accepts("add-modules").withRequiredArg(); 360 private final OptionSpec<String> argAddExports = parser.accepts("add-exports").withRequiredArg(); 361 private final OptionSpecBuilder argEnablePreview = parser.accepts("enable-preview"); 362 private final NonOptionArgumentSpec<String> argNonOptions = parser.nonOptions(); 363 364 private Options opts = new Options(); 365 private List<String> nonOptions; 366 private boolean failed = false; 367 nonOptions()368 List<String> nonOptions() { 369 return nonOptions; 370 } 371 msg(String key, Object... args)372 void msg(String key, Object... args) { 373 errormsg(key, args); 374 } 375 parse(String[] args)376 Options parse(String[] args) throws OptionException { 377 try { 378 OptionSet oset = parser.parse(args); 379 nonOptions = oset.valuesOf(argNonOptions); 380 return parse(oset); 381 } catch (OptionException ex) { 382 if (ex.options().isEmpty()) { 383 msg("jshell.err.opt.invalid", stream(args).collect(joining(", "))); 384 } else { 385 boolean isKnown = parser.recognizedOptions().containsKey(ex.options().iterator().next()); 386 msg(isKnown 387 ? "jshell.err.opt.arg" 388 : "jshell.err.opt.unknown", 389 ex.options() 390 .stream() 391 .collect(joining(", "))); 392 } 393 exitCode = 1; 394 return null; 395 } 396 } 397 398 // check that the supplied string represent valid class/module paths 399 // converting any ~/ to user home validPaths(Collection<String> vals, String context, boolean isModulePath)400 private Collection<String> validPaths(Collection<String> vals, String context, boolean isModulePath) { 401 Stream<String> result = vals.stream() 402 .map(s -> Arrays.stream(s.split(File.pathSeparator)) 403 .flatMap(sp -> toPathImpl(sp, context)) 404 .filter(p -> checkValidPathEntry(p, context, isModulePath)) 405 .map(p -> p.toString()) 406 .collect(Collectors.joining(File.pathSeparator))); 407 if (failed) { 408 return Collections.emptyList(); 409 } else { 410 return result.collect(toList()); 411 } 412 } 413 414 // Adapted from compiler method Locations.checkValidModulePathEntry checkValidPathEntry(Path p, String context, boolean isModulePath)415 private boolean checkValidPathEntry(Path p, String context, boolean isModulePath) { 416 if (!Files.exists(p)) { 417 msg("jshell.err.file.not.found", context, p); 418 failed = true; 419 return false; 420 } 421 if (Files.isDirectory(p)) { 422 // if module-path, either an exploded module or a directory of modules 423 return true; 424 } 425 426 String name = p.getFileName().toString(); 427 int lastDot = name.lastIndexOf("."); 428 if (lastDot > 0) { 429 switch (name.substring(lastDot)) { 430 case ".jar": 431 return true; 432 case ".jmod": 433 if (isModulePath) { 434 return true; 435 } 436 } 437 } 438 msg("jshell.err.arg", context, p); 439 failed = true; 440 return false; 441 } 442 toPathImpl(String path, String context)443 private Stream<Path> toPathImpl(String path, String context) { 444 try { 445 return Stream.of(toPathResolvingUserHome(path)); 446 } catch (InvalidPathException ex) { 447 msg("jshell.err.file.not.found", context, path); 448 failed = true; 449 return Stream.empty(); 450 } 451 } 452 parse(OptionSet options)453 Options parse(OptionSet options) { 454 addOptions(OptionKind.CLASS_PATH, 455 validPaths(options.valuesOf(argClassPath), "--class-path", false)); 456 addOptions(OptionKind.MODULE_PATH, 457 validPaths(options.valuesOf(argModulePath), "--module-path", true)); 458 addOptions(OptionKind.ADD_MODULES, options.valuesOf(argAddModules)); 459 addOptions(OptionKind.ADD_EXPORTS, options.valuesOf(argAddExports).stream() 460 .map(mp -> mp.contains("=") ? mp : mp + "=ALL-UNNAMED") 461 .collect(toList()) 462 ); 463 if (options.has(argEnablePreview)) { 464 opts.addAll(OptionKind.ENABLE_PREVIEW, List.of( 465 OptionKind.ENABLE_PREVIEW.optionFlag)); 466 opts.addAll(OptionKind.SOURCE_RELEASE, List.of( 467 OptionKind.SOURCE_RELEASE.optionFlag, 468 System.getProperty("java.specification.version"))); 469 } 470 471 if (failed) { 472 exitCode = 1; 473 return null; 474 } else { 475 return opts; 476 } 477 } 478 addOptions(OptionKind kind, Collection<String> vals)479 void addOptions(OptionKind kind, Collection<String> vals) { 480 if (!vals.isEmpty()) { 481 if (kind.onlyOne && vals.size() > 1) { 482 msg("jshell.err.opt.one", kind.optionFlag); 483 failed = true; 484 return; 485 } 486 if (kind.passFlag) { 487 vals = vals.stream() 488 .flatMap(mp -> Stream.of(kind.optionFlag, mp)) 489 .collect(toList()); 490 } 491 opts.addAll(kind, vals); 492 } 493 } 494 } 495 496 // option parsing for /reload (adds -restore -quiet) 497 private class OptionParserReload extends OptionParserBase { 498 499 private final OptionSpecBuilder argRestore = parser.accepts("restore"); 500 private final OptionSpecBuilder argQuiet = parser.accepts("quiet"); 501 502 private boolean restore = false; 503 private boolean quiet = false; 504 restore()505 boolean restore() { 506 return restore; 507 } 508 quiet()509 boolean quiet() { 510 return quiet; 511 } 512 513 @Override parse(OptionSet options)514 Options parse(OptionSet options) { 515 if (options.has(argRestore)) { 516 restore = true; 517 } 518 if (options.has(argQuiet)) { 519 quiet = true; 520 } 521 return super.parse(options); 522 } 523 } 524 525 // option parsing for command-line 526 private class OptionParserCommandLine extends OptionParserBase { 527 528 private final OptionSpec<String> argStart = parser.accepts("startup").withRequiredArg(); 529 private final OptionSpecBuilder argNoStart = parser.acceptsAll(asList("n", "no-startup")); 530 private final OptionSpec<String> argFeedback = parser.accepts("feedback").withRequiredArg(); 531 private final OptionSpec<String> argExecution = parser.accepts("execution").withRequiredArg(); 532 private final OptionSpecBuilder argQ = parser.accepts("q"); 533 private final OptionSpecBuilder argS = parser.accepts("s"); 534 private final OptionSpecBuilder argV = parser.accepts("v"); 535 private final OptionSpec<String> argR = parser.accepts("R").withRequiredArg(); 536 private final OptionSpec<String> argC = parser.accepts("C").withRequiredArg(); 537 private final OptionSpecBuilder argHelp = parser.acceptsAll(asList("?", "h", "help")); 538 private final OptionSpecBuilder argVersion = parser.accepts("version"); 539 private final OptionSpecBuilder argFullVersion = parser.accepts("full-version"); 540 private final OptionSpecBuilder argShowVersion = parser.accepts("show-version"); 541 private final OptionSpecBuilder argHelpExtra = parser.acceptsAll(asList("X", "help-extra")); 542 543 private String feedbackMode = null; 544 private Startup initialStartup = null; 545 feedbackMode()546 String feedbackMode() { 547 return feedbackMode; 548 } 549 startup()550 Startup startup() { 551 return initialStartup; 552 } 553 554 @Override msg(String key, Object... args)555 void msg(String key, Object... args) { 556 errormsg(key, args); 557 } 558 559 /** 560 * Parse the command line options. 561 * @return the options as an Options object, or null if error 562 */ 563 @Override parse(OptionSet options)564 Options parse(OptionSet options) { 565 if (options.has(argHelp)) { 566 printUsage(); 567 return null; 568 } 569 if (options.has(argHelpExtra)) { 570 printUsageX(); 571 return null; 572 } 573 if (options.has(argVersion)) { 574 cmdout.printf("jshell %s\n", version()); 575 return null; 576 } 577 if (options.has(argFullVersion)) { 578 cmdout.printf("jshell %s\n", fullVersion()); 579 return null; 580 } 581 if (options.has(argShowVersion)) { 582 cmdout.printf("jshell %s\n", version()); 583 } 584 if ((options.valuesOf(argFeedback).size() + 585 (options.has(argQ) ? 1 : 0) + 586 (options.has(argS) ? 1 : 0) + 587 (options.has(argV) ? 1 : 0)) > 1) { 588 msg("jshell.err.opt.feedback.one"); 589 exitCode = 1; 590 return null; 591 } else if (options.has(argFeedback)) { 592 feedbackMode = options.valueOf(argFeedback); 593 } else if (options.has("q")) { 594 feedbackMode = "concise"; 595 } else if (options.has("s")) { 596 feedbackMode = "silent"; 597 } else if (options.has("v")) { 598 feedbackMode = "verbose"; 599 } 600 if (options.has(argStart)) { 601 List<String> sts = options.valuesOf(argStart); 602 if (options.has("no-startup")) { 603 msg("jshell.err.opt.startup.conflict"); 604 exitCode = 1; 605 return null; 606 } 607 initialStartup = Startup.fromFileList(sts, "--startup", new InitMessageHandler()); 608 if (initialStartup == null) { 609 exitCode = 1; 610 return null; 611 } 612 } else if (options.has(argNoStart)) { 613 initialStartup = Startup.noStartup(); 614 } else { 615 String packedStartup = prefs.get(STARTUP_KEY); 616 initialStartup = Startup.unpack(packedStartup, new InitMessageHandler()); 617 } 618 if (options.has(argExecution)) { 619 executionControlSpec = options.valueOf(argExecution); 620 } 621 addOptions(OptionKind.TO_REMOTE_VM, options.valuesOf(argR)); 622 addOptions(OptionKind.TO_COMPILER, options.valuesOf(argC)); 623 return super.parse(options); 624 } 625 } 626 627 /** 628 * Encapsulate a history of snippets and commands which can be replayed. 629 */ 630 private static class ReplayableHistory { 631 632 // the history 633 private List<String> hist; 634 635 // the length of the history as of last save 636 private int lastSaved; 637 ReplayableHistory(List<String> hist)638 private ReplayableHistory(List<String> hist) { 639 this.hist = hist; 640 this.lastSaved = 0; 641 } 642 643 // factory for empty histories emptyHistory()644 static ReplayableHistory emptyHistory() { 645 return new ReplayableHistory(new ArrayList<>()); 646 } 647 648 // factory for history stored in persistent storage fromPrevious(PersistentStorage prefs)649 static ReplayableHistory fromPrevious(PersistentStorage prefs) { 650 // Read replay history from last jshell session 651 String prevReplay = prefs.get(REPLAY_RESTORE_KEY); 652 if (prevReplay == null) { 653 return null; 654 } else { 655 return new ReplayableHistory(Arrays.asList(prevReplay.split(RECORD_SEPARATOR))); 656 } 657 658 } 659 660 // store the history in persistent storage storeHistory(PersistentStorage prefs)661 void storeHistory(PersistentStorage prefs) { 662 if (hist.size() > lastSaved) { 663 // Prevent history overflow by calculating what will fit, starting 664 // with most recent 665 int sepLen = RECORD_SEPARATOR.length(); 666 int length = 0; 667 int first = hist.size(); 668 while (length < Preferences.MAX_VALUE_LENGTH && --first >= 0) { 669 length += hist.get(first).length() + sepLen; 670 } 671 if (first >= 0) { 672 hist = hist.subList(first + 1, hist.size()); 673 } 674 String shist = String.join(RECORD_SEPARATOR, hist); 675 prefs.put(REPLAY_RESTORE_KEY, shist); 676 markSaved(); 677 } 678 prefs.flush(); 679 } 680 681 // add a snippet or command to the history add(String s)682 void add(String s) { 683 hist.add(s); 684 } 685 686 // return history to reloaded iterable()687 Iterable<String> iterable() { 688 return hist; 689 } 690 691 // mark that persistent storage and current history are in sync markSaved()692 void markSaved() { 693 lastSaved = hist.size(); 694 } 695 } 696 697 /** 698 * Is the input/output currently interactive 699 * 700 * @return true if console 701 */ interactive()702 boolean interactive() { 703 return input != null && input.interactiveOutput(); 704 } 705 debug(String format, Object... args)706 void debug(String format, Object... args) { 707 if (debug) { 708 cmderr.printf(format + "\n", args); 709 } 710 } 711 712 /** 713 * Must show command output 714 * 715 * @param format printf format 716 * @param args printf args 717 */ 718 @Override hard(String format, Object... args)719 public void hard(String format, Object... args) { 720 cmdout.printf(prefix(format), args); 721 } 722 723 /** 724 * Error command output 725 * 726 * @param format printf format 727 * @param args printf args 728 */ error(String format, Object... args)729 void error(String format, Object... args) { 730 (interactiveModeBegun? cmdout : cmderr).printf(prefixError(format), args); 731 } 732 733 /** 734 * Should optional informative be displayed? 735 * @return true if they should be displayed 736 */ 737 @Override showFluff()738 public boolean showFluff() { 739 return feedback.shouldDisplayCommandFluff() && interactive(); 740 } 741 742 /** 743 * Optional output 744 * 745 * @param format printf format 746 * @param args printf args 747 */ 748 @Override fluff(String format, Object... args)749 public void fluff(String format, Object... args) { 750 if (showFluff()) { 751 hard(format, args); 752 } 753 } 754 755 /** 756 * Resource bundle look-up 757 * 758 * @param key the resource key 759 */ getResourceString(String key)760 String getResourceString(String key) { 761 if (outputRB == null) { 762 try { 763 outputRB = ResourceBundle.getBundle(L10N_RB_NAME, locale); 764 } catch (MissingResourceException mre) { 765 error("Cannot find ResourceBundle: %s for locale: %s", L10N_RB_NAME, locale); 766 return ""; 767 } 768 } 769 String s; 770 try { 771 s = outputRB.getString(key); 772 } catch (MissingResourceException mre) { 773 error("Missing resource: %s in %s", key, L10N_RB_NAME); 774 return ""; 775 } 776 return s; 777 } 778 779 /** 780 * Add normal prefixing/postfixing to embedded newlines in a string, 781 * bracketing with normal prefix/postfix 782 * 783 * @param s the string to prefix 784 * @return the pre/post-fixed and bracketed string 785 */ prefix(String s)786 String prefix(String s) { 787 return prefix(s, feedback.getPre(), feedback.getPost()); 788 } 789 790 /** 791 * Add error prefixing/postfixing to embedded newlines in a string, 792 * bracketing with error prefix/postfix 793 * 794 * @param s the string to prefix 795 * @return the pre/post-fixed and bracketed string 796 */ prefixError(String s)797 String prefixError(String s) { 798 return prefix(s, feedback.getErrorPre(), feedback.getErrorPost()); 799 } 800 801 /** 802 * Add prefixing/postfixing to embedded newlines in a string, 803 * bracketing with prefix/postfix. No prefixing when non-interactive. 804 * Result is expected to be the format for a printf. 805 * 806 * @param s the string to prefix 807 * @param pre the string to prepend to each line 808 * @param post the string to append to each line (replacing newline) 809 * @return the pre/post-fixed and bracketed string 810 */ prefix(String s, String pre, String post)811 String prefix(String s, String pre, String post) { 812 if (s == null) { 813 return ""; 814 } 815 if (!interactiveModeBegun) { 816 // messages expect to be new-line terminated (even when not prefixed) 817 return s + "%n"; 818 } 819 String pp = s.replaceAll("\\R", post + pre); 820 if (pp.endsWith(post + pre)) { 821 // prevent an extra prefix char and blank line when the string 822 // already terminates with newline 823 pp = pp.substring(0, pp.length() - (post + pre).length()); 824 } 825 return pre + pp + post; 826 } 827 828 /** 829 * Print using resource bundle look-up and adding prefix and postfix 830 * 831 * @param key the resource key 832 */ hardrb(String key)833 void hardrb(String key) { 834 hard(getResourceString(key)); 835 } 836 837 /** 838 * Format using resource bundle look-up using MessageFormat 839 * 840 * @param key the resource key 841 * @param args 842 */ messageFormat(String key, Object... args)843 String messageFormat(String key, Object... args) { 844 String rs = getResourceString(key); 845 return MessageFormat.format(rs, args); 846 } 847 848 /** 849 * Print using resource bundle look-up, MessageFormat, and add prefix and 850 * postfix 851 * 852 * @param key the resource key 853 * @param args 854 */ 855 @Override hardmsg(String key, Object... args)856 public void hardmsg(String key, Object... args) { 857 hard(messageFormat(key, args)); 858 } 859 860 /** 861 * Print error using resource bundle look-up, MessageFormat, and add prefix 862 * and postfix 863 * 864 * @param key the resource key 865 * @param args 866 */ 867 @Override errormsg(String key, Object... args)868 public void errormsg(String key, Object... args) { 869 error("%s", messageFormat(key, args)); 870 } 871 872 /** 873 * Print (fluff) using resource bundle look-up, MessageFormat, and add 874 * prefix and postfix 875 * 876 * @param key the resource key 877 * @param args 878 */ 879 @Override fluffmsg(String key, Object... args)880 public void fluffmsg(String key, Object... args) { 881 if (showFluff()) { 882 hardmsg(key, args); 883 } 884 } 885 hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b)886 <T> void hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b) { 887 Map<String, String> a2b = stream.collect(toMap(a, b, 888 (m1, m2) -> m1, 889 LinkedHashMap::new)); 890 for (Entry<String, String> e : a2b.entrySet()) { 891 hard("%s", e.getKey()); 892 cmdout.printf(prefix(e.getValue(), feedback.getPre() + "\t", feedback.getPost())); 893 } 894 } 895 896 /** 897 * Trim whitespace off end of string 898 * 899 * @param s 900 * @return 901 */ trimEnd(String s)902 static String trimEnd(String s) { 903 int last = s.length() - 1; 904 int i = last; 905 while (i >= 0 && Character.isWhitespace(s.charAt(i))) { 906 --i; 907 } 908 if (i != last) { 909 return s.substring(0, i + 1); 910 } else { 911 return s; 912 } 913 } 914 indent()915 private String indent() { 916 String indentValue = prefs.get(INDENT_KEY); 917 if (indentValue == null) indentValue = Integer.toString(DEFAULT_INDENT); 918 return indentValue; 919 } 920 921 /** 922 * The entry point into the JShell tool. 923 * 924 * @param args the command-line arguments 925 * @throws Exception catastrophic fatal exception 926 * @return the exit code 927 */ start(String[] args)928 public int start(String[] args) throws Exception { 929 OptionParserCommandLine commandLineArgs = new OptionParserCommandLine(); 930 options = commandLineArgs.parse(args); 931 if (options == null) { 932 // A null means end immediately, this may be an error or because 933 // of options like --version. Exit code has been set. 934 return exitCode; 935 } 936 startup = commandLineArgs.startup(); 937 // initialize editor settings 938 configEditor(); 939 // initialize JShell instance 940 try { 941 resetState(); 942 } catch (IllegalStateException ex) { 943 // Display just the cause (not a exception backtrace) 944 cmderr.println(ex.getMessage()); 945 //abort 946 return 1; 947 } 948 // Read replay history from last jshell session into previous history 949 replayableHistoryPrevious = ReplayableHistory.fromPrevious(prefs); 950 // load snippet/command files given on command-line 951 for (String loadFile : commandLineArgs.nonOptions()) { 952 if (!runFile(loadFile, "jshell")) { 953 // Load file failed -- abort 954 return 1; 955 } 956 } 957 // if we survived that... 958 if (regenerateOnDeath) { 959 // initialize the predefined feedback modes 960 initFeedback(commandLineArgs.feedbackMode()); 961 } 962 // check again, as feedback setting could have failed 963 if (regenerateOnDeath) { 964 // if we haven't died, and the feedback mode wants fluff, print welcome 965 interactiveModeBegun = true; 966 if (feedback.shouldDisplayCommandFluff()) { 967 hardmsg("jshell.msg.welcome", version()); 968 } 969 // Be sure history is always saved so that user code isn't lost 970 Thread shutdownHook = new Thread() { 971 @Override 972 public void run() { 973 replayableHistory.storeHistory(prefs); 974 } 975 }; 976 Runtime.getRuntime().addShutdownHook(shutdownHook); 977 // execute from user input 978 try (IOContext in = new ConsoleIOContext(this, cmdin, console)) { 979 int indent; 980 try { 981 String indentValue = indent(); 982 indent = Integer.parseInt(indentValue); 983 } catch (NumberFormatException ex) { 984 indent = DEFAULT_INDENT; 985 } 986 in.setIndent(indent); 987 while (regenerateOnDeath) { 988 if (!live) { 989 resetState(); 990 } 991 run(in); 992 } 993 } finally { 994 replayableHistory.storeHistory(prefs); 995 closeState(); 996 try { 997 Runtime.getRuntime().removeShutdownHook(shutdownHook); 998 } catch (Exception ex) { 999 // ignore, this probably caused by VM aready being shutdown 1000 // and this is the last act anyhow 1001 } 1002 } 1003 } 1004 closeState(); 1005 return exitCode; 1006 } 1007 configEditor()1008 private EditorSetting configEditor() { 1009 // Read retained editor setting (if any) 1010 editor = EditorSetting.fromPrefs(prefs); 1011 if (editor != null) { 1012 return editor; 1013 } 1014 // Try getting editor setting from OS environment variables 1015 for (String envvar : EDITOR_ENV_VARS) { 1016 String v = envvars.get(envvar); 1017 if (v != null) { 1018 return editor = new EditorSetting(v.split("\\s+"), false); 1019 } 1020 } 1021 // Default to the built-in editor 1022 return editor = BUILT_IN_EDITOR; 1023 } 1024 printUsage()1025 private void printUsage() { 1026 cmdout.print(getResourceString("help.usage")); 1027 } 1028 printUsageX()1029 private void printUsageX() { 1030 cmdout.print(getResourceString("help.usage.x")); 1031 } 1032 1033 /** 1034 * Message handler to use during initial start-up. 1035 */ 1036 private class InitMessageHandler implements MessageHandler { 1037 1038 @Override fluff(String format, Object... args)1039 public void fluff(String format, Object... args) { 1040 //ignore 1041 } 1042 1043 @Override fluffmsg(String messageKey, Object... args)1044 public void fluffmsg(String messageKey, Object... args) { 1045 //ignore 1046 } 1047 1048 @Override hard(String format, Object... args)1049 public void hard(String format, Object... args) { 1050 //ignore 1051 } 1052 1053 @Override hardmsg(String messageKey, Object... args)1054 public void hardmsg(String messageKey, Object... args) { 1055 //ignore 1056 } 1057 1058 @Override errormsg(String messageKey, Object... args)1059 public void errormsg(String messageKey, Object... args) { 1060 JShellTool.this.errormsg(messageKey, args); 1061 } 1062 1063 @Override showFluff()1064 public boolean showFluff() { 1065 return false; 1066 } 1067 } 1068 resetState()1069 private void resetState() { 1070 closeState(); 1071 1072 // Initialize tool id mapping 1073 mainNamespace = new NameSpace("main", ""); 1074 startNamespace = new NameSpace("start", "s"); 1075 errorNamespace = new NameSpace("error", "e"); 1076 mapSnippet = new LinkedHashMap<>(); 1077 currentNameSpace = startNamespace; 1078 1079 // Reset the replayable history, saving the old for restore 1080 replayableHistoryPrevious = replayableHistory; 1081 replayableHistory = ReplayableHistory.emptyHistory(); 1082 JShell.Builder builder = 1083 JShell.builder() 1084 .in(userin) 1085 .out(userout) 1086 .err(usererr) 1087 .tempVariableNameGenerator(() -> "$" + currentNameSpace.tidNext()) 1088 .idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive()) 1089 ? currentNameSpace.tid(sn) 1090 : errorNamespace.tid(sn)) 1091 .remoteVMOptions(options.remoteVmOptions()) 1092 .compilerOptions(options.compilerOptions()); 1093 if (executionControlSpec != null) { 1094 builder.executionEngine(executionControlSpec); 1095 } 1096 state = builder.build(); 1097 InternalDebugControl.setDebugFlags(state, debugFlags); 1098 shutdownSubscription = state.onShutdown((JShell deadState) -> { 1099 if (deadState == state) { 1100 hardmsg("jshell.msg.terminated"); 1101 fluffmsg("jshell.msg.terminated.restore"); 1102 live = false; 1103 } 1104 }); 1105 analysis = state.sourceCodeAnalysis(); 1106 live = true; 1107 1108 // Run the start-up script. 1109 // Avoid an infinite loop running start-up while running start-up. 1110 // This could, otherwise, occur when /env /reset or /reload commands are 1111 // in the start-up script. 1112 if (!isCurrentlyRunningStartup) { 1113 try { 1114 isCurrentlyRunningStartup = true; 1115 startUpRun(startup.toString()); 1116 } finally { 1117 isCurrentlyRunningStartup = false; 1118 } 1119 } 1120 // Record subsequent snippets in the main namespace. 1121 currentNameSpace = mainNamespace; 1122 } 1123 1124 //where -- one-time per run initialization of feedback modes initFeedback(String initMode)1125 private void initFeedback(String initMode) { 1126 // No fluff, no prefix, for init failures 1127 MessageHandler initmh = new InitMessageHandler(); 1128 // Execute the feedback initialization code in the resource file 1129 startUpRun(getResourceString("startup.feedback")); 1130 // These predefined modes are read-only 1131 feedback.markModesReadOnly(); 1132 // Restore user defined modes retained on previous run with /set mode -retain 1133 boolean oldModes = false; 1134 String encoded = prefs.get(MODE2_KEY); 1135 if (encoded == null || encoded.isEmpty()) { 1136 // No new layout modes, see if there are old (JDK-14 and before) modes 1137 oldModes = true; 1138 encoded = prefs.get(MODE_KEY); 1139 } 1140 if (encoded != null && !encoded.isEmpty()) { 1141 if (!feedback.restoreEncodedModes(initmh, encoded)) { 1142 // Catastrophic corruption -- remove the retained modes 1143 // Leave old mode corruption clean-up to old versions 1144 if (!oldModes) { 1145 prefs.remove(MODE2_KEY); 1146 } 1147 } 1148 } 1149 if (initMode != null) { 1150 // The feedback mode to use was specified on the command line, use it 1151 if (!setFeedback(initmh, new ArgTokenizer("--feedback", initMode))) { 1152 regenerateOnDeath = false; 1153 exitCode = 1; 1154 } 1155 } else { 1156 String fb = prefs.get(FEEDBACK_KEY); 1157 if (fb != null) { 1158 // Restore the feedback mode to use that was retained 1159 // on a previous run with /set feedback -retain 1160 setFeedback(initmh, new ArgTokenizer("previous retain feedback", "-retain " + fb)); 1161 } 1162 } 1163 } 1164 1165 //where startUpRun(String start)1166 private void startUpRun(String start) { 1167 try (IOContext suin = new ScannerIOContext(new StringReader(start))) { 1168 run(suin); 1169 } catch (Exception ex) { 1170 errormsg("jshell.err.startup.unexpected.exception", ex); 1171 ex.printStackTrace(cmderr); 1172 } 1173 } 1174 closeState()1175 private void closeState() { 1176 live = false; 1177 JShell oldState = state; 1178 if (oldState != null) { 1179 state = null; 1180 analysis = null; 1181 oldState.unsubscribe(shutdownSubscription); // No notification 1182 oldState.close(); 1183 } 1184 } 1185 1186 /** 1187 * Main loop 1188 * 1189 * @param in the line input/editing context 1190 */ run(IOContext in)1191 private void run(IOContext in) { 1192 IOContext oldInput = input; 1193 input = in; 1194 try { 1195 // remaining is the source left after one snippet is evaluated 1196 String remaining = ""; 1197 while (live) { 1198 // Get a line(s) of input 1199 String src = getInput(remaining); 1200 // Process the snippet or command, returning the remaining source 1201 remaining = processInput(src); 1202 } 1203 } catch (EOFException ex) { 1204 // Just exit loop 1205 } catch (IOException ex) { 1206 errormsg("jshell.err.unexpected.exception", ex); 1207 } finally { 1208 input = oldInput; 1209 } 1210 } 1211 1212 /** 1213 * Process an input command or snippet. 1214 * 1215 * @param src the source to process 1216 * @return any remaining input to processed 1217 */ processInput(String src)1218 private String processInput(String src) { 1219 if (isCommand(src)) { 1220 // It is a command 1221 processCommand(src.trim()); 1222 // No remaining input after a command 1223 return ""; 1224 } else { 1225 // It is a snipet. Separate the source from the remaining. Evaluate 1226 // the source 1227 CompletionInfo an = analysis.analyzeCompletion(src); 1228 if (processSourceCatchingReset(trimEnd(an.source()))) { 1229 // Snippet was successful use any leftover source 1230 return an.remaining(); 1231 } else { 1232 // Snippet failed, throw away any remaining source 1233 return ""; 1234 } 1235 } 1236 } 1237 1238 /** 1239 * Get the input line (or, if incomplete, lines). 1240 * 1241 * @param initial leading input (left over after last snippet) 1242 * @return the complete input snippet or command 1243 * @throws IOException on unexpected I/O error 1244 */ getInput(String initial)1245 private String getInput(String initial) throws IOException{ 1246 String src = initial; 1247 while (live) { // loop while incomplete (and live) 1248 if (!src.isEmpty() && isComplete(src)) { 1249 return src; 1250 } 1251 String firstLinePrompt = interactive() 1252 ? testPrompt ? " \005" 1253 : feedback.getPrompt(currentNameSpace.tidNext()) 1254 : "" // Non-interactive -- no prompt 1255 ; 1256 String continuationPrompt = interactive() 1257 ? testPrompt ? " \006" 1258 : feedback.getContinuationPrompt(currentNameSpace.tidNext()) 1259 : "" // Non-interactive -- no prompt 1260 ; 1261 String line; 1262 try { 1263 line = input.readLine(firstLinePrompt, continuationPrompt, src.isEmpty(), src); 1264 } catch (InputInterruptedException ex) { 1265 //input interrupted - clearing current state 1266 src = ""; 1267 continue; 1268 } 1269 if (line == null) { 1270 //EOF 1271 if (input.interactiveOutput()) { 1272 // End after user ctrl-D 1273 regenerateOnDeath = false; 1274 } 1275 throw new EOFException(); // no more input 1276 } 1277 src = src.isEmpty() 1278 ? line 1279 : src + "\n" + line; 1280 } 1281 throw new EOFException(); // not longer live 1282 } 1283 isComplete(String src)1284 public boolean isComplete(String src) { 1285 String check; 1286 1287 if (isCommand(src)) { 1288 // A command can only be incomplete if it is a /exit with 1289 // an argument 1290 int sp = src.indexOf(" "); 1291 if (sp < 0) return true; 1292 check = src.substring(sp).trim(); 1293 if (check.isEmpty()) return true; 1294 String cmd = src.substring(0, sp); 1295 Command[] match = findCommand(cmd, c -> c.kind.isRealCommand); 1296 if (match.length != 1 || !match[0].command.equals("/exit")) { 1297 // A command with no snippet arg, so no multi-line input 1298 return true; 1299 } 1300 } else { 1301 // For a snippet check the whole source 1302 check = src; 1303 } 1304 Completeness comp = analysis.analyzeCompletion(check).completeness(); 1305 if (comp.isComplete() || comp == Completeness.EMPTY) { 1306 return true; 1307 } 1308 return false; 1309 } 1310 isCommand(String line)1311 private boolean isCommand(String line) { 1312 return line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*"); 1313 } 1314 addToReplayHistory(String s)1315 private void addToReplayHistory(String s) { 1316 if (!isCurrentlyRunningStartup) { 1317 replayableHistory.add(s); 1318 } 1319 } 1320 1321 /** 1322 * Process a source snippet. 1323 * 1324 * @param src the snippet source to process 1325 * @return true on success, false on failure 1326 */ processSourceCatchingReset(String src)1327 private boolean processSourceCatchingReset(String src) { 1328 try { 1329 input.beforeUserCode(); 1330 return processSource(src); 1331 } catch (IllegalStateException ex) { 1332 hard("Resetting..."); 1333 live = false; // Make double sure 1334 return false; 1335 } finally { 1336 input.afterUserCode(); 1337 } 1338 } 1339 1340 /** 1341 * Process a command (as opposed to a snippet) -- things that start with 1342 * slash. 1343 * 1344 * @param input 1345 */ processCommand(String input)1346 private void processCommand(String input) { 1347 if (input.startsWith("/-")) { 1348 try { 1349 //handle "/-[number]" 1350 cmdUseHistoryEntry(Integer.parseInt(input.substring(1))); 1351 return ; 1352 } catch (NumberFormatException ex) { 1353 //ignore 1354 } 1355 } 1356 String cmd; 1357 String arg; 1358 int idx = input.indexOf(' '); 1359 if (idx > 0) { 1360 arg = input.substring(idx + 1).trim(); 1361 cmd = input.substring(0, idx); 1362 } else { 1363 cmd = input; 1364 arg = ""; 1365 } 1366 // find the command as a "real command", not a pseudo-command or doc subject 1367 Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand); 1368 switch (candidates.length) { 1369 case 0: 1370 // not found, it is either a rerun-ID command or an error 1371 if (RERUN_ID.matcher(cmd).matches()) { 1372 // it is in the form of a snipppet id, see if it is a valid history reference 1373 rerunHistoryEntriesById(input); 1374 } else { 1375 errormsg("jshell.err.invalid.command", cmd); 1376 fluffmsg("jshell.msg.help.for.help"); 1377 } 1378 break; 1379 case 1: 1380 Command command = candidates[0]; 1381 // If comand was successful and is of a replayable kind, add it the replayable history 1382 if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) { 1383 addToReplayHistory((command.command + " " + arg).trim()); 1384 } 1385 break; 1386 default: 1387 // command if too short (ambigous), show the possibly matches 1388 errormsg("jshell.err.command.ambiguous", cmd, 1389 Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", "))); 1390 fluffmsg("jshell.msg.help.for.help"); 1391 break; 1392 } 1393 } 1394 findCommand(String cmd, Predicate<Command> filter)1395 private Command[] findCommand(String cmd, Predicate<Command> filter) { 1396 Command exact = commands.get(cmd); 1397 if (exact != null) 1398 return new Command[] {exact}; 1399 1400 return commands.values() 1401 .stream() 1402 .filter(filter) 1403 .filter(command -> command.command.startsWith(cmd)) 1404 .toArray(Command[]::new); 1405 } 1406 toPathResolvingUserHome(String pathString)1407 static Path toPathResolvingUserHome(String pathString) { 1408 if (pathString.replace(File.separatorChar, '/').startsWith("~/")) 1409 return Paths.get(System.getProperty("user.home"), pathString.substring(2)); 1410 else 1411 return Paths.get(pathString); 1412 } 1413 1414 static final class Command { 1415 public final String command; 1416 public final String helpKey; 1417 public final Function<String,Boolean> run; 1418 public final CompletionProvider completions; 1419 public final CommandKind kind; 1420 1421 // NORMAL Commands Command(String command, Function<String,Boolean> run, CompletionProvider completions)1422 public Command(String command, Function<String,Boolean> run, CompletionProvider completions) { 1423 this(command, run, completions, CommandKind.NORMAL); 1424 } 1425 1426 // Special kinds of Commands Command(String command, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind)1427 public Command(String command, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) { 1428 this(command, "help." + command.substring(1), 1429 run, completions, kind); 1430 } 1431 1432 // Documentation pseudo-commands Command(String command, String helpKey, CommandKind kind)1433 public Command(String command, String helpKey, CommandKind kind) { 1434 this(command, helpKey, 1435 arg -> { throw new IllegalStateException(); }, 1436 EMPTY_COMPLETION_PROVIDER, 1437 kind); 1438 } 1439 Command(String command, String helpKey, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind)1440 public Command(String command, String helpKey, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) { 1441 this.command = command; 1442 this.helpKey = helpKey; 1443 this.run = run; 1444 this.completions = completions; 1445 this.kind = kind; 1446 } 1447 1448 } 1449 1450 interface CompletionProvider { completionSuggestions(String input, int cursor, int[] anchor)1451 List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor); 1452 1453 } 1454 1455 enum CommandKind { 1456 NORMAL(true, true, true), 1457 REPLAY(true, true, true), 1458 HIDDEN(true, false, false), 1459 HELP_ONLY(false, true, false), 1460 HELP_SUBJECT(false, false, false); 1461 1462 final boolean isRealCommand; 1463 final boolean showInHelp; 1464 final boolean shouldSuggestCompletions; CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions)1465 private CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions) { 1466 this.isRealCommand = isRealCommand; 1467 this.showInHelp = showInHelp; 1468 this.shouldSuggestCompletions = shouldSuggestCompletions; 1469 } 1470 } 1471 1472 static final class FixedCompletionProvider implements CompletionProvider { 1473 1474 private final String[] alternatives; 1475 FixedCompletionProvider(String... alternatives)1476 public FixedCompletionProvider(String... alternatives) { 1477 this.alternatives = alternatives; 1478 } 1479 1480 // Add more options to an existing provider FixedCompletionProvider(FixedCompletionProvider base, String... alternatives)1481 public FixedCompletionProvider(FixedCompletionProvider base, String... alternatives) { 1482 List<String> l = new ArrayList<>(Arrays.asList(base.alternatives)); 1483 l.addAll(Arrays.asList(alternatives)); 1484 this.alternatives = l.toArray(new String[l.size()]); 1485 } 1486 1487 @Override completionSuggestions(String input, int cursor, int[] anchor)1488 public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) { 1489 List<Suggestion> result = new ArrayList<>(); 1490 1491 for (String alternative : alternatives) { 1492 if (alternative.startsWith(input)) { 1493 result.add(new ArgSuggestion(alternative)); 1494 } 1495 } 1496 1497 anchor[0] = 0; 1498 1499 return result; 1500 } 1501 1502 } 1503 1504 static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider(); 1505 private static final CompletionProvider SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start ", "-history"); 1506 private static final CompletionProvider SAVE_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all ", "-start ", "-history "); 1507 private static final CompletionProvider HISTORY_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all"); 1508 private static final CompletionProvider SNIPPET_OPTION_COMPLETION_PROVIDER = new FixedCompletionProvider("-all", "-start " ); 1509 private static final FixedCompletionProvider COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider( 1510 "-class-path ", "-module-path ", "-add-modules ", "-add-exports "); 1511 private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider( 1512 COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER, 1513 "-restore ", "-quiet "); 1514 private static final CompletionProvider SET_MODE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-command", "-quiet", "-delete"); 1515 private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true); 1516 private static final Map<String, CompletionProvider> ARG_OPTIONS = new HashMap<>(); 1517 static { 1518 ARG_OPTIONS.put("-class-path", classPathCompletion()); 1519 ARG_OPTIONS.put("-module-path", fileCompletions(Files::isDirectory)); 1520 ARG_OPTIONS.put("-add-modules", EMPTY_COMPLETION_PROVIDER); 1521 ARG_OPTIONS.put("-add-exports", EMPTY_COMPLETION_PROVIDER); 1522 } 1523 private final Map<String, Command> commands = new LinkedHashMap<>(); registerCommand(Command cmd)1524 private void registerCommand(Command cmd) { 1525 commands.put(cmd.command, cmd); 1526 } 1527 skipWordThenCompletion(CompletionProvider completionProvider)1528 private static CompletionProvider skipWordThenCompletion(CompletionProvider completionProvider) { 1529 return (input, cursor, anchor) -> { 1530 List<Suggestion> result = Collections.emptyList(); 1531 1532 int space = input.indexOf(' '); 1533 if (space != -1) { 1534 String rest = input.substring(space + 1); 1535 result = completionProvider.completionSuggestions(rest, cursor - space - 1, anchor); 1536 anchor[0] += space + 1; 1537 } 1538 1539 return result; 1540 }; 1541 } 1542 fileCompletions(Predicate<Path> accept)1543 private static CompletionProvider fileCompletions(Predicate<Path> accept) { 1544 return (code, cursor, anchor) -> { 1545 int lastSlash = code.lastIndexOf('/'); 1546 String path = code.substring(0, lastSlash + 1); 1547 String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code; 1548 Path current = toPathResolvingUserHome(path); 1549 List<Suggestion> result = new ArrayList<>(); 1550 try (Stream<Path> dir = Files.list(current)) { 1551 dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix)) 1552 .map(f -> new ArgSuggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : ""))) 1553 .forEach(result::add); 1554 } catch (IOException ex) { 1555 //ignore... 1556 } 1557 if (path.isEmpty()) { 1558 StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false) 1559 .filter(root -> Files.exists(root)) 1560 .filter(root -> accept.test(root) && root.toString().startsWith(prefix)) 1561 .map(root -> new ArgSuggestion(root.toString())) 1562 .forEach(result::add); 1563 } 1564 anchor[0] = path.length(); 1565 return result; 1566 }; 1567 } 1568 1569 private static CompletionProvider classPathCompletion() { 1570 return fileCompletions(p -> Files.isDirectory(p) || 1571 p.getFileName().toString().endsWith(".zip") || 1572 p.getFileName().toString().endsWith(".jar")); 1573 } 1574 1575 // Completion based on snippet supplier 1576 private CompletionProvider snippetCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) { 1577 return (prefix, cursor, anchor) -> { 1578 anchor[0] = 0; 1579 int space = prefix.lastIndexOf(' '); 1580 Set<String> prior = new HashSet<>(Arrays.asList(prefix.split(" "))); 1581 if (prior.contains("-all") || prior.contains("-history")) { 1582 return Collections.emptyList(); 1583 } 1584 String argPrefix = prefix.substring(space + 1); 1585 return snippetsSupplier.get() 1586 .filter(k -> !prior.contains(String.valueOf(k.id())) 1587 && (!(k instanceof DeclarationSnippet) 1588 || !prior.contains(((DeclarationSnippet) k).name()))) 1589 .flatMap(k -> (k instanceof DeclarationSnippet) 1590 ? Stream.of(String.valueOf(k.id()) + " ", ((DeclarationSnippet) k).name() + " ") 1591 : Stream.of(String.valueOf(k.id()) + " ")) 1592 .filter(k -> k.startsWith(argPrefix)) 1593 .map(ArgSuggestion::new) 1594 .collect(Collectors.toList()); 1595 }; 1596 } 1597 1598 // Completion based on snippet supplier with -all -start (and sometimes -history) options 1599 private CompletionProvider snippetWithOptionCompletion(CompletionProvider optionProvider, 1600 Supplier<Stream<? extends Snippet>> snippetsSupplier) { 1601 return (code, cursor, anchor) -> { 1602 List<Suggestion> result = new ArrayList<>(); 1603 int pastSpace = code.lastIndexOf(' ') + 1; // zero if no space 1604 if (pastSpace == 0) { 1605 result.addAll(optionProvider.completionSuggestions(code, cursor, anchor)); 1606 } 1607 result.addAll(snippetCompletion(snippetsSupplier).completionSuggestions(code, cursor, anchor)); 1608 anchor[0] += pastSpace; 1609 return result; 1610 }; 1611 } 1612 1613 // Completion of help, commands and subjects 1614 private CompletionProvider helpCompletion() { 1615 return (code, cursor, anchor) -> { 1616 List<Suggestion> result; 1617 int pastSpace = code.indexOf(' ') + 1; // zero if no space 1618 if (pastSpace == 0) { 1619 // initially suggest commands (with slash) and subjects, 1620 // however, if their subject starts without slash, include 1621 // commands without slash 1622 boolean noslash = code.length() > 0 && !code.startsWith("/"); 1623 result = new FixedCompletionProvider(commands.values().stream() 1624 .filter(cmd -> cmd.kind.showInHelp || cmd.kind == CommandKind.HELP_SUBJECT) 1625 .map(c -> ((noslash && c.command.startsWith("/")) 1626 ? c.command.substring(1) 1627 : c.command) + " ") 1628 .toArray(String[]::new)) 1629 .completionSuggestions(code, cursor, anchor); 1630 } else if (code.startsWith("/se") || code.startsWith("se")) { 1631 result = new FixedCompletionProvider(SET_SUBCOMMANDS) 1632 .completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor); 1633 } else { 1634 result = Collections.emptyList(); 1635 } 1636 anchor[0] += pastSpace; 1637 return result; 1638 }; 1639 } 1640 1641 private static CompletionProvider saveCompletion() { 1642 return (code, cursor, anchor) -> { 1643 List<Suggestion> result = new ArrayList<>(); 1644 int space = code.indexOf(' '); 1645 if (space == (-1)) { 1646 result.addAll(SAVE_OPTION_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor)); 1647 } 1648 result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor)); 1649 anchor[0] += space + 1; 1650 return result; 1651 }; 1652 } 1653 1654 // command-line-like option completion -- options with values 1655 private static CompletionProvider optionCompletion(CompletionProvider provider) { 1656 return (code, cursor, anchor) -> { 1657 Matcher ovm = OPTION_VALUE_PATTERN.matcher(code); 1658 if (ovm.matches()) { 1659 String flag = ovm.group("flag"); 1660 List<CompletionProvider> ps = ARG_OPTIONS.entrySet().stream() 1661 .filter(es -> es.getKey().startsWith(flag)) 1662 .map(es -> es.getValue()) 1663 .collect(toList()); 1664 if (ps.size() == 1) { 1665 int pastSpace = ovm.start("val"); 1666 List<Suggestion> result = ps.get(0).completionSuggestions( 1667 ovm.group("val"), cursor - pastSpace, anchor); 1668 anchor[0] += pastSpace; 1669 return result; 1670 } 1671 } 1672 Matcher om = OPTION_PATTERN.matcher(code); 1673 if (om.matches()) { 1674 int pastSpace = om.start("flag"); 1675 List<Suggestion> result = provider.completionSuggestions( 1676 om.group("flag"), cursor - pastSpace, anchor); 1677 if (!om.group("dd").isEmpty()) { 1678 result = result.stream() 1679 .map(sug -> new Suggestion() { 1680 @Override 1681 public String continuation() { 1682 return "-" + sug.continuation(); 1683 } 1684 1685 @Override 1686 public boolean matchesType() { 1687 return false; 1688 } 1689 }) 1690 .collect(toList()); 1691 --pastSpace; 1692 } 1693 anchor[0] += pastSpace; 1694 return result; 1695 } 1696 Matcher opp = OPTION_PRE_PATTERN.matcher(code); 1697 if (opp.matches()) { 1698 int pastSpace = opp.end(); 1699 List<Suggestion> result = provider.completionSuggestions( 1700 "", cursor - pastSpace, anchor); 1701 anchor[0] += pastSpace; 1702 return result; 1703 } 1704 return Collections.emptyList(); 1705 }; 1706 } 1707 1708 // /history command completion 1709 private static CompletionProvider historyCompletion() { 1710 return optionCompletion(HISTORY_OPTION_COMPLETION_PROVIDER); 1711 } 1712 1713 // /reload command completion 1714 private static CompletionProvider reloadCompletion() { 1715 return optionCompletion(RELOAD_OPTIONS_COMPLETION_PROVIDER); 1716 } 1717 1718 // /env command completion 1719 private static CompletionProvider envCompletion() { 1720 return optionCompletion(COMMAND_LINE_LIKE_OPTIONS_COMPLETION_PROVIDER); 1721 } 1722 1723 private static CompletionProvider orMostSpecificCompletion( 1724 CompletionProvider left, CompletionProvider right) { 1725 return (code, cursor, anchor) -> { 1726 int[] leftAnchor = {-1}; 1727 int[] rightAnchor = {-1}; 1728 1729 List<Suggestion> leftSuggestions = left.completionSuggestions(code, cursor, leftAnchor); 1730 List<Suggestion> rightSuggestions = right.completionSuggestions(code, cursor, rightAnchor); 1731 1732 List<Suggestion> suggestions = new ArrayList<>(); 1733 1734 if (leftAnchor[0] >= rightAnchor[0]) { 1735 anchor[0] = leftAnchor[0]; 1736 suggestions.addAll(leftSuggestions); 1737 } 1738 1739 if (leftAnchor[0] <= rightAnchor[0]) { 1740 anchor[0] = rightAnchor[0]; 1741 suggestions.addAll(rightSuggestions); 1742 } 1743 1744 return suggestions; 1745 }; 1746 } 1747 1748 // Snippet lists 1749 1750 Stream<Snippet> allSnippets() { 1751 return state.snippets(); 1752 } 1753 1754 Stream<Snippet> dropableSnippets() { 1755 return state.snippets() 1756 .filter(sn -> state.status(sn).isActive()); 1757 } 1758 1759 Stream<VarSnippet> allVarSnippets() { 1760 return state.snippets() 1761 .filter(sn -> sn.kind() == Snippet.Kind.VAR) 1762 .map(sn -> (VarSnippet) sn); 1763 } 1764 1765 Stream<MethodSnippet> allMethodSnippets() { 1766 return state.snippets() 1767 .filter(sn -> sn.kind() == Snippet.Kind.METHOD) 1768 .map(sn -> (MethodSnippet) sn); 1769 } 1770 1771 Stream<TypeDeclSnippet> allTypeSnippets() { 1772 return state.snippets() 1773 .filter(sn -> sn.kind() == Snippet.Kind.TYPE_DECL) 1774 .map(sn -> (TypeDeclSnippet) sn); 1775 } 1776 1777 // Table of commands -- with command forms, argument kinds, helpKey message, implementation, ... 1778 1779 { 1780 registerCommand(new Command("/list", 1781 this::cmdList, 1782 snippetWithOptionCompletion(SNIPPET_HISTORY_OPTION_COMPLETION_PROVIDER, 1783 this::allSnippets))); 1784 registerCommand(new Command("/edit", 1785 this::cmdEdit, 1786 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER, 1787 this::allSnippets))); 1788 registerCommand(new Command("/drop", 1789 this::cmdDrop, 1790 snippetCompletion(this::dropableSnippets), 1791 CommandKind.REPLAY)); 1792 registerCommand(new Command("/save", 1793 this::cmdSave, 1794 saveCompletion())); 1795 registerCommand(new Command("/open", 1796 this::cmdOpen, 1797 FILE_COMPLETION_PROVIDER)); 1798 registerCommand(new Command("/vars", 1799 this::cmdVars, 1800 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER, 1801 this::allVarSnippets))); 1802 registerCommand(new Command("/methods", 1803 this::cmdMethods, 1804 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER, 1805 this::allMethodSnippets))); 1806 registerCommand(new Command("/types", 1807 this::cmdTypes, 1808 snippetWithOptionCompletion(SNIPPET_OPTION_COMPLETION_PROVIDER, 1809 this::allTypeSnippets))); 1810 registerCommand(new Command("/imports", 1811 arg -> cmdImports(), 1812 EMPTY_COMPLETION_PROVIDER)); 1813 registerCommand(new Command("/exit", 1814 arg -> cmdExit(arg), 1815 (sn, c, a) -> { 1816 if (analysis == null || sn.isEmpty()) { 1817 // No completions if uninitialized or snippet not started 1818 return Collections.emptyList(); 1819 } else { 1820 // Give exit code an int context by prefixing the arg 1821 List<Suggestion> suggestions = analysis.completionSuggestions(INT_PREFIX + sn, 1822 INT_PREFIX.length() + c, a); 1823 a[0] -= INT_PREFIX.length(); 1824 return suggestions; 1825 } 1826 })); 1827 registerCommand(new Command("/env", 1828 arg -> cmdEnv(arg), 1829 envCompletion())); 1830 registerCommand(new Command("/reset", 1831 arg -> cmdReset(arg), 1832 envCompletion())); 1833 registerCommand(new Command("/reload", 1834 this::cmdReload, 1835 reloadCompletion())); 1836 registerCommand(new Command("/history", 1837 this::cmdHistory, 1838 historyCompletion())); 1839 registerCommand(new Command("/debug", 1840 this::cmdDebug, 1841 EMPTY_COMPLETION_PROVIDER, 1842 CommandKind.HIDDEN)); 1843 registerCommand(new Command("/help", 1844 this::cmdHelp, 1845 helpCompletion())); 1846 registerCommand(new Command("/set", 1847 this::cmdSet, 1848 new ContinuousCompletionProvider(Map.of( 1849 // need more completion for format for usability 1850 "format", feedback.modeCompletions(), 1851 "truncation", feedback.modeCompletions(), 1852 "feedback", feedback.modeCompletions(), 1853 "mode", skipWordThenCompletion(orMostSpecificCompletion( 1854 feedback.modeCompletions(SET_MODE_OPTIONS_COMPLETION_PROVIDER), 1855 SET_MODE_OPTIONS_COMPLETION_PROVIDER)), 1856 "prompt", feedback.modeCompletions(), 1857 "editor", fileCompletions(Files::isExecutable), 1858 "start", FILE_COMPLETION_PROVIDER, 1859 "indent", EMPTY_COMPLETION_PROVIDER), 1860 STARTSWITH_MATCHER))); 1861 registerCommand(new Command("/?", 1862 "help.quest", 1863 this::cmdHelp, 1864 helpCompletion(), 1865 CommandKind.NORMAL)); 1866 registerCommand(new Command("/!", 1867 "help.bang", 1868 arg -> cmdUseHistoryEntry(-1), 1869 EMPTY_COMPLETION_PROVIDER, 1870 CommandKind.NORMAL)); 1871 1872 // Documentation pseudo-commands 1873 registerCommand(new Command("/<id>", 1874 "help.slashID", 1875 arg -> cmdHelp("rerun"), 1876 EMPTY_COMPLETION_PROVIDER, 1877 CommandKind.HELP_ONLY)); 1878 registerCommand(new Command("/-<n>", 1879 "help.previous", 1880 arg -> cmdHelp("rerun"), 1881 EMPTY_COMPLETION_PROVIDER, 1882 CommandKind.HELP_ONLY)); 1883 registerCommand(new Command("intro", 1884 "help.intro", 1885 CommandKind.HELP_SUBJECT)); 1886 registerCommand(new Command("keys", 1887 "help.keys", 1888 CommandKind.HELP_SUBJECT)); 1889 registerCommand(new Command("id", 1890 "help.id", 1891 CommandKind.HELP_SUBJECT)); 1892 registerCommand(new Command("shortcuts", 1893 "help.shortcuts", 1894 CommandKind.HELP_SUBJECT)); 1895 registerCommand(new Command("context", 1896 "help.context", 1897 CommandKind.HELP_SUBJECT)); 1898 registerCommand(new Command("rerun", 1899 "help.rerun", 1900 CommandKind.HELP_SUBJECT)); 1901 1902 commandCompletions = new ContinuousCompletionProvider( 1903 commands.values().stream() 1904 .filter(c -> c.kind.shouldSuggestCompletions) 1905 .collect(toMap(c -> c.command, c -> c.completions)), 1906 STARTSWITH_MATCHER); 1907 } 1908 1909 private ContinuousCompletionProvider commandCompletions; 1910 1911 public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) { 1912 return commandCompletions.completionSuggestions(code, cursor, anchor); 1913 } 1914 1915 public List<String> commandDocumentation(String code, int cursor, boolean shortDescription) { 1916 code = code.substring(0, cursor).replaceAll("\\h+", " "); 1917 String stripped = code.replaceFirst("/(he(lp?)?|\\?) ", ""); 1918 boolean inHelp = !code.equals(stripped); 1919 int space = stripped.indexOf(' '); 1920 String prefix = space != (-1) ? stripped.substring(0, space) : stripped; 1921 List<String> result = new ArrayList<>(); 1922 1923 List<Entry<String, String>> toShow; 1924 1925 if (SET_SUB.matcher(stripped).matches()) { 1926 String setSubcommand = stripped.replaceFirst("/?set ([^ ]*)($| .*)", "$1"); 1927 toShow = 1928 Arrays.stream(SET_SUBCOMMANDS) 1929 .filter(s -> s.startsWith(setSubcommand)) 1930 .map(s -> new SimpleEntry<>("/set " + s, "help.set." + s)) 1931 .collect(toList()); 1932 } else if (RERUN_ID.matcher(stripped).matches()) { 1933 toShow = 1934 singletonList(new SimpleEntry<>("/<id>", "help.rerun")); 1935 } else if (RERUN_PREVIOUS.matcher(stripped).matches()) { 1936 toShow = 1937 singletonList(new SimpleEntry<>("/-<n>", "help.rerun")); 1938 } else { 1939 toShow = 1940 commands.values() 1941 .stream() 1942 .filter(c -> c.command.startsWith(prefix) 1943 || c.command.substring(1).startsWith(prefix)) 1944 .filter(c -> c.kind.showInHelp 1945 || (inHelp && c.kind == CommandKind.HELP_SUBJECT)) 1946 .sorted((c1, c2) -> c1.command.compareTo(c2.command)) 1947 .map(c -> new SimpleEntry<>(c.command, c.helpKey)) 1948 .collect(toList()); 1949 } 1950 1951 if (toShow.size() == 1 && !inHelp) { 1952 result.add(getResourceString(toShow.get(0).getValue() + (shortDescription ? ".summary" : ""))); 1953 } else { 1954 for (Entry<String, String> e : toShow) { 1955 result.add(e.getKey() + "\n" + getResourceString(e.getValue() + (shortDescription ? ".summary" : ""))); 1956 } 1957 } 1958 1959 return result; 1960 } 1961 1962 // Attempt to stop currently running evaluation 1963 void stop() { 1964 state.stop(); 1965 } 1966 1967 // --- Command implementations --- 1968 1969 private static final String[] SET_SUBCOMMANDS = new String[]{ 1970 "format", "truncation", "feedback", "mode", "prompt", "editor", "start", "indent"}; 1971 1972 final boolean cmdSet(String arg) { 1973 String cmd = "/set"; 1974 ArgTokenizer at = new ArgTokenizer(cmd, arg.trim()); 1975 String which = subCommand(cmd, at, SET_SUBCOMMANDS); 1976 if (which == null) { 1977 return false; 1978 } 1979 switch (which) { 1980 case "_retain": { 1981 errormsg("jshell.err.setting.to.retain.must.be.specified", at.whole()); 1982 return false; 1983 } 1984 case "_blank": { 1985 // show top-level settings 1986 new SetEditor().set(); 1987 showIndent(); 1988 showSetStart(); 1989 setFeedback(this, at); // no args so shows feedback setting 1990 hardmsg("jshell.msg.set.show.mode.settings"); 1991 return true; 1992 } 1993 case "format": 1994 return feedback.setFormat(this, at); 1995 case "truncation": 1996 return feedback.setTruncation(this, at); 1997 case "feedback": 1998 return setFeedback(this, at); 1999 case "mode": 2000 return feedback.setMode(this, at, 2001 retained -> prefs.put(MODE2_KEY, retained)); 2002 case "prompt": 2003 return feedback.setPrompt(this, at); 2004 case "editor": 2005 return new SetEditor(at).set(); 2006 case "start": 2007 return setStart(at); 2008 case "indent": 2009 String value = at.next(); 2010 if (value != null) { 2011 try { 2012 int indent = Integer.parseInt(value); 2013 String indentValue = Integer.toString(indent); 2014 prefs.put(INDENT_KEY, indentValue); 2015 input.setIndent(indent); 2016 fluffmsg("jshell.msg.set.indent.set", indentValue); 2017 } catch (NumberFormatException ex) { 2018 errormsg("jshell.err.invalid.indent", value); 2019 return false; 2020 } 2021 } else { 2022 showIndent(); 2023 } 2024 return true; 2025 default: 2026 errormsg("jshell.err.arg", cmd, at.val()); 2027 return false; 2028 } 2029 } 2030 2031 boolean setFeedback(MessageHandler messageHandler, ArgTokenizer at) { 2032 return feedback.setFeedback(messageHandler, at, 2033 fb -> prefs.put(FEEDBACK_KEY, fb)); 2034 } 2035 2036 // Find which, if any, sub-command matches. 2037 // Return null on error 2038 String subCommand(String cmd, ArgTokenizer at, String[] subs) { 2039 at.allowedOptions("-retain"); 2040 String sub = at.next(); 2041 if (sub == null) { 2042 // No sub-command was given 2043 return at.hasOption("-retain") 2044 ? "_retain" 2045 : "_blank"; 2046 } 2047 String[] matches = Arrays.stream(subs) 2048 .filter(s -> s.startsWith(sub)) 2049 .toArray(String[]::new); 2050 if (matches.length == 0) { 2051 // There are no matching sub-commands 2052 errormsg("jshell.err.arg", cmd, sub); 2053 fluffmsg("jshell.msg.use.one.of", Arrays.stream(subs) 2054 .collect(Collectors.joining(", ")) 2055 ); 2056 return null; 2057 } 2058 if (matches.length > 1) { 2059 // More than one sub-command matches the initial characters provided 2060 errormsg("jshell.err.sub.ambiguous", cmd, sub); 2061 fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches) 2062 .collect(Collectors.joining(", ")) 2063 ); 2064 return null; 2065 } 2066 return matches[0]; 2067 } 2068 2069 static class EditorSetting { 2070 2071 static String BUILT_IN_REP = "-default"; 2072 static char WAIT_PREFIX = '-'; 2073 static char NORMAL_PREFIX = '*'; 2074 2075 final String[] cmd; 2076 final boolean wait; 2077 2078 EditorSetting(String[] cmd, boolean wait) { 2079 this.wait = wait; 2080 this.cmd = cmd; 2081 } 2082 2083 // returns null if not stored in preferences 2084 static EditorSetting fromPrefs(PersistentStorage prefs) { 2085 // Read retained editor setting (if any) 2086 String editorString = prefs.get(EDITOR_KEY); 2087 if (editorString == null || editorString.isEmpty()) { 2088 return null; 2089 } else if (editorString.equals(BUILT_IN_REP)) { 2090 return BUILT_IN_EDITOR; 2091 } else { 2092 boolean wait = false; 2093 char waitMarker = editorString.charAt(0); 2094 if (waitMarker == WAIT_PREFIX || waitMarker == NORMAL_PREFIX) { 2095 wait = waitMarker == WAIT_PREFIX; 2096 editorString = editorString.substring(1); 2097 } 2098 String[] cmd = editorString.split(RECORD_SEPARATOR); 2099 return new EditorSetting(cmd, wait); 2100 } 2101 } 2102 2103 static void removePrefs(PersistentStorage prefs) { 2104 prefs.remove(EDITOR_KEY); 2105 } 2106 2107 void toPrefs(PersistentStorage prefs) { 2108 prefs.put(EDITOR_KEY, (this == BUILT_IN_EDITOR) 2109 ? BUILT_IN_REP 2110 : (wait ? WAIT_PREFIX : NORMAL_PREFIX) + String.join(RECORD_SEPARATOR, cmd)); 2111 } 2112 2113 @Override 2114 public boolean equals(Object o) { 2115 if (o instanceof EditorSetting) { 2116 EditorSetting ed = (EditorSetting) o; 2117 return Arrays.equals(cmd, ed.cmd) && wait == ed.wait; 2118 } else { 2119 return false; 2120 } 2121 } 2122 2123 @Override 2124 public int hashCode() { 2125 int hash = 7; 2126 hash = 71 * hash + Arrays.deepHashCode(this.cmd); 2127 hash = 71 * hash + (this.wait ? 1 : 0); 2128 return hash; 2129 } 2130 } 2131 2132 class SetEditor { 2133 2134 private final ArgTokenizer at; 2135 private final String[] command; 2136 private final boolean hasCommand; 2137 private final boolean defaultOption; 2138 private final boolean deleteOption; 2139 private final boolean waitOption; 2140 private final boolean retainOption; 2141 private final int primaryOptionCount; 2142 2143 SetEditor(ArgTokenizer at) { 2144 at.allowedOptions("-default", "-wait", "-retain", "-delete"); 2145 String prog = at.next(); 2146 List<String> ed = new ArrayList<>(); 2147 while (at.val() != null) { 2148 ed.add(at.val()); 2149 at.nextToken(); // so that options are not interpreted as jshell options 2150 } 2151 this.at = at; 2152 this.command = ed.toArray(new String[ed.size()]); 2153 this.hasCommand = command.length > 0; 2154 this.defaultOption = at.hasOption("-default"); 2155 this.deleteOption = at.hasOption("-delete"); 2156 this.waitOption = at.hasOption("-wait"); 2157 this.retainOption = at.hasOption("-retain"); 2158 this.primaryOptionCount = (hasCommand? 1 : 0) + (defaultOption? 1 : 0) + (deleteOption? 1 : 0); 2159 } 2160 2161 SetEditor() { 2162 this(new ArgTokenizer("", "")); 2163 } 2164 2165 boolean set() { 2166 if (!check()) { 2167 return false; 2168 } 2169 if (primaryOptionCount == 0 && !retainOption) { 2170 // No settings or -retain, so this is a query 2171 EditorSetting retained = EditorSetting.fromPrefs(prefs); 2172 if (retained != null) { 2173 // retained editor is set 2174 hard("/set editor -retain %s", format(retained)); 2175 } 2176 if (retained == null || !retained.equals(editor)) { 2177 // editor is not retained or retained is different from set 2178 hard("/set editor %s", format(editor)); 2179 } 2180 return true; 2181 } 2182 if (retainOption && deleteOption) { 2183 EditorSetting.removePrefs(prefs); 2184 } 2185 install(); 2186 if (retainOption && !deleteOption) { 2187 editor.toPrefs(prefs); 2188 fluffmsg("jshell.msg.set.editor.retain", format(editor)); 2189 } 2190 return true; 2191 } 2192 2193 private boolean check() { 2194 if (!checkOptionsAndRemainingInput(at)) { 2195 return false; 2196 } 2197 if (primaryOptionCount > 1) { 2198 errormsg("jshell.err.default.option.or.program", at.whole()); 2199 return false; 2200 } 2201 if (waitOption && !hasCommand) { 2202 errormsg("jshell.err.wait.applies.to.external.editor", at.whole()); 2203 return false; 2204 } 2205 return true; 2206 } 2207 2208 private void install() { 2209 if (hasCommand) { 2210 editor = new EditorSetting(command, waitOption); 2211 } else if (defaultOption) { 2212 editor = BUILT_IN_EDITOR; 2213 } else if (deleteOption) { 2214 configEditor(); 2215 } else { 2216 return; 2217 } 2218 fluffmsg("jshell.msg.set.editor.set", format(editor)); 2219 } 2220 2221 private String format(EditorSetting ed) { 2222 if (ed == BUILT_IN_EDITOR) { 2223 return "-default"; 2224 } else { 2225 Stream<String> elems = Arrays.stream(ed.cmd); 2226 if (ed.wait) { 2227 elems = Stream.concat(Stream.of("-wait"), elems); 2228 } 2229 return elems.collect(joining(" ")); 2230 } 2231 } 2232 } 2233 2234 // The sub-command: /set start <start-file> 2235 boolean setStart(ArgTokenizer at) { 2236 at.allowedOptions("-default", "-none", "-retain"); 2237 List<String> fns = new ArrayList<>(); 2238 while (at.next() != null) { 2239 fns.add(at.val()); 2240 } 2241 if (!checkOptionsAndRemainingInput(at)) { 2242 return false; 2243 } 2244 boolean defaultOption = at.hasOption("-default"); 2245 boolean noneOption = at.hasOption("-none"); 2246 boolean retainOption = at.hasOption("-retain"); 2247 boolean hasFile = !fns.isEmpty(); 2248 2249 int argCount = (defaultOption ? 1 : 0) + (noneOption ? 1 : 0) + (hasFile ? 1 : 0); 2250 if (argCount > 1) { 2251 errormsg("jshell.err.option.or.filename", at.whole()); 2252 return false; 2253 } 2254 if (argCount == 0 && !retainOption) { 2255 // no options or filename, show current setting 2256 showSetStart(); 2257 return true; 2258 } 2259 if (hasFile) { 2260 startup = Startup.fromFileList(fns, "/set start", this); 2261 if (startup == null) { 2262 return false; 2263 } 2264 } else if (defaultOption) { 2265 startup = Startup.defaultStartup(this); 2266 } else if (noneOption) { 2267 startup = Startup.noStartup(); 2268 } 2269 if (retainOption) { 2270 // retain startup setting 2271 prefs.put(STARTUP_KEY, startup.storedForm()); 2272 } 2273 return true; 2274 } 2275 2276 // show the "/set start" settings (retained and, if different, current) 2277 // as commands (and file contents). All commands first, then contents. 2278 void showSetStart() { 2279 StringBuilder sb = new StringBuilder(); 2280 String retained = prefs.get(STARTUP_KEY); 2281 if (retained != null) { 2282 Startup retainedStart = Startup.unpack(retained, this); 2283 boolean currentDifferent = !startup.equals(retainedStart); 2284 sb.append(retainedStart.show(true)); 2285 if (currentDifferent) { 2286 sb.append(startup.show(false)); 2287 } 2288 sb.append(retainedStart.showDetail()); 2289 if (currentDifferent) { 2290 sb.append(startup.showDetail()); 2291 } 2292 } else { 2293 sb.append(startup.show(false)); 2294 sb.append(startup.showDetail()); 2295 } 2296 hard(sb.toString()); 2297 } 2298 2299 private void showIndent() { 2300 hard("/set indent %s", indent()); 2301 } 2302 2303 boolean cmdDebug(String arg) { 2304 if (arg.isEmpty()) { 2305 debug = !debug; 2306 InternalDebugControl.setDebugFlags(state, debug ? DBG_GEN : 0); 2307 fluff("Debugging %s", debug ? "on" : "off"); 2308 } else { 2309 for (char ch : arg.toCharArray()) { 2310 switch (ch) { 2311 case '0': 2312 debugFlags = 0; 2313 debug = false; 2314 fluff("Debugging off"); 2315 break; 2316 case 'r': 2317 debug = true; 2318 fluff("REPL tool debugging on"); 2319 break; 2320 case 'g': 2321 debugFlags |= DBG_GEN; 2322 fluff("General debugging on"); 2323 break; 2324 case 'f': 2325 debugFlags |= DBG_FMGR; 2326 fluff("File manager debugging on"); 2327 break; 2328 case 'c': 2329 debugFlags |= DBG_COMPA; 2330 fluff("Completion analysis debugging on"); 2331 break; 2332 case 'd': 2333 debugFlags |= DBG_DEP; 2334 fluff("Dependency debugging on"); 2335 break; 2336 case 'e': 2337 debugFlags |= DBG_EVNT; 2338 fluff("Event debugging on"); 2339 break; 2340 case 'w': 2341 debugFlags |= DBG_WRAP; 2342 fluff("Wrap debugging on"); 2343 break; 2344 case 'b': 2345 cmdout.printf("RemoteVM Options: %s\nCompiler options: %s\n", 2346 Arrays.toString(options.remoteVmOptions()), 2347 Arrays.toString(options.compilerOptions())); 2348 break; 2349 default: 2350 error("Unknown debugging option: %c", ch); 2351 fluff("Use: 0 r g f c d e w b"); 2352 return false; 2353 } 2354 } 2355 InternalDebugControl.setDebugFlags(state, debugFlags); 2356 } 2357 return true; 2358 } 2359 2360 private boolean cmdExit(String arg) { 2361 if (!arg.trim().isEmpty()) { 2362 debug("Compiling exit: %s", arg); 2363 List<SnippetEvent> events = state.eval(arg); 2364 for (SnippetEvent e : events) { 2365 // Only care about main snippet 2366 if (e.causeSnippet() == null) { 2367 Snippet sn = e.snippet(); 2368 2369 // Show any diagnostics 2370 List<Diag> diagnostics = state.diagnostics(sn).collect(toList()); 2371 String source = sn.source(); 2372 displayDiagnostics(source, diagnostics); 2373 2374 // Show any exceptions 2375 if (e.exception() != null && e.status() != Status.REJECTED) { 2376 if (displayException(e.exception())) { 2377 // Abort: an exception occurred (reported) 2378 return false; 2379 } 2380 } 2381 2382 if (e.status() != Status.VALID) { 2383 // Abort: can only use valid snippets, diagnostics have been reported (above) 2384 return false; 2385 } 2386 String typeName; 2387 if (sn.kind() == Kind.EXPRESSION) { 2388 typeName = ((ExpressionSnippet) sn).typeName(); 2389 } else if (sn.subKind() == TEMP_VAR_EXPRESSION_SUBKIND) { 2390 typeName = ((VarSnippet) sn).typeName(); 2391 } else { 2392 // Abort: not an expression 2393 errormsg("jshell.err.exit.not.expression", arg); 2394 return false; 2395 } 2396 switch (typeName) { 2397 case "int": 2398 case "Integer": 2399 case "byte": 2400 case "Byte": 2401 case "short": 2402 case "Short": 2403 try { 2404 int i = Integer.parseInt(e.value()); 2405 /** 2406 addToReplayHistory("/exit " + arg); 2407 replayableHistory.storeHistory(prefs); 2408 closeState(); 2409 try { 2410 input.close(); 2411 } catch (Exception exc) { 2412 // ignore 2413 } 2414 * **/ 2415 exitCode = i; 2416 break; 2417 } catch (NumberFormatException exc) { 2418 // Abort: bad value 2419 errormsg("jshell.err.exit.bad.value", arg, e.value()); 2420 return false; 2421 } 2422 default: 2423 // Abort: bad type 2424 errormsg("jshell.err.exit.bad.type", arg, typeName); 2425 return false; 2426 } 2427 } 2428 } 2429 } 2430 regenerateOnDeath = false; 2431 live = false; 2432 if (exitCode == 0) { 2433 fluffmsg("jshell.msg.goodbye"); 2434 } else { 2435 fluffmsg("jshell.msg.goodbye.value", exitCode); 2436 } 2437 return true; 2438 } 2439 2440 boolean cmdHelp(String arg) { 2441 ArgTokenizer at = new ArgTokenizer("/help", arg); 2442 String subject = at.next(); 2443 if (subject != null) { 2444 // check if the requested subject is a help subject or 2445 // a command, with or without slash 2446 Command[] matches = commands.values().stream() 2447 .filter(c -> c.command.startsWith(subject) 2448 || c.command.substring(1).startsWith(subject)) 2449 .toArray(Command[]::new); 2450 if (matches.length == 1) { 2451 String cmd = matches[0].command; 2452 if (cmd.equals("/set")) { 2453 // Print the help doc for the specified sub-command 2454 String which = subCommand(cmd, at, SET_SUBCOMMANDS); 2455 if (which == null) { 2456 return false; 2457 } 2458 if (!which.equals("_blank")) { 2459 printHelp("/set " + which, "help.set." + which); 2460 return true; 2461 } 2462 } 2463 } 2464 if (matches.length > 0) { 2465 for (Command c : matches) { 2466 printHelp(c.command, c.helpKey); 2467 } 2468 return true; 2469 } else { 2470 // failing everything else, check if this is the start of 2471 // a /set sub-command name 2472 String[] subs = Arrays.stream(SET_SUBCOMMANDS) 2473 .filter(s -> s.startsWith(subject)) 2474 .toArray(String[]::new); 2475 if (subs.length > 0) { 2476 for (String sub : subs) { 2477 printHelp("/set " + sub, "help.set." + sub); 2478 } 2479 return true; 2480 } 2481 errormsg("jshell.err.help.arg", arg); 2482 } 2483 } 2484 hardmsg("jshell.msg.help.begin"); 2485 hardPairs(commands.values().stream() 2486 .filter(cmd -> cmd.kind.showInHelp), 2487 cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"), 2488 cmd -> getResourceString(cmd.helpKey + ".summary") 2489 ); 2490 hardmsg("jshell.msg.help.subject"); 2491 hardPairs(commands.values().stream() 2492 .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT), 2493 cmd -> cmd.command, 2494 cmd -> getResourceString(cmd.helpKey + ".summary") 2495 ); 2496 return true; 2497 } 2498 2499 private void printHelp(String name, String key) { 2500 int len = name.length(); 2501 String centered = "%" + ((OUTPUT_WIDTH + len) / 2) + "s"; 2502 hard(""); 2503 hard(centered, name); 2504 hard(centered, Stream.generate(() -> "=").limit(len).collect(Collectors.joining())); 2505 hard(""); 2506 hardrb(key); 2507 } 2508 2509 private boolean cmdHistory(String rawArgs) { 2510 ArgTokenizer at = new ArgTokenizer("/history", rawArgs.trim()); 2511 at.allowedOptions("-all"); 2512 if (!checkOptionsAndRemainingInput(at)) { 2513 return false; 2514 } 2515 cmdout.println(); 2516 for (String s : input.history(!at.hasOption("-all"))) { 2517 // No number prefix, confusing with snippet ids 2518 cmdout.printf("%s\n", s); 2519 } 2520 return true; 2521 } 2522 2523 /** 2524 * Avoid parameterized varargs possible heap pollution warning. 2525 */ 2526 private interface SnippetPredicate<T extends Snippet> extends Predicate<T> { } 2527 2528 /** 2529 * Apply filters to a stream until one that is non-empty is found. 2530 * Adapted from Stuart Marks 2531 * 2532 * @param supplier Supply the Snippet stream to filter 2533 * @param filters Filters to attempt 2534 * @return The non-empty filtered Stream, or null 2535 */ 2536 @SafeVarargs 2537 private static <T extends Snippet> Stream<T> nonEmptyStream(Supplier<Stream<T>> supplier, 2538 SnippetPredicate<T>... filters) { 2539 for (SnippetPredicate<T> filt : filters) { 2540 Iterator<T> iterator = supplier.get().filter(filt).iterator(); 2541 if (iterator.hasNext()) { 2542 return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false); 2543 } 2544 } 2545 return null; 2546 } 2547 2548 private boolean inStartUp(Snippet sn) { 2549 return mapSnippet.get(sn).space == startNamespace; 2550 } 2551 2552 private boolean isActive(Snippet sn) { 2553 return state.status(sn).isActive(); 2554 } 2555 2556 private boolean mainActive(Snippet sn) { 2557 return !inStartUp(sn) && isActive(sn); 2558 } 2559 2560 private boolean matchingDeclaration(Snippet sn, String name) { 2561 return sn instanceof DeclarationSnippet 2562 && ((DeclarationSnippet) sn).name().equals(name); 2563 } 2564 2565 /** 2566 * Convert user arguments to a Stream of snippets referenced by those 2567 * arguments (or lack of arguments). 2568 * 2569 * @param snippets the base list of possible snippets 2570 * @param defFilter the filter to apply to the arguments if no argument 2571 * @param rawargs the user's argument to the command, maybe be the empty 2572 * string 2573 * @return a Stream of referenced snippets or null if no matches are found 2574 */ 2575 private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier, 2576 Predicate<Snippet> defFilter, String rawargs, String cmd) { 2577 ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim()); 2578 at.allowedOptions("-all", "-start"); 2579 return argsOptionsToSnippets(snippetSupplier, defFilter, at); 2580 } 2581 2582 /** 2583 * Convert user arguments to a Stream of snippets referenced by those 2584 * arguments (or lack of arguments). 2585 * 2586 * @param snippets the base list of possible snippets 2587 * @param defFilter the filter to apply to the arguments if no argument 2588 * @param at the ArgTokenizer, with allowed options set 2589 * @return 2590 */ 2591 private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier, 2592 Predicate<Snippet> defFilter, ArgTokenizer at) { 2593 List<String> args = new ArrayList<>(); 2594 String s; 2595 while ((s = at.next()) != null) { 2596 args.add(s); 2597 } 2598 if (!checkOptionsAndRemainingInput(at)) { 2599 return null; 2600 } 2601 if (at.optionCount() > 0 && args.size() > 0) { 2602 errormsg("jshell.err.may.not.specify.options.and.snippets", at.whole()); 2603 return null; 2604 } 2605 if (at.optionCount() > 1) { 2606 errormsg("jshell.err.conflicting.options", at.whole()); 2607 return null; 2608 } 2609 if (at.isAllowedOption("-all") && at.hasOption("-all")) { 2610 // all snippets including start-up, failed, and overwritten 2611 return snippetSupplier.get(); 2612 } 2613 if (at.isAllowedOption("-start") && at.hasOption("-start")) { 2614 // start-up snippets 2615 return snippetSupplier.get() 2616 .filter(this::inStartUp); 2617 } 2618 if (args.isEmpty()) { 2619 // Default is all active user snippets 2620 return snippetSupplier.get() 2621 .filter(defFilter); 2622 } 2623 return new ArgToSnippets<>(snippetSupplier).argsToSnippets(args); 2624 } 2625 2626 /** 2627 * Support for converting arguments that are definition names, snippet ids, 2628 * or snippet id ranges into a stream of snippets, 2629 * 2630 * @param <T> the snipper subtype 2631 */ 2632 private class ArgToSnippets<T extends Snippet> { 2633 2634 // the supplier of snippet streams 2635 final Supplier<Stream<T>> snippetSupplier; 2636 // these two are parallel, and lazily filled if a range is encountered 2637 List<T> allSnippets; 2638 String[] allIds = null; 2639 2640 /** 2641 * 2642 * @param snippetSupplier the base list of possible snippets 2643 */ 2644 ArgToSnippets(Supplier<Stream<T>> snippetSupplier) { 2645 this.snippetSupplier = snippetSupplier; 2646 } 2647 2648 /** 2649 * Convert user arguments to a Stream of snippets referenced by those 2650 * arguments. 2651 * 2652 * @param args the user's argument to the command, maybe be the empty 2653 * list 2654 * @return a Stream of referenced snippets or null if no matches to 2655 * specific arg 2656 */ 2657 Stream<T> argsToSnippets(List<String> args) { 2658 Stream<T> result = null; 2659 for (String arg : args) { 2660 // Find the best match 2661 Stream<T> st = argToSnippets(arg); 2662 if (st == null) { 2663 return null; 2664 } else { 2665 result = (result == null) 2666 ? st 2667 : Stream.concat(result, st); 2668 } 2669 } 2670 return result; 2671 } 2672 2673 /** 2674 * Convert a user argument to a Stream of snippets referenced by the 2675 * argument. 2676 * 2677 * @param snippetSupplier the base list of possible snippets 2678 * @param arg the user's argument to the command 2679 * @return a Stream of referenced snippets or null if no matches to 2680 * specific arg 2681 */ 2682 Stream<T> argToSnippets(String arg) { 2683 if (arg.contains("-")) { 2684 return range(arg); 2685 } 2686 // Find the best match 2687 Stream<T> st = layeredSnippetSearch(snippetSupplier, arg); 2688 if (st == null) { 2689 badSnippetErrormsg(arg); 2690 return null; 2691 } else { 2692 return st; 2693 } 2694 } 2695 2696 /** 2697 * Look for inappropriate snippets to give best error message 2698 * 2699 * @param arg the bad snippet arg 2700 * @param errKey the not found error key 2701 */ 2702 void badSnippetErrormsg(String arg) { 2703 Stream<Snippet> est = layeredSnippetSearch(state::snippets, arg); 2704 if (est == null) { 2705 if (ID.matcher(arg).matches()) { 2706 errormsg("jshell.err.no.snippet.with.id", arg); 2707 } else { 2708 errormsg("jshell.err.no.such.snippets", arg); 2709 } 2710 } else { 2711 errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command", 2712 arg, est.findFirst().get().source()); 2713 } 2714 } 2715 2716 /** 2717 * Search through the snippets for the best match to the id/name. 2718 * 2719 * @param <R> the snippet type 2720 * @param aSnippetSupplier the supplier of snippet streams 2721 * @param arg the arg to match 2722 * @return a Stream of referenced snippets or null if no matches to 2723 * specific arg 2724 */ 2725 <R extends Snippet> Stream<R> layeredSnippetSearch(Supplier<Stream<R>> aSnippetSupplier, String arg) { 2726 return nonEmptyStream( 2727 // the stream supplier 2728 aSnippetSupplier, 2729 // look for active user declarations matching the name 2730 sn -> isActive(sn) && matchingDeclaration(sn, arg), 2731 // else, look for any declarations matching the name 2732 sn -> matchingDeclaration(sn, arg), 2733 // else, look for an id of this name 2734 sn -> sn.id().equals(arg) 2735 ); 2736 } 2737 2738 /** 2739 * Given an id1-id2 range specifier, return a stream of snippets within 2740 * our context 2741 * 2742 * @param arg the range arg 2743 * @return a Stream of referenced snippets or null if no matches to 2744 * specific arg 2745 */ 2746 Stream<T> range(String arg) { 2747 int dash = arg.indexOf('-'); 2748 String iid = arg.substring(0, dash); 2749 String tid = arg.substring(dash + 1); 2750 int iidx = snippetIndex(iid); 2751 if (iidx < 0) { 2752 return null; 2753 } 2754 int tidx = snippetIndex(tid); 2755 if (tidx < 0) { 2756 return null; 2757 } 2758 if (tidx < iidx) { 2759 errormsg("jshell.err.end.snippet.range.less.than.start", iid, tid); 2760 return null; 2761 } 2762 return allSnippets.subList(iidx, tidx+1).stream(); 2763 } 2764 2765 /** 2766 * Lazily initialize the id mapping -- needed only for id ranges. 2767 */ 2768 void initIdMapping() { 2769 if (allIds == null) { 2770 allSnippets = snippetSupplier.get() 2771 .sorted((a, b) -> order(a) - order(b)) 2772 .collect(toList()); 2773 allIds = allSnippets.stream() 2774 .map(sn -> sn.id()) 2775 .toArray(n -> new String[n]); 2776 } 2777 } 2778 2779 /** 2780 * Return all the snippet ids -- within the context, and in order. 2781 * 2782 * @return the snippet ids 2783 */ 2784 String[] allIds() { 2785 initIdMapping(); 2786 return allIds; 2787 } 2788 2789 /** 2790 * Establish an order on snippet ids. All startup snippets are first, 2791 * all error snippets are last -- within that is by snippet number. 2792 * 2793 * @param id the id string 2794 * @return an ordering int 2795 */ 2796 int order(String id) { 2797 try { 2798 switch (id.charAt(0)) { 2799 case 's': 2800 return Integer.parseInt(id.substring(1)); 2801 case 'e': 2802 return 0x40000000 + Integer.parseInt(id.substring(1)); 2803 default: 2804 return 0x20000000 + Integer.parseInt(id); 2805 } 2806 } catch (Exception ex) { 2807 return 0x60000000; 2808 } 2809 } 2810 2811 /** 2812 * Establish an order on snippets, based on its snippet id. All startup 2813 * snippets are first, all error snippets are last -- within that is by 2814 * snippet number. 2815 * 2816 * @param sn the id string 2817 * @return an ordering int 2818 */ 2819 int order(Snippet sn) { 2820 return order(sn.id()); 2821 } 2822 2823 /** 2824 * Find the index into the parallel allSnippets and allIds structures. 2825 * 2826 * @param s the snippet id name 2827 * @return the index, or, if not found, report the error and return a 2828 * negative number 2829 */ 2830 int snippetIndex(String s) { 2831 int idx = Arrays.binarySearch(allIds(), 0, allIds().length, s, 2832 (a, b) -> order(a) - order(b)); 2833 if (idx < 0) { 2834 // the id is not in the snippet domain, find the right error to report 2835 if (!ID.matcher(s).matches()) { 2836 errormsg("jshell.err.range.requires.id", s); 2837 } else { 2838 badSnippetErrormsg(s); 2839 } 2840 } 2841 return idx; 2842 } 2843 2844 } 2845 2846 private boolean cmdDrop(String rawargs) { 2847 ArgTokenizer at = new ArgTokenizer("/drop", rawargs.trim()); 2848 at.allowedOptions(); 2849 List<String> args = new ArrayList<>(); 2850 String s; 2851 while ((s = at.next()) != null) { 2852 args.add(s); 2853 } 2854 if (!checkOptionsAndRemainingInput(at)) { 2855 return false; 2856 } 2857 if (args.isEmpty()) { 2858 errormsg("jshell.err.drop.arg"); 2859 return false; 2860 } 2861 Stream<Snippet> stream = new ArgToSnippets<>(this::dropableSnippets).argsToSnippets(args); 2862 if (stream == null) { 2863 // Snippet not found. Error already printed 2864 fluffmsg("jshell.msg.see.classes.etc"); 2865 return false; 2866 } 2867 stream.forEach(sn -> state.drop(sn).forEach(this::handleEvent)); 2868 return true; 2869 } 2870 2871 private boolean cmdEdit(String arg) { 2872 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, 2873 this::mainActive, arg, "/edit"); 2874 if (stream == null) { 2875 return false; 2876 } 2877 Set<String> srcSet = new LinkedHashSet<>(); 2878 stream.forEachOrdered(sn -> { 2879 String src = sn.source(); 2880 switch (sn.subKind()) { 2881 case VAR_VALUE_SUBKIND: 2882 break; 2883 case ASSIGNMENT_SUBKIND: 2884 case OTHER_EXPRESSION_SUBKIND: 2885 case TEMP_VAR_EXPRESSION_SUBKIND: 2886 case UNKNOWN_SUBKIND: 2887 if (!src.endsWith(";")) { 2888 src = src + ";"; 2889 } 2890 srcSet.add(src); 2891 break; 2892 case STATEMENT_SUBKIND: 2893 if (src.endsWith("}")) { 2894 // Could end with block or, for example, new Foo() {...} 2895 // so, we need deeper analysis to know if it needs a semicolon 2896 src = analysis.analyzeCompletion(src).source(); 2897 } else if (!src.endsWith(";")) { 2898 src = src + ";"; 2899 } 2900 srcSet.add(src); 2901 break; 2902 default: 2903 srcSet.add(src); 2904 break; 2905 } 2906 }); 2907 StringBuilder sb = new StringBuilder(); 2908 for (String s : srcSet) { 2909 sb.append(s); 2910 sb.append('\n'); 2911 } 2912 String src = sb.toString(); 2913 Consumer<String> saveHandler = new SaveHandler(src, srcSet); 2914 Consumer<String> errorHandler = s -> hard("Edit Error: %s", s); 2915 if (editor == BUILT_IN_EDITOR) { 2916 return builtInEdit(src, saveHandler, errorHandler); 2917 } else { 2918 // Changes have occurred in temp edit directory, 2919 // transfer the new sources to JShell (unless the editor is 2920 // running directly in JShell's window -- don't make a mess) 2921 String[] buffer = new String[1]; 2922 Consumer<String> extSaveHandler = s -> { 2923 if (input.terminalEditorRunning()) { 2924 buffer[0] = s; 2925 } else { 2926 saveHandler.accept(s); 2927 } 2928 }; 2929 ExternalEditor.edit(editor.cmd, src, 2930 errorHandler, extSaveHandler, 2931 () -> input.suspend(), 2932 () -> input.resume(), 2933 editor.wait, 2934 () -> hardrb("jshell.msg.press.return.to.leave.edit.mode")); 2935 if (buffer[0] != null) { 2936 saveHandler.accept(buffer[0]); 2937 } 2938 } 2939 return true; 2940 } 2941 //where 2942 // start the built-in editor 2943 private boolean builtInEdit(String initialText, 2944 Consumer<String> saveHandler, Consumer<String> errorHandler) { 2945 try { 2946 ServiceLoader<BuildInEditorProvider> sl 2947 = ServiceLoader.load(BuildInEditorProvider.class); 2948 // Find the highest ranking provider 2949 BuildInEditorProvider provider = null; 2950 for (BuildInEditorProvider p : sl) { 2951 if (provider == null || p.rank() > provider.rank()) { 2952 provider = p; 2953 } 2954 } 2955 if (provider != null) { 2956 provider.edit(getResourceString("jshell.label.editpad"), 2957 initialText, saveHandler, errorHandler); 2958 return true; 2959 } else { 2960 errormsg("jshell.err.no.builtin.editor"); 2961 } 2962 } catch (RuntimeException ex) { 2963 errormsg("jshell.err.cant.launch.editor", ex); 2964 } 2965 fluffmsg("jshell.msg.try.set.editor"); 2966 return false; 2967 } 2968 //where 2969 // receives editor requests to save 2970 private class SaveHandler implements Consumer<String> { 2971 2972 String src; 2973 Set<String> currSrcs; 2974 2975 SaveHandler(String src, Set<String> ss) { 2976 this.src = src; 2977 this.currSrcs = ss; 2978 } 2979 2980 @Override 2981 public void accept(String s) { 2982 if (!s.equals(src)) { // quick check first 2983 src = s; 2984 try { 2985 Set<String> nextSrcs = new LinkedHashSet<>(); 2986 boolean failed = false; 2987 while (true) { 2988 CompletionInfo an = analysis.analyzeCompletion(s); 2989 if (!an.completeness().isComplete()) { 2990 break; 2991 } 2992 String tsrc = trimNewlines(an.source()); 2993 if (!failed && !currSrcs.contains(tsrc)) { 2994 failed = !processSource(tsrc); 2995 } 2996 nextSrcs.add(tsrc); 2997 if (an.remaining().isEmpty()) { 2998 break; 2999 } 3000 s = an.remaining(); 3001 } 3002 currSrcs = nextSrcs; 3003 } catch (IllegalStateException ex) { 3004 errormsg("jshell.msg.resetting"); 3005 resetState(); 3006 currSrcs = new LinkedHashSet<>(); // re-process everything 3007 } 3008 } 3009 } 3010 3011 private String trimNewlines(String s) { 3012 int b = 0; 3013 while (b < s.length() && s.charAt(b) == '\n') { 3014 ++b; 3015 } 3016 int e = s.length() -1; 3017 while (e >= 0 && s.charAt(e) == '\n') { 3018 --e; 3019 } 3020 return s.substring(b, e + 1); 3021 } 3022 } 3023 3024 private boolean cmdList(String arg) { 3025 if (arg.length() >= 2 && "-history".startsWith(arg)) { 3026 return cmdHistory(""); 3027 } 3028 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, 3029 this::mainActive, arg, "/list"); 3030 if (stream == null) { 3031 return false; 3032 } 3033 3034 // prevent double newline on empty list 3035 boolean[] hasOutput = new boolean[1]; 3036 stream.forEachOrdered(sn -> { 3037 if (!hasOutput[0]) { 3038 cmdout.println(); 3039 hasOutput[0] = true; 3040 } 3041 cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n ")); 3042 }); 3043 return true; 3044 } 3045 3046 private boolean cmdOpen(String filename) { 3047 return runFile(filename, "/open"); 3048 } 3049 3050 private boolean runFile(String filename, String context) { 3051 if (!filename.isEmpty()) { 3052 try { 3053 Scanner scanner; 3054 if (!interactiveModeBegun && filename.equals("-")) { 3055 // - on command line: no interactive later, read from input 3056 regenerateOnDeath = false; 3057 scanner = new Scanner(cmdin); 3058 } else { 3059 Path path = null; 3060 URL url = null; 3061 String resource; 3062 try { 3063 path = toPathResolvingUserHome(filename); 3064 } catch (InvalidPathException ipe) { 3065 try { 3066 url = new URL(filename); 3067 if (url.getProtocol().equalsIgnoreCase("file")) { 3068 path = Paths.get(url.toURI()); 3069 } 3070 } catch (MalformedURLException | URISyntaxException e) { 3071 throw new FileNotFoundException(filename); 3072 } 3073 } 3074 if (path != null && Files.exists(path)) { 3075 scanner = new Scanner(new FileReader(path.toString())); 3076 } else if ((resource = getResource(filename)) != null) { 3077 scanner = new Scanner(new StringReader(resource)); 3078 } else { 3079 if (url == null) { 3080 try { 3081 url = new URL(filename); 3082 } catch (MalformedURLException mue) { 3083 throw new FileNotFoundException(filename); 3084 } 3085 } 3086 scanner = new Scanner(url.openStream()); 3087 } 3088 } 3089 try (var scannerIOContext = new ScannerIOContext(scanner)) { 3090 run(scannerIOContext); 3091 } 3092 return true; 3093 } catch (FileNotFoundException e) { 3094 errormsg("jshell.err.file.not.found", context, filename, e.getMessage()); 3095 } catch (Exception e) { 3096 errormsg("jshell.err.file.exception", context, filename, e); 3097 } 3098 } else { 3099 errormsg("jshell.err.file.filename", context); 3100 } 3101 return false; 3102 } 3103 3104 static String getResource(String name) { 3105 if (BUILTIN_FILE_PATTERN.matcher(name).matches()) { 3106 try { 3107 return readResource(name); 3108 } catch (Throwable t) { 3109 // Fall-through to null 3110 } 3111 } 3112 return null; 3113 } 3114 3115 // Read a built-in file from resources or compute it 3116 static String readResource(String name) throws Exception { 3117 // Class to compute imports by following requires for a module 3118 class ComputeImports { 3119 final String base; 3120 ModuleFinder finder = ModuleFinder.ofSystem(); 3121 3122 ComputeImports(String base) { 3123 this.base = base; 3124 } 3125 3126 Set<ModuleDescriptor> modules() { 3127 Set<ModuleDescriptor> closure = new HashSet<>(); 3128 moduleClosure(finder.find(base), closure); 3129 return closure; 3130 } 3131 3132 void moduleClosure(Optional<ModuleReference> omr, Set<ModuleDescriptor> closure) { 3133 if (omr.isPresent()) { 3134 ModuleDescriptor mdesc = omr.get().descriptor(); 3135 if (closure.add(mdesc)) { 3136 for (ModuleDescriptor.Requires req : mdesc.requires()) { 3137 if (!req.modifiers().contains(ModuleDescriptor.Requires.Modifier.STATIC)) { 3138 moduleClosure(finder.find(req.name()), closure); 3139 } 3140 } 3141 } 3142 } 3143 } 3144 3145 Set<String> packages() { 3146 return modules().stream().flatMap(md -> md.exports().stream()) 3147 .filter(e -> !e.isQualified()).map(Object::toString).collect(Collectors.toSet()); 3148 } 3149 3150 String imports() { 3151 Set<String> si = packages(); 3152 String[] ai = si.toArray(new String[si.size()]); 3153 Arrays.sort(ai); 3154 return Arrays.stream(ai) 3155 .map(p -> String.format("import %s.*;\n", p)) 3156 .collect(Collectors.joining()); 3157 } 3158 } 3159 3160 if (name.equals("JAVASE")) { 3161 // The built-in JAVASE is computed as the imports of all the packages in Java SE 3162 return new ComputeImports("java.se").imports(); 3163 } 3164 3165 // Attempt to find the file as a resource 3166 String spec = String.format(BUILTIN_FILE_PATH_FORMAT, name); 3167 3168 try (InputStream in = JShellTool.class.getResourceAsStream(spec); 3169 BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { 3170 return reader.lines().collect(Collectors.joining("\n", "", "\n")); 3171 } 3172 } 3173 3174 private boolean cmdReset(String rawargs) { 3175 Options oldOptions = rawargs.trim().isEmpty()? null : options; 3176 if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) { 3177 return false; 3178 } 3179 live = false; 3180 fluffmsg("jshell.msg.resetting.state"); 3181 return doReload(null, false, oldOptions); 3182 } 3183 3184 private boolean cmdReload(String rawargs) { 3185 Options oldOptions = rawargs.trim().isEmpty()? null : options; 3186 OptionParserReload ap = new OptionParserReload(); 3187 if (!parseCommandLineLikeFlags(rawargs, ap)) { 3188 return false; 3189 } 3190 ReplayableHistory history; 3191 if (ap.restore()) { 3192 if (replayableHistoryPrevious == null) { 3193 errormsg("jshell.err.reload.no.previous"); 3194 return false; 3195 } 3196 history = replayableHistoryPrevious; 3197 fluffmsg("jshell.err.reload.restarting.previous.state"); 3198 } else { 3199 history = replayableHistory; 3200 fluffmsg("jshell.err.reload.restarting.state"); 3201 } 3202 boolean success = doReload(history, !ap.quiet(), oldOptions); 3203 if (success && ap.restore()) { 3204 // if we are restoring from previous, then if nothing was added 3205 // before time of exit, there is nothing to save 3206 replayableHistory.markSaved(); 3207 } 3208 return success; 3209 } 3210 3211 private boolean cmdEnv(String rawargs) { 3212 if (rawargs.trim().isEmpty()) { 3213 // No arguments, display current settings (as option flags) 3214 StringBuilder sb = new StringBuilder(); 3215 for (String a : options.shownOptions()) { 3216 sb.append( 3217 a.startsWith("-") 3218 ? sb.length() > 0 3219 ? "\n " 3220 : " " 3221 : " "); 3222 sb.append(a); 3223 } 3224 if (sb.length() > 0) { 3225 hard(sb.toString()); 3226 } 3227 return false; 3228 } 3229 Options oldOptions = options; 3230 if (!parseCommandLineLikeFlags(rawargs, new OptionParserBase())) { 3231 return false; 3232 } 3233 fluffmsg("jshell.msg.set.restore"); 3234 return doReload(replayableHistory, false, oldOptions); 3235 } 3236 3237 private boolean doReload(ReplayableHistory history, boolean echo, Options oldOptions) { 3238 if (oldOptions != null) { 3239 try { 3240 resetState(); 3241 } catch (IllegalStateException ex) { 3242 currentNameSpace = mainNamespace; // back out of start-up (messages) 3243 errormsg("jshell.err.restart.failed", ex.getMessage()); 3244 // attempt recovery to previous option settings 3245 options = oldOptions; 3246 resetState(); 3247 } 3248 } else { 3249 resetState(); 3250 } 3251 if (history != null) { 3252 run(new ReloadIOContext(history.iterable(), 3253 echo ? cmdout : null)); 3254 } 3255 return true; 3256 } 3257 3258 private boolean parseCommandLineLikeFlags(String rawargs, OptionParserBase ap) { 3259 String[] args = Arrays.stream(rawargs.split("\\s+")) 3260 .filter(s -> !s.isEmpty()) 3261 .toArray(String[]::new); 3262 Options opts = ap.parse(args); 3263 if (opts == null) { 3264 return false; 3265 } 3266 if (!ap.nonOptions().isEmpty()) { 3267 errormsg("jshell.err.unexpected.at.end", ap.nonOptions(), rawargs); 3268 return false; 3269 } 3270 options = options.override(opts); 3271 return true; 3272 } 3273 3274 private boolean cmdSave(String rawargs) { 3275 // The filename to save to is the last argument, extract it 3276 String[] args = rawargs.split("\\s"); 3277 String filename = args[args.length - 1]; 3278 if (filename.isEmpty()) { 3279 errormsg("jshell.err.file.filename", "/save"); 3280 return false; 3281 } 3282 // All the non-filename arguments are the specifier of what to save 3283 String srcSpec = Arrays.stream(args, 0, args.length - 1) 3284 .collect(Collectors.joining("\n")); 3285 // From the what to save specifier, compute the snippets (as a stream) 3286 ArgTokenizer at = new ArgTokenizer("/save", srcSpec); 3287 at.allowedOptions("-all", "-start", "-history"); 3288 Stream<Snippet> snippetStream = argsOptionsToSnippets(state::snippets, this::mainActive, at); 3289 if (snippetStream == null) { 3290 // error occurred, already reported 3291 return false; 3292 } 3293 try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename), 3294 Charset.defaultCharset(), 3295 CREATE, TRUNCATE_EXISTING, WRITE)) { 3296 if (at.hasOption("-history")) { 3297 // they want history (commands and snippets), ignore the snippet stream 3298 for (String s : input.history(true)) { 3299 writer.write(s); 3300 writer.write("\n"); 3301 } 3302 } else { 3303 // write the snippet stream to the file 3304 writer.write(snippetStream 3305 .map(Snippet::source) 3306 .collect(Collectors.joining("\n"))); 3307 } 3308 } catch (FileNotFoundException e) { 3309 errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage()); 3310 return false; 3311 } catch (Exception e) { 3312 errormsg("jshell.err.file.exception", "/save", filename, e); 3313 return false; 3314 } 3315 return true; 3316 } 3317 3318 private boolean cmdVars(String arg) { 3319 Stream<VarSnippet> stream = argsOptionsToSnippets(this::allVarSnippets, 3320 this::isActive, arg, "/vars"); 3321 if (stream == null) { 3322 return false; 3323 } 3324 stream.forEachOrdered(vk -> 3325 { 3326 String val = state.status(vk) == Status.VALID 3327 ? feedback.truncateVarValue(state.varValue(vk)) 3328 : getResourceString("jshell.msg.vars.not.active"); 3329 hard(" %s %s = %s", vk.typeName(), vk.name(), val); 3330 }); 3331 return true; 3332 } 3333 3334 private boolean cmdMethods(String arg) { 3335 Stream<MethodSnippet> stream = argsOptionsToSnippets(this::allMethodSnippets, 3336 this::isActive, arg, "/methods"); 3337 if (stream == null) { 3338 return false; 3339 } 3340 stream.forEachOrdered(meth -> { 3341 String sig = meth.signature(); 3342 int i = sig.lastIndexOf(")") + 1; 3343 if (i <= 0) { 3344 hard(" %s", meth.name()); 3345 } else { 3346 hard(" %s %s%s", sig.substring(i), meth.name(), sig.substring(0, i)); 3347 } 3348 printSnippetStatus(meth, true); 3349 }); 3350 return true; 3351 } 3352 3353 private boolean cmdTypes(String arg) { 3354 Stream<TypeDeclSnippet> stream = argsOptionsToSnippets(this::allTypeSnippets, 3355 this::isActive, arg, "/types"); 3356 if (stream == null) { 3357 return false; 3358 } 3359 stream.forEachOrdered(ck 3360 -> { 3361 String kind; 3362 switch (ck.subKind()) { 3363 case INTERFACE_SUBKIND: 3364 kind = "interface"; 3365 break; 3366 case CLASS_SUBKIND: 3367 kind = "class"; 3368 break; 3369 case ENUM_SUBKIND: 3370 kind = "enum"; 3371 break; 3372 case ANNOTATION_TYPE_SUBKIND: 3373 kind = "@interface"; 3374 break; 3375 case RECORD_SUBKIND: 3376 kind = "record"; 3377 break; 3378 default: 3379 assert false : "Wrong kind" + ck.subKind(); 3380 kind = "class"; 3381 break; 3382 } 3383 hard(" %s %s", kind, ck.name()); 3384 printSnippetStatus(ck, true); 3385 }); 3386 return true; 3387 } 3388 3389 private boolean cmdImports() { 3390 state.imports().forEach(ik -> { 3391 hard(" import %s%s", ik.isStatic() ? "static " : "", ik.fullname()); 3392 }); 3393 return true; 3394 } 3395 3396 private boolean cmdUseHistoryEntry(int index) { 3397 List<Snippet> keys = state.snippets().collect(toList()); 3398 if (index < 0) 3399 index += keys.size(); 3400 else 3401 index--; 3402 if (index >= 0 && index < keys.size()) { 3403 rerunSnippet(keys.get(index)); 3404 } else { 3405 errormsg("jshell.err.out.of.range"); 3406 return false; 3407 } 3408 return true; 3409 } 3410 3411 boolean checkOptionsAndRemainingInput(ArgTokenizer at) { 3412 String junk = at.remainder(); 3413 if (!junk.isEmpty()) { 3414 errormsg("jshell.err.unexpected.at.end", junk, at.whole()); 3415 return false; 3416 } else { 3417 String bad = at.badOptions(); 3418 if (!bad.isEmpty()) { 3419 errormsg("jshell.err.unknown.option", bad, at.whole()); 3420 return false; 3421 } 3422 } 3423 return true; 3424 } 3425 3426 /** 3427 * Handle snippet reevaluation commands: {@code /<id>}. These commands are a 3428 * sequence of ids and id ranges (names are permitted, though not in the 3429 * first position. Support for names is purposely not documented). 3430 * 3431 * @param rawargs the whole command including arguments 3432 */ 3433 private void rerunHistoryEntriesById(String rawargs) { 3434 ArgTokenizer at = new ArgTokenizer("/<id>", rawargs.trim().substring(1)); 3435 at.allowedOptions(); 3436 Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, sn -> true, at); 3437 if (stream != null) { 3438 // successfully parsed, rerun snippets 3439 stream.forEach(sn -> rerunSnippet(sn)); 3440 } 3441 } 3442 3443 private void rerunSnippet(Snippet snippet) { 3444 String source = snippet.source(); 3445 cmdout.printf("%s\n", source); 3446 input.replaceLastHistoryEntry(source); 3447 processSourceCatchingReset(source); 3448 } 3449 3450 /** 3451 * Filter diagnostics for only errors (no warnings, ...) 3452 * @param diagnostics input list 3453 * @return filtered list 3454 */ 3455 List<Diag> errorsOnly(List<Diag> diagnostics) { 3456 return diagnostics.stream() 3457 .filter(Diag::isError) 3458 .collect(toList()); 3459 } 3460 3461 /** 3462 * Print out a snippet exception. 3463 * 3464 * @param exception the throwable to print 3465 * @return true on fatal exception 3466 */ 3467 private boolean displayException(Throwable exception) { 3468 Throwable rootCause = exception; 3469 while (rootCause instanceof EvalException) { 3470 rootCause = rootCause.getCause(); 3471 } 3472 if (rootCause != exception && rootCause instanceof UnresolvedReferenceException) { 3473 // An unresolved reference caused a chained exception, just show the unresolved 3474 return displayException(rootCause, null); 3475 } else { 3476 return displayException(exception, null); 3477 } 3478 } 3479 //where 3480 private boolean displayException(Throwable exception, StackTraceElement[] caused) { 3481 if (exception instanceof EvalException) { 3482 // User exception 3483 return displayEvalException((EvalException) exception, caused); 3484 } else if (exception instanceof UnresolvedReferenceException) { 3485 // Reference to an undefined snippet 3486 return displayUnresolvedException((UnresolvedReferenceException) exception); 3487 } else { 3488 // Should never occur 3489 error("Unexpected execution exception: %s", exception); 3490 return true; 3491 } 3492 } 3493 //where 3494 private boolean displayUnresolvedException(UnresolvedReferenceException ex) { 3495 // Display the resolution issue 3496 printSnippetStatus(ex.getSnippet(), false); 3497 return false; 3498 } 3499 3500 //where 3501 private boolean displayEvalException(EvalException ex, StackTraceElement[] caused) { 3502 // The message for the user exception is configured based on the 3503 // existance of an exception message and if this is a recursive 3504 // invocation for a chained exception. 3505 String msg = ex.getMessage(); 3506 String key = "jshell.err.exception" + 3507 (caused == null? ".thrown" : ".cause") + 3508 (msg == null? "" : ".message"); 3509 errormsg(key, ex.getExceptionClassName(), msg); 3510 // The caused trace is sent to truncate duplicate elements in the cause trace 3511 printStackTrace(ex.getStackTrace(), caused); 3512 JShellException cause = ex.getCause(); 3513 if (cause != null) { 3514 // Display the cause (recursively) 3515 displayException(cause, ex.getStackTrace()); 3516 } 3517 return true; 3518 } 3519 3520 /** 3521 * Display a list of diagnostics. 3522 * 3523 * @param source the source line with the error/warning 3524 * @param diagnostics the diagnostics to display 3525 */ 3526 private void displayDiagnostics(String source, List<Diag> diagnostics) { 3527 for (Diag d : diagnostics) { 3528 errormsg(d.isError() ? "jshell.msg.error" : "jshell.msg.warning"); 3529 List<String> disp = new ArrayList<>(); 3530 displayableDiagnostic(source, d, disp); 3531 disp.stream() 3532 .forEach(l -> error("%s", l)); 3533 } 3534 } 3535 3536 /** 3537 * Convert a diagnostic into a list of pretty displayable strings with 3538 * source context. 3539 * 3540 * @param source the source line for the error/warning 3541 * @param diag the diagnostic to convert 3542 * @param toDisplay a list that the displayable strings are added to 3543 */ 3544 private void displayableDiagnostic(String source, Diag diag, List<String> toDisplay) { 3545 for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize 3546 if (!line.trim().startsWith("location:")) { 3547 toDisplay.add(line); 3548 } 3549 } 3550 3551 int pstart = (int) diag.getStartPosition(); 3552 int pend = (int) diag.getEndPosition(); 3553 if (pstart < 0 || pend < 0) { 3554 pstart = 0; 3555 pend = source.length(); 3556 } 3557 Matcher m = LINEBREAK.matcher(source); 3558 int pstartl = 0; 3559 int pendl = -2; 3560 while (m.find(pstartl)) { 3561 pendl = m.start(); 3562 if (pendl >= pstart) { 3563 break; 3564 } else { 3565 pstartl = m.end(); 3566 } 3567 } 3568 if (pendl < pstartl) { 3569 pendl = source.length(); 3570 } 3571 toDisplay.add(source.substring(pstartl, pendl)); 3572 3573 StringBuilder sb = new StringBuilder(); 3574 int start = pstart - pstartl; 3575 for (int i = 0; i < start; ++i) { 3576 sb.append(' '); 3577 } 3578 sb.append('^'); 3579 boolean multiline = pend > pendl; 3580 int end = (multiline ? pendl : pend) - pstartl - 1; 3581 if (end > start) { 3582 for (int i = start + 1; i < end; ++i) { 3583 sb.append('-'); 3584 } 3585 if (multiline) { 3586 sb.append("-..."); 3587 } else { 3588 sb.append('^'); 3589 } 3590 } 3591 toDisplay.add(sb.toString()); 3592 3593 debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this); 3594 debug("Code: %s", diag.getCode()); 3595 debug("Pos: %d (%d - %d)", diag.getPosition(), 3596 diag.getStartPosition(), diag.getEndPosition()); 3597 } 3598 3599 /** 3600 * Process a source snippet. 3601 * 3602 * @param source the input source 3603 * @return true if the snippet succeeded 3604 */ 3605 boolean processSource(String source) { 3606 debug("Compiling: %s", source); 3607 boolean failed = false; 3608 boolean isActive = false; 3609 List<SnippetEvent> events = state.eval(source); 3610 for (SnippetEvent e : events) { 3611 // Report the event, recording failure 3612 failed |= handleEvent(e); 3613 3614 // If any main snippet is active, this should be replayable 3615 // also ignore var value queries 3616 isActive |= e.causeSnippet() == null && 3617 e.status().isActive() && 3618 e.snippet().subKind() != VAR_VALUE_SUBKIND; 3619 } 3620 // If this is an active snippet and it didn't cause the backend to die, 3621 // add it to the replayable history 3622 if (isActive && live) { 3623 addToReplayHistory(source); 3624 } 3625 3626 return !failed; 3627 } 3628 3629 // Handle incoming snippet events -- return true on failure 3630 private boolean handleEvent(SnippetEvent ste) { 3631 Snippet sn = ste.snippet(); 3632 if (sn == null) { 3633 debug("Event with null key: %s", ste); 3634 return false; 3635 } 3636 List<Diag> diagnostics = state.diagnostics(sn).collect(toList()); 3637 String source = sn.source(); 3638 if (ste.causeSnippet() == null) { 3639 // main event 3640 displayDiagnostics(source, diagnostics); 3641 3642 if (ste.status() != Status.REJECTED) { 3643 if (ste.exception() != null) { 3644 if (displayException(ste.exception())) { 3645 return true; 3646 } 3647 } else { 3648 new DisplayEvent(ste, FormatWhen.PRIMARY, ste.value(), diagnostics) 3649 .displayDeclarationAndValue(); 3650 } 3651 } else { 3652 if (diagnostics.isEmpty()) { 3653 errormsg("jshell.err.failed"); 3654 } 3655 return true; 3656 } 3657 } else { 3658 // Update 3659 if (sn instanceof DeclarationSnippet) { 3660 List<Diag> other = errorsOnly(diagnostics); 3661 3662 // display update information 3663 new DisplayEvent(ste, FormatWhen.UPDATE, ste.value(), other) 3664 .displayDeclarationAndValue(); 3665 } 3666 } 3667 return false; 3668 } 3669 3670 // Print a stack trace, elide frames displayed for the caused exception 3671 void printStackTrace(StackTraceElement[] stes, StackTraceElement[] caused) { 3672 int overlap = 0; 3673 if (caused != null) { 3674 int maxOverlap = Math.min(stes.length, caused.length); 3675 while (overlap < maxOverlap 3676 && stes[stes.length - (overlap + 1)].equals(caused[caused.length - (overlap + 1)])) { 3677 ++overlap; 3678 } 3679 } 3680 for (int i = 0; i < stes.length - overlap; ++i) { 3681 StackTraceElement ste = stes[i]; 3682 StringBuilder sb = new StringBuilder(); 3683 String cn = ste.getClassName(); 3684 if (!cn.isEmpty()) { 3685 int dot = cn.lastIndexOf('.'); 3686 if (dot > 0) { 3687 sb.append(cn.substring(dot + 1)); 3688 } else { 3689 sb.append(cn); 3690 } 3691 sb.append("."); 3692 } 3693 if (!ste.getMethodName().isEmpty()) { 3694 sb.append(ste.getMethodName()); 3695 sb.append(" "); 3696 } 3697 String fileName = ste.getFileName(); 3698 int lineNumber = ste.getLineNumber(); 3699 String loc = ste.isNativeMethod() 3700 ? getResourceString("jshell.msg.native.method") 3701 : fileName == null 3702 ? getResourceString("jshell.msg.unknown.source") 3703 : lineNumber >= 0 3704 ? fileName + ":" + lineNumber 3705 : fileName; 3706 error(" at %s(%s)", sb, loc); 3707 3708 } 3709 if (overlap != 0) { 3710 error(" ..."); 3711 } 3712 } 3713 3714 private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) { 3715 FormatAction act; 3716 switch (status) { 3717 case VALID: 3718 case RECOVERABLE_DEFINED: 3719 case RECOVERABLE_NOT_DEFINED: 3720 if (previousStatus.isActive()) { 3721 act = isSignatureChange 3722 ? FormatAction.REPLACED 3723 : FormatAction.MODIFIED; 3724 } else { 3725 act = FormatAction.ADDED; 3726 } 3727 break; 3728 case OVERWRITTEN: 3729 act = FormatAction.OVERWROTE; 3730 break; 3731 case DROPPED: 3732 act = FormatAction.DROPPED; 3733 break; 3734 case REJECTED: 3735 case NONEXISTENT: 3736 default: 3737 // Should not occur 3738 error("Unexpected status: " + previousStatus.toString() + "=>" + status.toString()); 3739 act = FormatAction.DROPPED; 3740 } 3741 return act; 3742 } 3743 3744 void printSnippetStatus(DeclarationSnippet sn, boolean resolve) { 3745 List<Diag> otherErrors = errorsOnly(state.diagnostics(sn).collect(toList())); 3746 new DisplayEvent(sn, state.status(sn), resolve, otherErrors) 3747 .displayDeclarationAndValue(); 3748 } 3749 3750 class DisplayEvent { 3751 private final Snippet sn; 3752 private final FormatAction action; 3753 private final FormatWhen update; 3754 private final String value; 3755 private final List<String> errorLines; 3756 private final FormatResolve resolution; 3757 private final String unresolved; 3758 private final FormatUnresolved unrcnt; 3759 private final FormatErrors errcnt; 3760 private final boolean resolve; 3761 3762 DisplayEvent(SnippetEvent ste, FormatWhen update, String value, List<Diag> errors) { 3763 this(ste.snippet(), ste.status(), false, 3764 toAction(ste.status(), ste.previousStatus(), ste.isSignatureChange()), 3765 update, value, errors); 3766 } 3767 3768 DisplayEvent(Snippet sn, Status status, boolean resolve, List<Diag> errors) { 3769 this(sn, status, resolve, FormatAction.USED, FormatWhen.UPDATE, null, errors); 3770 } 3771 3772 private DisplayEvent(Snippet sn, Status status, boolean resolve, 3773 FormatAction action, FormatWhen update, String value, List<Diag> errors) { 3774 this.sn = sn; 3775 this.resolve =resolve; 3776 this.action = action; 3777 this.update = update; 3778 this.value = value; 3779 this.errorLines = new ArrayList<>(); 3780 for (Diag d : errors) { 3781 displayableDiagnostic(sn.source(), d, errorLines); 3782 } 3783 if (resolve) { 3784 // resolve needs error lines indented 3785 for (int i = 0; i < errorLines.size(); ++i) { 3786 errorLines.set(i, " " + errorLines.get(i)); 3787 } 3788 } 3789 long unresolvedCount; 3790 if (sn instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) { 3791 resolution = (status == Status.RECOVERABLE_NOT_DEFINED) 3792 ? FormatResolve.NOTDEFINED 3793 : FormatResolve.DEFINED; 3794 unresolved = unresolved((DeclarationSnippet) sn); 3795 unresolvedCount = state.unresolvedDependencies((DeclarationSnippet) sn).count(); 3796 } else { 3797 resolution = FormatResolve.OK; 3798 unresolved = ""; 3799 unresolvedCount = 0; 3800 } 3801 unrcnt = unresolvedCount == 0 3802 ? FormatUnresolved.UNRESOLVED0 3803 : unresolvedCount == 1 3804 ? FormatUnresolved.UNRESOLVED1 3805 : FormatUnresolved.UNRESOLVED2; 3806 errcnt = errors.isEmpty() 3807 ? FormatErrors.ERROR0 3808 : errors.size() == 1 3809 ? FormatErrors.ERROR1 3810 : FormatErrors.ERROR2; 3811 } 3812 3813 private String unresolved(DeclarationSnippet key) { 3814 List<String> unr = state.unresolvedDependencies(key).collect(toList()); 3815 StringBuilder sb = new StringBuilder(); 3816 int fromLast = unr.size(); 3817 if (fromLast > 0) { 3818 sb.append(" "); 3819 } 3820 for (String u : unr) { 3821 --fromLast; 3822 sb.append(u); 3823 switch (fromLast) { 3824 // No suffix 3825 case 0: 3826 break; 3827 case 1: 3828 sb.append(", and "); 3829 break; 3830 default: 3831 sb.append(", "); 3832 break; 3833 } 3834 } 3835 return sb.toString(); 3836 } 3837 3838 private void custom(FormatCase fcase, String name) { 3839 custom(fcase, name, null); 3840 } 3841 3842 private void custom(FormatCase fcase, String name, String type) { 3843 if (resolve) { 3844 String resolutionErrors = feedback.format("resolve", fcase, action, update, 3845 resolution, unrcnt, errcnt, 3846 name, type, value, unresolved, errorLines); 3847 if (!resolutionErrors.trim().isEmpty()) { 3848 error(" %s", resolutionErrors); 3849 } 3850 } else if (interactive()) { 3851 String display = feedback.format(fcase, action, update, 3852 resolution, unrcnt, errcnt, 3853 name, type, value, unresolved, errorLines); 3854 cmdout.print(display); 3855 } 3856 } 3857 3858 @SuppressWarnings("fallthrough") 3859 private void displayDeclarationAndValue() { 3860 switch (sn.subKind()) { 3861 case CLASS_SUBKIND: 3862 custom(FormatCase.CLASS, ((TypeDeclSnippet) sn).name()); 3863 break; 3864 case INTERFACE_SUBKIND: 3865 custom(FormatCase.INTERFACE, ((TypeDeclSnippet) sn).name()); 3866 break; 3867 case ENUM_SUBKIND: 3868 custom(FormatCase.ENUM, ((TypeDeclSnippet) sn).name()); 3869 break; 3870 case ANNOTATION_TYPE_SUBKIND: 3871 custom(FormatCase.ANNOTATION, ((TypeDeclSnippet) sn).name()); 3872 break; 3873 case RECORD_SUBKIND: 3874 custom(FormatCase.RECORD, ((TypeDeclSnippet) sn).name()); 3875 break; 3876 case METHOD_SUBKIND: 3877 custom(FormatCase.METHOD, ((MethodSnippet) sn).name(), ((MethodSnippet) sn).parameterTypes()); 3878 break; 3879 case VAR_DECLARATION_SUBKIND: { 3880 VarSnippet vk = (VarSnippet) sn; 3881 custom(FormatCase.VARDECL, vk.name(), vk.typeName()); 3882 break; 3883 } 3884 case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: { 3885 VarSnippet vk = (VarSnippet) sn; 3886 custom(FormatCase.VARINIT, vk.name(), vk.typeName()); 3887 break; 3888 } 3889 case TEMP_VAR_EXPRESSION_SUBKIND: { 3890 VarSnippet vk = (VarSnippet) sn; 3891 custom(FormatCase.EXPRESSION, vk.name(), vk.typeName()); 3892 break; 3893 } 3894 case OTHER_EXPRESSION_SUBKIND: 3895 error("Unexpected expression form -- value is: %s", (value)); 3896 break; 3897 case VAR_VALUE_SUBKIND: { 3898 ExpressionSnippet ek = (ExpressionSnippet) sn; 3899 custom(FormatCase.VARVALUE, ek.name(), ek.typeName()); 3900 break; 3901 } 3902 case ASSIGNMENT_SUBKIND: { 3903 ExpressionSnippet ek = (ExpressionSnippet) sn; 3904 custom(FormatCase.ASSIGNMENT, ek.name(), ek.typeName()); 3905 break; 3906 } 3907 case SINGLE_TYPE_IMPORT_SUBKIND: 3908 case TYPE_IMPORT_ON_DEMAND_SUBKIND: 3909 case SINGLE_STATIC_IMPORT_SUBKIND: 3910 case STATIC_IMPORT_ON_DEMAND_SUBKIND: 3911 custom(FormatCase.IMPORT, ((ImportSnippet) sn).name()); 3912 break; 3913 case STATEMENT_SUBKIND: 3914 custom(FormatCase.STATEMENT, null); 3915 break; 3916 } 3917 } 3918 } 3919 3920 /** The current version number as a string. 3921 */ 3922 String version() { 3923 return version("release"); // mm.nn.oo[-milestone] 3924 } 3925 3926 /** The current full version number as a string. 3927 */ 3928 String fullVersion() { 3929 return version("full"); // mm.mm.oo[-milestone]-build 3930 } 3931 3932 private String version(String key) { 3933 if (versionRB == null) { 3934 try { 3935 versionRB = ResourceBundle.getBundle(VERSION_RB_NAME, locale); 3936 } catch (MissingResourceException e) { 3937 return "(version info not available)"; 3938 } 3939 } 3940 try { 3941 return versionRB.getString(key); 3942 } 3943 catch (MissingResourceException e) { 3944 return "(version info not available)"; 3945 } 3946 } 3947 3948 class NameSpace { 3949 final String spaceName; 3950 final String prefix; 3951 private int nextNum; 3952 3953 NameSpace(String spaceName, String prefix) { 3954 this.spaceName = spaceName; 3955 this.prefix = prefix; 3956 this.nextNum = 1; 3957 } 3958 3959 String tid(Snippet sn) { 3960 String tid = prefix + nextNum++; 3961 mapSnippet.put(sn, new SnippetInfo(sn, this, tid)); 3962 return tid; 3963 } 3964 3965 String tidNext() { 3966 return prefix + nextNum; 3967 } 3968 } 3969 3970 static class SnippetInfo { 3971 final Snippet snippet; 3972 final NameSpace space; 3973 final String tid; 3974 3975 SnippetInfo(Snippet snippet, NameSpace space, String tid) { 3976 this.snippet = snippet; 3977 this.space = space; 3978 this.tid = tid; 3979 } 3980 } 3981 3982 static class ArgSuggestion implements Suggestion { 3983 3984 private final String continuation; 3985 3986 /** 3987 * Create a {@code Suggestion} instance. 3988 * 3989 * @param continuation a candidate continuation of the user's input 3990 */ 3991 public ArgSuggestion(String continuation) { 3992 this.continuation = continuation; 3993 } 3994 3995 /** 3996 * The candidate continuation of the given user's input. 3997 * 3998 * @return the continuation string 3999 */ 4000 @Override 4001 public String continuation() { 4002 return continuation; 4003 } 4004 4005 /** 4006 * Indicates whether input continuation matches the target type and is thus 4007 * more likely to be the desired continuation. A matching continuation is 4008 * preferred. 4009 * 4010 * @return {@code false}, non-types analysis 4011 */ 4012 @Override 4013 public boolean matchesType() { 4014 return false; 4015 } 4016 } 4017 } 4018 4019 abstract class NonInteractiveIOContext extends IOContext { 4020 4021 @Override 4022 public boolean interactiveOutput() { 4023 return false; 4024 } 4025 4026 @Override 4027 public Iterable<String> history(boolean currentSession) { 4028 return Collections.emptyList(); 4029 } 4030 4031 @Override 4032 public boolean terminalEditorRunning() { 4033 return false; 4034 } 4035 4036 @Override 4037 public void suspend() { 4038 } 4039 4040 @Override 4041 public void resume() { 4042 } 4043 4044 @Override 4045 public void beforeUserCode() { 4046 } 4047 4048 @Override 4049 public void afterUserCode() { 4050 } 4051 4052 @Override 4053 public void replaceLastHistoryEntry(String source) { 4054 } 4055 } 4056 4057 class ScannerIOContext extends NonInteractiveIOContext { 4058 private final Scanner scannerIn; 4059 4060 ScannerIOContext(Scanner scannerIn) { 4061 this.scannerIn = scannerIn; 4062 } 4063 4064 ScannerIOContext(Reader rdr) throws FileNotFoundException { 4065 this(new Scanner(rdr)); 4066 } 4067 4068 @Override 4069 public String readLine(String firstLinePrompt, String continuationPrompt, boolean firstLine, String prefix) { 4070 if (scannerIn.hasNextLine()) { 4071 return scannerIn.nextLine(); 4072 } else { 4073 return null; 4074 } 4075 } 4076 4077 @Override 4078 public void close() { 4079 scannerIn.close(); 4080 } 4081 4082 @Override 4083 public int readUserInput() { 4084 return -1; 4085 } 4086 } 4087 4088 class ReloadIOContext extends NonInteractiveIOContext { 4089 private final Iterator<String> it; 4090 private final PrintStream echoStream; 4091 4092 ReloadIOContext(Iterable<String> history, PrintStream echoStream) { 4093 this.it = history.iterator(); 4094 this.echoStream = echoStream; 4095 } 4096 4097 @Override 4098 public String readLine(String firstLinePrompt, String continuationPrompt, boolean firstLine, String prefix) { 4099 String s = it.hasNext() 4100 ? it.next() 4101 : null; 4102 if (echoStream != null && s != null) { 4103 String p = "-: "; 4104 String p2 = "\n "; 4105 echoStream.printf("%s%s\n", p, s.replace("\n", p2)); 4106 } 4107 return s; 4108 } 4109 4110 @Override 4111 public void close() { 4112 } 4113 4114 @Override 4115 public int readUserInput() { 4116 return -1; 4117 } 4118 } 4119