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