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