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