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