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