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