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