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