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