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