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