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