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