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