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