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