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