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