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