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