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