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