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