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