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