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