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