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