langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java
author shinyafox
Wed, 28 Sep 2016 16:36:10 +0900
changeset 41248 2a3e74c5ad8a
parent 40767 c7908e8c786b
child 41514 a75c2b869d8d
permissions -rw-r--r--
8154714: jshell tool: add exports support Reviewed-by: jlahoda, rfield

/*
 * Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.internal.jshell.tool;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.nio.file.AccessDeniedException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Scanner;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.prefs.Preferences;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import jdk.internal.jshell.debug.InternalDebugControl;
import jdk.internal.jshell.tool.IOContext.InputInterruptedException;
import jdk.jshell.DeclarationSnippet;
import jdk.jshell.Diag;
import jdk.jshell.EvalException;
import jdk.jshell.ExpressionSnippet;
import jdk.jshell.ImportSnippet;
import jdk.jshell.JShell;
import jdk.jshell.JShell.Subscription;
import jdk.jshell.MethodSnippet;
import jdk.jshell.PersistentSnippet;
import jdk.jshell.Snippet;
import jdk.jshell.Snippet.Status;
import jdk.jshell.SnippetEvent;
import jdk.jshell.SourceCodeAnalysis;
import jdk.jshell.SourceCodeAnalysis.CompletionInfo;
import jdk.jshell.SourceCodeAnalysis.Suggestion;
import jdk.jshell.TypeDeclSnippet;
import jdk.jshell.UnresolvedReferenceException;
import jdk.jshell.VarSnippet;

import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import java.util.MissingResourceException;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.Spliterators;
import java.util.function.Function;
import java.util.function.Supplier;
import jdk.internal.joptsimple.*;
import jdk.internal.jshell.tool.Feedback.FormatAction;
import jdk.internal.jshell.tool.Feedback.FormatCase;
import jdk.internal.jshell.tool.Feedback.FormatErrors;
import jdk.internal.jshell.tool.Feedback.FormatResolve;
import jdk.internal.jshell.tool.Feedback.FormatUnresolved;
import jdk.internal.jshell.tool.Feedback.FormatWhen;
import static java.util.Arrays.asList;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static jdk.jshell.Snippet.SubKind.VAR_VALUE_SUBKIND;
import static java.util.stream.Collectors.toMap;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_COMPA;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_DEP;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_EVNT;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_FMGR;
import static jdk.internal.jshell.debug.InternalDebugControl.DBG_GEN;
import static jdk.internal.jshell.tool.ContinuousCompletionProvider.STARTSWITH_MATCHER;

/**
 * Command line REPL tool for Java using the JShell API.
 * @author Robert Field
 */
public class JShellTool implements MessageHandler {

    private static final String LINE_SEP = System.getProperty("line.separator");
    private static final Pattern LINEBREAK = Pattern.compile("\\R");
    private static final String RECORD_SEPARATOR = "\u241E";
    private static final String RB_NAME_PREFIX  = "jdk.internal.jshell.tool.resources";
    private static final String VERSION_RB_NAME = RB_NAME_PREFIX + ".version";
    private static final String L10N_RB_NAME    = RB_NAME_PREFIX + ".l10n";

    final InputStream cmdin;
    final PrintStream cmdout;
    final PrintStream cmderr;
    final PrintStream console;
    final InputStream userin;
    final PrintStream userout;
    final PrintStream usererr;
    final Preferences prefs;
    final Locale locale;

    final Feedback feedback = new Feedback();

    /**
     * The constructor for the tool (used by tool launch via main and by test
     * harnesses to capture ins and outs.
     * @param in command line input -- snippets, commands and user input
     * @param cmdout command line output, feedback including errors
     * @param cmderr start-up errors and debugging info
     * @param console console control interaction
     * @param userout code execution output  -- System.out.printf("hi")
     * @param usererr code execution error stream  -- System.err.printf("Oops")
     * @param prefs preferences to use
     * @param locale locale to use
     */
    public JShellTool(InputStream in, PrintStream cmdout, PrintStream cmderr,
            PrintStream console,
            PrintStream userout, PrintStream usererr,
            Preferences prefs, Locale locale) {
        this(in, cmdout, cmderr, console, null, userout, usererr, prefs, locale);
    }

    /**
     * The constructor for the tool (used by tool launch via main and by test
     * harnesses to capture ins and outs.
     * @param cmdin command line input -- snippets and commands
     * @param cmdout command line output, feedback including errors
     * @param cmderr start-up errors and debugging info
     * @param console console control interaction
     * @param userin code execution input, or null to use IOContext
     * @param userout code execution output  -- System.out.printf("hi")
     * @param usererr code execution error stream  -- System.err.printf("Oops")
     * @param prefs preferences to use
     * @param locale locale to use
     */
    public JShellTool(InputStream cmdin, PrintStream cmdout, PrintStream cmderr,
            PrintStream console,
            InputStream userin, PrintStream userout, PrintStream usererr,
            Preferences prefs, Locale locale) {
        this.cmdin = cmdin;
        this.cmdout = cmdout;
        this.cmderr = cmderr;
        this.console = console;
        this.userin = userin != null ? userin : new InputStream() {
            @Override
            public int read() throws IOException {
                return input.readUserInput();
            }
        };
        this.userout = userout;
        this.usererr = usererr;
        this.prefs = prefs;
        this.locale = locale;
    }

    private ResourceBundle versionRB = null;
    private ResourceBundle outputRB  = null;

    private IOContext input = null;
    private boolean regenerateOnDeath = true;
    private boolean live = false;
    private boolean feedbackInitialized = false;
    private String commandLineFeedbackMode = null;
    private List<String> remoteVMOptions = new ArrayList<>();
    private List<String> compilerOptions = new ArrayList<>();

    SourceCodeAnalysis analysis;
    JShell state = null;
    Subscription shutdownSubscription = null;

    private boolean debug = false;
    public boolean testPrompt = false;
    private String cmdlineClasspath = null;
    private String startup = null;
    private String[] editor = null;
    private boolean editorWait = false;

    // Commands and snippets which should be replayed
    private List<String> replayableHistory;
    private List<String> replayableHistoryPrevious;

    static final String STARTUP_KEY  = "STARTUP";
    static final String EDITOR_KEY   = "EDITOR";
    static final String FEEDBACK_KEY = "FEEDBACK";
    static final String MODE_KEY     = "MODE";
    static final String REPLAY_RESTORE_KEY = "REPLAY_RESTORE";

    static final String DEFAULT_STARTUP =
            "\n" +
            "import java.util.*;\n" +
            "import java.io.*;\n" +
            "import java.math.*;\n" +
            "import java.net.*;\n" +
            "import java.util.concurrent.*;\n" +
            "import java.util.prefs.*;\n" +
            "import java.util.regex.*;\n" +
            "void printf(String format, Object... args) { System.out.printf(format, args); }\n";

    // Tool id (tid) mapping: the three name spaces
    NameSpace mainNamespace;
    NameSpace startNamespace;
    NameSpace errorNamespace;

    // Tool id (tid) mapping: the current name spaces
    NameSpace currentNameSpace;

    Map<Snippet,SnippetInfo> mapSnippet;

    /**
     * Is the input/output currently interactive
     *
     * @return true if console
     */
    boolean interactive() {
        return input != null && input.interactiveOutput();
    }

    void debug(String format, Object... args) {
        if (debug) {
            cmderr.printf(format + "\n", args);
        }
    }

    /**
     * Base output for command output -- no pre- or post-fix
     *
     * @param printf format
     * @param printf args
     */
    void rawout(String format, Object... args) {
        cmdout.printf(format, args);
    }

    /**
     * Must show command output
     *
     * @param format printf format
     * @param args printf args
     */
    void hard(String format, Object... args) {
        rawout(feedback.getPre() + format + feedback.getPost(), args);
    }

    /**
     * Error command output
     *
     * @param format printf format
     * @param args printf args
     */
    void error(String format, Object... args) {
        rawout(feedback.getErrorPre() + format + feedback.getErrorPost(), args);
    }

    /**
     * Optional output
     *
     * @param format printf format
     * @param args printf args
     */
    @Override
    public void fluff(String format, Object... args) {
        if (feedback.shouldDisplayCommandFluff() && interactive()) {
            hard(format, args);
        }
    }

    /**
     * Optional output -- with embedded per- and post-fix
     *
     * @param format printf format
     * @param args printf args
     */
    void fluffRaw(String format, Object... args) {
        if (feedback.shouldDisplayCommandFluff() && interactive()) {
            rawout(format, args);
        }
    }

    /**
     * Print using resource bundle look-up and adding prefix and postfix
     *
     * @param key the resource key
     */
    String getResourceString(String key) {
        if (outputRB == null) {
            try {
                outputRB = ResourceBundle.getBundle(L10N_RB_NAME, locale);
            } catch (MissingResourceException mre) {
                error("Cannot find ResourceBundle: %s for locale: %s", L10N_RB_NAME, locale);
                return "";
            }
        }
        String s;
        try {
            s = outputRB.getString(key);
        } catch (MissingResourceException mre) {
            error("Missing resource: %s in %s", key, L10N_RB_NAME);
            return "";
        }
        return s;
    }

    /**
     * Add prefixing to embedded newlines in a string, leading with the normal
     * prefix
     *
     * @param s the string to prefix
     */
    String prefix(String s) {
        return prefix(s, feedback.getPre());
    }

    /**
     * Add prefixing to embedded newlines in a string
     *
     * @param s the string to prefix
     * @param leading the string to prepend
     */
    String prefix(String s, String leading) {
        if (s == null || s.isEmpty()) {
            return "";
        }
        return leading
                + s.substring(0, s.length() - 1).replaceAll("\\R", System.getProperty("line.separator") + feedback.getPre())
                + s.substring(s.length() - 1, s.length());
    }

    /**
     * Print using resource bundle look-up and adding prefix and postfix
     *
     * @param key the resource key
     */
    void hardrb(String key) {
        String s = prefix(getResourceString(key));
        cmdout.println(s);
    }

    /**
     * Format using resource bundle look-up using MessageFormat
     *
     * @param key the resource key
     * @param args
     */
    String messageFormat(String key, Object... args) {
        String rs = getResourceString(key);
        return MessageFormat.format(rs, args);
    }

    /**
     * Print using resource bundle look-up, MessageFormat, and add prefix and
     * postfix
     *
     * @param key the resource key
     * @param args
     */
    void hardmsg(String key, Object... args) {
        cmdout.println(prefix(messageFormat(key, args)));
    }

    /**
     * Print error using resource bundle look-up, MessageFormat, and add prefix
     * and postfix
     *
     * @param key the resource key
     * @param args
     */
    @Override
    public void errormsg(String key, Object... args) {
        if (isRunningInteractive()) {
            cmdout.println(prefix(messageFormat(key, args), feedback.getErrorPre()));
        } else {
            startmsg(key, args);
        }
    }

    /**
     * Print command-line error using resource bundle look-up, MessageFormat
     *
     * @param key the resource key
     * @param args
     */
    void startmsg(String key, Object... args) {
        cmderr.println(prefix(messageFormat(key, args), ""));
    }

    /**
     * Print (fluff) using resource bundle look-up, MessageFormat, and add
     * prefix and postfix
     *
     * @param key the resource key
     * @param args
     */
    @Override
    public void fluffmsg(String key, Object... args) {
        if (feedback.shouldDisplayCommandFluff() && interactive()) {
            hardmsg(key, args);
        }
    }

    <T> void hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b) {
        Map<String, String> a2b = stream.collect(toMap(a, b,
                (m1, m2) -> m1,
                () -> new LinkedHashMap<>()));
        int aLen = 0;
        for (String av : a2b.keySet()) {
            aLen = Math.max(aLen, av.length());
        }
        String format = "   %-" + aLen + "s -- %s";
        String indentedNewLine = LINE_SEP + feedback.getPre()
                + String.format("   %-" + (aLen + 4) + "s", "");
        for (Entry<String, String> e : a2b.entrySet()) {
            hard(format, e.getKey(), e.getValue().replaceAll("\n", indentedNewLine));
        }
    }

    /**
     * Trim whitespace off end of string
     *
     * @param s
     * @return
     */
    static String trimEnd(String s) {
        int last = s.length() - 1;
        int i = last;
        while (i >= 0 && Character.isWhitespace(s.charAt(i))) {
            --i;
        }
        if (i != last) {
            return s.substring(0, i + 1);
        } else {
            return s;
        }
    }

    /**
     * Normal start entry point
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        new JShellTool(System.in, System.out, System.err, System.out,
                 System.out, System.err,
                 Preferences.userRoot().node("tool/JShell"),
                 Locale.getDefault())
                .start(args);
    }

    public void start(String[] args) throws Exception {
        List<String> loadList = processCommandArgs(args);
        if (loadList == null) {
            // Abort
            return;
        }
        try (IOContext in = new ConsoleIOContext(this, cmdin, console)) {
            start(in, loadList);
        }
    }

    private void start(IOContext in, List<String> loadList) {
        // If startup hasn't been set by command line, set from retained/default
        if (startup == null) {
            startup = prefs.get(STARTUP_KEY, null);
            if (startup == null) {
                startup = DEFAULT_STARTUP;
            }
        }

        // Read retained editor setting (if any)
        String editorString = prefs.get(EDITOR_KEY, "");
        if (editorString == null || editorString.isEmpty()) {
            editor = null;
        } else {
            char waitMarker = editorString.charAt(0);
            if (waitMarker == '-' || waitMarker == '*') {
                editorWait = waitMarker == '-';
                editorString = editorString.substring(1);
            }
            editor = editorString.split(RECORD_SEPARATOR);
        }

        resetState(); // Initialize

        // Read replay history from last jshell session into previous history
        String prevReplay = prefs.get(REPLAY_RESTORE_KEY, null);
        if (prevReplay != null) {
            replayableHistoryPrevious = Arrays.asList(prevReplay.split(RECORD_SEPARATOR));
        }

        for (String loadFile : loadList) {
            runFile(loadFile, "jshell");
        }

        if (regenerateOnDeath) {
            hardmsg("jshell.msg.welcome", version());
        }

        try {
            while (regenerateOnDeath) {
                if (!live) {
                    resetState();
                }
                run(in);
            }
        } finally {
            closeState();
        }
    }

    /**
     * Process the command line arguments.
     * Set options.
     * @param args the command line arguments
     * @return the list of files to be loaded
     */
    private List<String> processCommandArgs(String[] args) {
        OptionParser parser = new OptionParser();
        OptionSpec<String> cp = parser.accepts("class-path").withRequiredArg();
        OptionSpec<String> st = parser.accepts("startup").withRequiredArg();
        parser.acceptsAll(asList("n", "no-startup"));
        OptionSpec<String> fb = parser.accepts("feedback").withRequiredArg();
        parser.accepts("q");
        parser.accepts("s");
        parser.accepts("v");
        OptionSpec<String> r = parser.accepts("R").withRequiredArg();
        OptionSpec<String> c = parser.accepts("C").withRequiredArg();
        parser.acceptsAll(asList("h", "help"));
        parser.accepts("version");
        parser.accepts("full-version");

        parser.accepts("X");
        OptionSpec<String> addExports = parser.accepts("add-exports").withRequiredArg();

        NonOptionArgumentSpec<String> loadFileSpec = parser.nonOptions();

        OptionSet options;
        try {
            options = parser.parse(args);
        } catch (OptionException ex) {
            if (ex.options().isEmpty()) {
                startmsg("jshell.err.opt.invalid", stream(args).collect(joining(", ")));
            } else {
                boolean isKnown = parser.recognizedOptions().containsKey(ex.options().iterator().next());
                startmsg(isKnown
                        ? "jshell.err.opt.arg"
                        : "jshell.err.opt.unknown",
                        ex.options()
                        .stream()
                        .collect(joining(", ")));
            }
            return null;
        }

        if (options.has("help")) {
            printUsage();
            return null;
        }
        if (options.has("X")) {
            printUsageX();
            return null;
        }
        if (options.has("version")) {
            cmdout.printf("jshell %s\n", version());
            return null;
        }
        if (options.has("full-version")) {
            cmdout.printf("jshell %s\n", fullVersion());
            return null;
        }
        if (options.has(cp)) {
            List<String> cps = options.valuesOf(cp);
            if (cps.size() > 1) {
                startmsg("jshell.err.opt.one", "--class-path");
                return null;
            }
            cmdlineClasspath = cps.get(0);
        }
        if (options.has(st)) {
            List<String> sts = options.valuesOf(st);
            if (sts.size() != 1 || options.has("no-startup")) {
                startmsg("jshell.err.opt.startup.one");
                return null;
            }
            startup = readFile(sts.get(0), "--startup");
            if (startup == null) {
                return null;
            }
        } else if (options.has("no-startup")) {
            startup = "";
        }
        if ((options.valuesOf(fb).size() +
                 (options.has("q") ? 1 : 0) +
                 (options.has("s") ? 1 : 0) +
                 (options.has("v") ? 1 : 0)) > 1) {
            startmsg("jshell.err.opt.feedback.one");
            return null;
        } else if (options.has(fb)) {
            commandLineFeedbackMode = options.valueOf(fb);
        } else if (options.has("q")) {
            commandLineFeedbackMode = "concise";
        } else if (options.has("s")) {
            commandLineFeedbackMode = "silent";
        } else if (options.has("v")) {
            commandLineFeedbackMode = "verbose";
        }
        if (options.has(r)) {
            remoteVMOptions.addAll(options.valuesOf(r));
        }
        if (options.has(c)) {
            compilerOptions.addAll(options.valuesOf(c));
        }

        if (options.has(addExports)) {
            List<String> exports = options.valuesOf(addExports).stream()
                    .map(mp -> mp + "=ALL-UNNAMED")
                    .flatMap(mp -> Stream.of("--add-exports", mp))
                    .collect(toList());
            remoteVMOptions.addAll(exports);
            compilerOptions.addAll(exports);
        }

        return options.valuesOf(loadFileSpec);
    }

    private void printUsage() {
        cmdout.print(getResourceString("help.usage"));
    }

    private void printUsageX() {
        cmdout.print(getResourceString("help.usage.x"));
    }

    /**
     * Message handler to use during initial start-up.
     */
    private class InitMessageHandler implements MessageHandler {

        @Override
        public void fluff(String format, Object... args) {
            //ignore
        }

        @Override
        public void fluffmsg(String messageKey, Object... args) {
            //ignore
        }

        @Override
        public void errormsg(String messageKey, Object... args) {
            startmsg(messageKey, args);
        }
    }

    private void resetState() {
        closeState();

        // Initialize tool id mapping
        mainNamespace = new NameSpace("main", "");
        startNamespace = new NameSpace("start", "s");
        errorNamespace = new NameSpace("error", "e");
        mapSnippet = new LinkedHashMap<>();
        currentNameSpace = startNamespace;

        // Reset the replayable history, saving the old for restore
        replayableHistoryPrevious = replayableHistory;
        replayableHistory = new ArrayList<>();

        state = JShell.builder()
                .in(userin)
                .out(userout)
                .err(usererr)
                .tempVariableNameGenerator(()-> "$" + currentNameSpace.tidNext())
                .idGenerator((sn, i) -> (currentNameSpace == startNamespace || state.status(sn).isActive())
                        ? currentNameSpace.tid(sn)
                        : errorNamespace.tid(sn))
                .remoteVMOptions(remoteVMOptions.stream().toArray(String[]::new))
                .compilerOptions(compilerOptions.stream().toArray(String[]::new))
                .build();
        shutdownSubscription = state.onShutdown((JShell deadState) -> {
            if (deadState == state) {
                hardmsg("jshell.msg.terminated");
                live = false;
            }
        });
        analysis = state.sourceCodeAnalysis();
        live = true;
        if (!feedbackInitialized) {
            // One time per run feedback initialization
            feedbackInitialized = true;
            initFeedback();
        }

        if (cmdlineClasspath != null) {
            state.addToClasspath(cmdlineClasspath);
        }

        startUpRun(startup);
        currentNameSpace = mainNamespace;
    }

    private boolean isRunningInteractive() {
        return currentNameSpace != null && currentNameSpace == mainNamespace;
    }

    //where -- one-time per run initialization of feedback modes
    private void initFeedback() {
        // No fluff, no prefix, for init failures
        MessageHandler initmh = new InitMessageHandler();
        // Execute the feedback initialization code in the resource file
        startUpRun(getResourceString("startup.feedback"));
        // These predefined modes are read-only
        feedback.markModesReadOnly();
        // Restore user defined modes retained on previous run with /retain mode
        String encoded = prefs.get(MODE_KEY, null);
        if (encoded != null && !encoded.isEmpty()) {
            if (!feedback.restoreEncodedModes(initmh, encoded)) {
                // Catastrophic corruption -- remove the retained modes
                prefs.remove(MODE_KEY);
            }
        }
        if (commandLineFeedbackMode != null) {
            // The feedback mode to use was specified on the command line, use it
            if (!feedback.setFeedback(initmh, new ArgTokenizer("--feedback", commandLineFeedbackMode))) {
                regenerateOnDeath = false;
            }
            commandLineFeedbackMode = null;
        } else {
            String fb = prefs.get(FEEDBACK_KEY, null);
            if (fb != null) {
                // Restore the feedback mode to use that was retained
                // on a previous run with /retain feedback
                feedback.retainFeedback(initmh, new ArgTokenizer("/retain feedback", fb));
            }
        }
    }

    //where
    private void startUpRun(String start) {
        try (IOContext suin = new FileScannerIOContext(new StringReader(start))) {
            run(suin);
        } catch (Exception ex) {
            hardmsg("jshell.err.startup.unexpected.exception", ex);
            ex.printStackTrace(cmdout);
        }
    }

    private void closeState() {
        live = false;
        JShell oldState = state;
        if (oldState != null) {
            oldState.unsubscribe(shutdownSubscription); // No notification
            oldState.close();
        }
    }

    /**
     * Main loop
     * @param in the line input/editing context
     */
    private void run(IOContext in) {
        IOContext oldInput = input;
        input = in;
        try {
            String incomplete = "";
            while (live) {
                String prompt;
                if (isRunningInteractive()) {
                    prompt = testPrompt
                                    ? incomplete.isEmpty()
                                            ? "\u0005" //ENQ
                                            : "\u0006" //ACK
                                    : incomplete.isEmpty()
                                            ? feedback.getPrompt(currentNameSpace.tidNext())
                                            : feedback.getContinuationPrompt(currentNameSpace.tidNext())
                    ;
                } else {
                    prompt = "";
                }
                String raw;
                try {
                    raw = in.readLine(prompt, incomplete);
                } catch (InputInterruptedException ex) {
                    //input interrupted - clearing current state
                    incomplete = "";
                    continue;
                }
                if (raw == null) {
                    //EOF
                    if (in.interactiveOutput()) {
                        // End after user ctrl-D
                        regenerateOnDeath = false;
                    }
                    break;
                }
                String trimmed = trimEnd(raw);
                if (!trimmed.isEmpty()) {
                    String line = incomplete + trimmed;

                    // No commands in the middle of unprocessed source
                    if (incomplete.isEmpty() && line.startsWith("/") && !line.startsWith("//") && !line.startsWith("/*")) {
                        processCommand(line.trim());
                    } else {
                        incomplete = processSourceCatchingReset(line);
                    }
                }
            }
        } catch (IOException ex) {
            errormsg("jshell.err.unexpected.exception", ex);
        } finally {
            input = oldInput;
        }
    }

    private void addToReplayHistory(String s) {
        if (isRunningInteractive()) {
            replayableHistory.add(s);
        }
    }

    private String processSourceCatchingReset(String src) {
        try {
            input.beforeUserCode();
            return processSource(src);
        } catch (IllegalStateException ex) {
            hard("Resetting...");
            live = false; // Make double sure
            return "";
        } finally {
            input.afterUserCode();
        }
    }

    private void processCommand(String cmd) {
        if (cmd.startsWith("/-")) {
            try {
                //handle "/-[number]"
                cmdUseHistoryEntry(Integer.parseInt(cmd.substring(1)));
                return ;
            } catch (NumberFormatException ex) {
                //ignore
            }
        }
        String arg = "";
        int idx = cmd.indexOf(' ');
        if (idx > 0) {
            arg = cmd.substring(idx + 1).trim();
            cmd = cmd.substring(0, idx);
        }
        Command[] candidates = findCommand(cmd, c -> c.kind.isRealCommand);
        switch (candidates.length) {
            case 0:
                if (!rerunHistoryEntryById(cmd.substring(1))) {
                    errormsg("jshell.err.no.such.command.or.snippet.id", cmd);
                    fluffmsg("jshell.msg.help.for.help");
                }   break;
            case 1:
                Command command = candidates[0];
                // If comand was successful and is of a replayable kind, add it the replayable history
                if (command.run.apply(arg) && command.kind == CommandKind.REPLAY) {
                    addToReplayHistory((command.command + " " + arg).trim());
                }   break;
            default:
                errormsg("jshell.err.command.ambiguous", cmd,
                        Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", ")));
                fluffmsg("jshell.msg.help.for.help");
                break;
        }
    }

    private Command[] findCommand(String cmd, Predicate<Command> filter) {
        Command exact = commands.get(cmd);
        if (exact != null)
            return new Command[] {exact};

        return commands.values()
                       .stream()
                       .filter(filter)
                       .filter(command -> command.command.startsWith(cmd))
                       .toArray(size -> new Command[size]);
    }

    private static Path toPathResolvingUserHome(String pathString) {
        if (pathString.replace(File.separatorChar, '/').startsWith("~/"))
            return Paths.get(System.getProperty("user.home"), pathString.substring(2));
        else
            return Paths.get(pathString);
    }

    static final class Command {
        public final String command;
        public final String helpKey;
        public final Function<String,Boolean> run;
        public final CompletionProvider completions;
        public final CommandKind kind;

        // NORMAL Commands
        public Command(String command, Function<String,Boolean> run, CompletionProvider completions) {
            this(command, run, completions, CommandKind.NORMAL);
        }

        // Special kinds of Commands
        public Command(String command, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
            this(command, "help." + command.substring(1),
                    run, completions, kind);
        }

        // Documentation pseudo-commands
        public Command(String command, String helpKey, CommandKind kind) {
            this(command, helpKey,
                    arg -> { throw new IllegalStateException(); },
                    EMPTY_COMPLETION_PROVIDER,
                    kind);
        }

        public Command(String command, String helpKey, Function<String,Boolean> run, CompletionProvider completions, CommandKind kind) {
            this.command = command;
            this.helpKey = helpKey;
            this.run = run;
            this.completions = completions;
            this.kind = kind;
        }

    }

    interface CompletionProvider {
        List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor);

    }

    enum CommandKind {
        NORMAL(true, true, true),
        REPLAY(true, true, true),
        HIDDEN(true, false, false),
        HELP_ONLY(false, true, false),
        HELP_SUBJECT(false, false, false);

        final boolean isRealCommand;
        final boolean showInHelp;
        final boolean shouldSuggestCompletions;
        private CommandKind(boolean isRealCommand, boolean showInHelp, boolean shouldSuggestCompletions) {
            this.isRealCommand = isRealCommand;
            this.showInHelp = showInHelp;
            this.shouldSuggestCompletions = shouldSuggestCompletions;
        }
    }

    static final class FixedCompletionProvider implements CompletionProvider {

        private final String[] alternatives;

        public FixedCompletionProvider(String... alternatives) {
            this.alternatives = alternatives;
        }

        @Override
        public List<Suggestion> completionSuggestions(String input, int cursor, int[] anchor) {
            List<Suggestion> result = new ArrayList<>();

            for (String alternative : alternatives) {
                if (alternative.startsWith(input)) {
                    result.add(new ArgSuggestion(alternative));
                }
            }

            anchor[0] = 0;

            return result;
        }

    }

    static final CompletionProvider EMPTY_COMPLETION_PROVIDER = new FixedCompletionProvider();
    private static final CompletionProvider KEYWORD_COMPLETION_PROVIDER = new FixedCompletionProvider("-all ", "-start ", "-history ");
    private static final CompletionProvider RELOAD_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-restore", "-quiet");
    private static final CompletionProvider SET_MODE_OPTIONS_COMPLETION_PROVIDER = new FixedCompletionProvider("-command", "-quiet", "-delete");
    private static final CompletionProvider FILE_COMPLETION_PROVIDER = fileCompletions(p -> true);
    private final Map<String, Command> commands = new LinkedHashMap<>();
    private void registerCommand(Command cmd) {
        commands.put(cmd.command, cmd);
    }

    private static CompletionProvider skipWordThenCompletion(CompletionProvider completionProvider) {
        return (input, cursor, anchor) -> {
            List<Suggestion> result = Collections.emptyList();

            int space = input.indexOf(' ');
            if (space != -1) {
                String rest = input.substring(space + 1);
                result = completionProvider.completionSuggestions(rest, cursor - space - 1, anchor);
                anchor[0] += space + 1;
            }

            return result;
        };
    }

    private static CompletionProvider fileCompletions(Predicate<Path> accept) {
        return (code, cursor, anchor) -> {
            int lastSlash = code.lastIndexOf('/');
            String path = code.substring(0, lastSlash + 1);
            String prefix = lastSlash != (-1) ? code.substring(lastSlash + 1) : code;
            Path current = toPathResolvingUserHome(path);
            List<Suggestion> result = new ArrayList<>();
            try (Stream<Path> dir = Files.list(current)) {
                dir.filter(f -> accept.test(f) && f.getFileName().toString().startsWith(prefix))
                   .map(f -> new ArgSuggestion(f.getFileName() + (Files.isDirectory(f) ? "/" : "")))
                   .forEach(result::add);
            } catch (IOException ex) {
                //ignore...
            }
            if (path.isEmpty()) {
                StreamSupport.stream(FileSystems.getDefault().getRootDirectories().spliterator(), false)
                             .filter(root -> accept.test(root) && root.toString().startsWith(prefix))
                             .map(root -> new ArgSuggestion(root.toString()))
                             .forEach(result::add);
            }
            anchor[0] = path.length();
            return result;
        };
    }

    private static CompletionProvider classPathCompletion() {
        return fileCompletions(p -> Files.isDirectory(p) ||
                                    p.getFileName().toString().endsWith(".zip") ||
                                    p.getFileName().toString().endsWith(".jar"));
    }

    private CompletionProvider snippetCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) {
        return (prefix, cursor, anchor) -> {
            anchor[0] = 0;
            return snippetsSupplier.get()
                        .flatMap(k -> (k instanceof DeclarationSnippet)
                                ? Stream.of(String.valueOf(k.id()), ((DeclarationSnippet) k).name())
                                : Stream.of(String.valueOf(k.id())))
                        .filter(k -> k.startsWith(prefix))
                        .map(k -> new ArgSuggestion(k))
                        .collect(Collectors.toList());
        };
    }

    private CompletionProvider snippetKeywordCompletion(Supplier<Stream<? extends Snippet>> snippetsSupplier) {
        return (code, cursor, anchor) -> {
            List<Suggestion> result = new ArrayList<>();
            result.addAll(KEYWORD_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor));
            result.addAll(snippetCompletion(snippetsSupplier).completionSuggestions(code, cursor, anchor));
            return result;
        };
    }

    private static CompletionProvider saveCompletion() {
        return (code, cursor, anchor) -> {
            List<Suggestion> result = new ArrayList<>();
            int space = code.indexOf(' ');
            if (space == (-1)) {
                result.addAll(KEYWORD_COMPLETION_PROVIDER.completionSuggestions(code, cursor, anchor));
            }
            result.addAll(FILE_COMPLETION_PROVIDER.completionSuggestions(code.substring(space + 1), cursor - space - 1, anchor));
            anchor[0] += space + 1;
            return result;
        };
    }

    private static CompletionProvider reloadCompletion() {
        return (code, cursor, anchor) -> {
            List<Suggestion> result = new ArrayList<>();
            int pastSpace = code.indexOf(' ') + 1; // zero if no space
            result.addAll(RELOAD_OPTIONS_COMPLETION_PROVIDER.completionSuggestions(code.substring(pastSpace), cursor - pastSpace, anchor));
            anchor[0] += pastSpace;
            return result;
        };
    }

    private static CompletionProvider orMostSpecificCompletion(
            CompletionProvider left, CompletionProvider right) {
        return (code, cursor, anchor) -> {
            int[] leftAnchor = {-1};
            int[] rightAnchor = {-1};

            List<Suggestion> leftSuggestions = left.completionSuggestions(code, cursor, leftAnchor);
            List<Suggestion> rightSuggestions = right.completionSuggestions(code, cursor, rightAnchor);

            List<Suggestion> suggestions = new ArrayList<>();

            if (leftAnchor[0] >= rightAnchor[0]) {
                anchor[0] = leftAnchor[0];
                suggestions.addAll(leftSuggestions);
            }

            if (leftAnchor[0] <= rightAnchor[0]) {
                anchor[0] = rightAnchor[0];
                suggestions.addAll(rightSuggestions);
            }

            return suggestions;
        };
    }

    // Snippet lists

    Stream<Snippet> allSnippets() {
        return state.snippets();
    }

    Stream<PersistentSnippet> dropableSnippets() {
        return state.snippets()
                .filter(sn -> state.status(sn).isActive() && sn instanceof PersistentSnippet)
                .map(sn -> (PersistentSnippet) sn);
    }

    Stream<VarSnippet> allVarSnippets() {
        return state.snippets()
                .filter(sn -> sn.kind() == Snippet.Kind.VAR)
                .map(sn -> (VarSnippet) sn);
    }

    Stream<MethodSnippet> allMethodSnippets() {
        return state.snippets()
                .filter(sn -> sn.kind() == Snippet.Kind.METHOD)
                .map(sn -> (MethodSnippet) sn);
    }

    Stream<TypeDeclSnippet> allTypeSnippets() {
        return state.snippets()
                .filter(sn -> sn.kind() == Snippet.Kind.TYPE_DECL)
                .map(sn -> (TypeDeclSnippet) sn);
    }

    // Table of commands -- with command forms, argument kinds, helpKey message, implementation, ...

    {
        registerCommand(new Command("/list",
                arg -> cmdList(arg),
                snippetKeywordCompletion(this::allSnippets)));
        registerCommand(new Command("/edit",
                arg -> cmdEdit(arg),
                snippetCompletion(this::allSnippets)));
        registerCommand(new Command("/drop",
                arg -> cmdDrop(arg),
                snippetCompletion(this::dropableSnippets),
                CommandKind.REPLAY));
        registerCommand(new Command("/save",
                arg -> cmdSave(arg),
                saveCompletion()));
        registerCommand(new Command("/open",
                arg -> cmdOpen(arg),
                FILE_COMPLETION_PROVIDER));
        registerCommand(new Command("/vars",
                arg -> cmdVars(arg),
                snippetKeywordCompletion(this::allVarSnippets)));
        registerCommand(new Command("/methods",
                arg -> cmdMethods(arg),
                snippetKeywordCompletion(this::allMethodSnippets)));
        registerCommand(new Command("/types",
                arg -> cmdTypes(arg),
                snippetKeywordCompletion(this::allTypeSnippets)));
        registerCommand(new Command("/imports",
                arg -> cmdImports(),
                EMPTY_COMPLETION_PROVIDER));
        registerCommand(new Command("/exit",
                arg -> cmdExit(),
                EMPTY_COMPLETION_PROVIDER));
        registerCommand(new Command("/reset",
                arg -> cmdReset(),
                EMPTY_COMPLETION_PROVIDER));
        registerCommand(new Command("/reload",
                arg -> cmdReload(arg),
                reloadCompletion()));
        registerCommand(new Command("/classpath",
                arg -> cmdClasspath(arg),
                classPathCompletion(),
                CommandKind.REPLAY));
        registerCommand(new Command("/history",
                arg -> cmdHistory(),
                EMPTY_COMPLETION_PROVIDER));
        registerCommand(new Command("/debug",
                arg -> cmdDebug(arg),
                EMPTY_COMPLETION_PROVIDER,
                CommandKind.HIDDEN));
        registerCommand(new Command("/help",
                arg -> cmdHelp(arg),
                EMPTY_COMPLETION_PROVIDER));
        registerCommand(new Command("/set",
                arg -> cmdSet(arg),
                new ContinuousCompletionProvider(Map.of(
                        // need more completion for format for usability
                        "format", feedback.modeCompletions(),
                        "truncation", feedback.modeCompletions(),
                        "feedback", feedback.modeCompletions(),
                        "mode", skipWordThenCompletion(orMostSpecificCompletion(
                                feedback.modeCompletions(SET_MODE_OPTIONS_COMPLETION_PROVIDER),
                                SET_MODE_OPTIONS_COMPLETION_PROVIDER)),
                        "prompt", feedback.modeCompletions(),
                        "editor", fileCompletions(Files::isExecutable),
                        "start", FILE_COMPLETION_PROVIDER),
                        STARTSWITH_MATCHER)));
        registerCommand(new Command("/retain",
                arg -> cmdRetain(arg),
                new ContinuousCompletionProvider(Map.of(
                        "feedback", feedback.modeCompletions(),
                        "mode", feedback.modeCompletions(),
                        "editor", fileCompletions(Files::isExecutable),
                        "start", FILE_COMPLETION_PROVIDER),
                        STARTSWITH_MATCHER)));
        registerCommand(new Command("/?",
                "help.quest",
                arg -> cmdHelp(arg),
                EMPTY_COMPLETION_PROVIDER,
                CommandKind.NORMAL));
        registerCommand(new Command("/!",
                "help.bang",
                arg -> cmdUseHistoryEntry(-1),
                EMPTY_COMPLETION_PROVIDER,
                CommandKind.NORMAL));

        // Documentation pseudo-commands
        registerCommand(new Command("/<id>",
                "help.id",
                CommandKind.HELP_ONLY));
        registerCommand(new Command("/-<n>",
                "help.previous",
                CommandKind.HELP_ONLY));
        registerCommand(new Command("intro",
                "help.intro",
                CommandKind.HELP_SUBJECT));
        registerCommand(new Command("shortcuts",
                "help.shortcuts",
                CommandKind.HELP_SUBJECT));

        commandCompletions = new ContinuousCompletionProvider(
                commands.values().stream()
                        .filter(c -> c.kind.shouldSuggestCompletions)
                        .collect(toMap(c -> c.command, c -> c.completions)),
                STARTSWITH_MATCHER);
    }

    private ContinuousCompletionProvider commandCompletions;

    public List<Suggestion> commandCompletionSuggestions(String code, int cursor, int[] anchor) {
        return commandCompletions.completionSuggestions(code, cursor, anchor);
    }

    public String commandDocumentation(String code, int cursor) {
        code = code.substring(0, cursor);
        int space = code.indexOf(' ');

        if (space != (-1)) {
            String cmd = code.substring(0, space);
            Command command = commands.get(cmd);
            if (command != null) {
                return getResourceString(command.helpKey + ".summary");
            }
        }

        return null;
    }

    // --- Command implementations ---

    private static final String[] SET_SUBCOMMANDS = new String[]{
        "format", "truncation", "feedback", "mode", "prompt", "editor", "start"};

    private static final String[] RETAIN_SUBCOMMANDS = new String[]{
        "feedback", "mode", "editor", "start"};

    final boolean cmdSet(String arg) {
        String cmd = "/set";
        ArgTokenizer at = new ArgTokenizer(cmd, arg.trim());
        String which = subCommand(cmd, at, SET_SUBCOMMANDS);
        if (which == null) {
            return false;
        }
        switch (which) {
            case "format":
                return feedback.setFormat(this, at);
            case "truncation":
                return feedback.setTruncation(this, at);
            case "feedback":
                return feedback.setFeedback(this, at);
            case "mode":
                return feedback.setMode(this, at);
            case "prompt":
                return feedback.setPrompt(this, at);
            case "editor":
                return setEditor(at, true);
            case "start":
                return setStart(cmd, at, true);
            default:
                errormsg("jshell.err.arg", cmd, at.val());
                return false;
        }
    }

    final boolean cmdRetain(String arg) {
        String cmd = "/retain";
        ArgTokenizer at = new ArgTokenizer(cmd, arg.trim());
        String which = subCommand(cmd, at, RETAIN_SUBCOMMANDS);
        if (which == null) {
            return false;
        }
        switch (which) {
            case "feedback": {
                String fb = feedback.retainFeedback(this, at);
                if (fb != null) {
                    // If a feedback mode has been set now, or in the past, retain it
                    prefs.put(FEEDBACK_KEY, fb);
                    return true;
                }
                return false;
            }
            case "mode":
                String retained = feedback.retainMode(this, at);
                if (retained != null) {
                    // Retain this mode and all previously retained modes
                    prefs.put(MODE_KEY, retained);
                    return true;
                }
                return false;
            case "editor":
                if (!setEditor(at, false)) {
                    return false;
                }
                // retain editor setting
                prefs.put(EDITOR_KEY, (editor == null)
                        ? ""
                        : (editorWait? "-" : "*") + String.join(RECORD_SEPARATOR, editor));
                return true;
            case "start": {
                if (!setStart(cmd, at, false)) {
                    return false;
                }
                // retain startup setting
                prefs.put(STARTUP_KEY, startup);
                return true;
            }
            default:
                errormsg("jshell.err.arg", cmd, at.val());
                return false;
        }
    }

    // Print the help doc for the specified sub-command
    boolean printSubCommandHelp(String cmd, ArgTokenizer at, String helpPrefix, String[] subs) {
        String which = subCommand(cmd, at, subs);
        if (which == null) {
            return false;
        }
        hardrb(helpPrefix + which);
        return true;
    }

    // Find which, if any, sub-command matches
    String subCommand(String cmd, ArgTokenizer at, String[] subs) {
        String[] matches = at.next(subs);
        if (matches == null) {
            // No sub-command was given
            errormsg("jshell.err.sub.arg", cmd);
            return null;
        }
        if (matches.length == 0) {
            // There are no matching sub-commands
            errormsg("jshell.err.arg", cmd, at.val());
            fluffmsg("jshell.msg.use.one.of", Arrays.stream(subs)
                    .collect(Collectors.joining(", "))
            );
            return null;
        }
        if (matches.length > 1) {
            // More than one sub-command matches the initial characters provided
            errormsg("jshell.err.sub.ambiguous", cmd, at.val());
            fluffmsg("jshell.msg.use.one.of", Arrays.stream(matches)
                    .collect(Collectors.joining(", "))
            );
            return null;
        }
        return matches[0];
    }

    // The sub-command:  /set editor <editor-command-line>>
    boolean setEditor(ArgTokenizer at, boolean argsRequired) {
        at.allowedOptions("-default", "-wait");
        String prog = at.next();
        List<String> ed = new ArrayList<>();
        while (at.val() != null) {
            ed.add(at.val());
            at.nextToken();
        }
        if (!checkOptionsAndRemainingInput(at)) {
            return false;
        }
        boolean defaultOption = at.hasOption("-default");
        boolean waitOption = at.hasOption("-wait");
        if (prog != null) {
            if (defaultOption) {
                errormsg("jshell.err.default.option.or.program", at.whole());
                return false;
            }
            editor = ed.toArray(new String[ed.size()]);
            editorWait = waitOption;
            fluffmsg("jshell.msg.set.editor.set", prog);
        } else if (defaultOption) {
            if (waitOption) {
                errormsg("jshell.err.wait.applies.to.external.editor", at.whole());
                return false;
            }
            editor = null;
        } else if (argsRequired) {
            errormsg("jshell.err.set.editor.arg");
            return false;
        }
        return true;
    }

    // The sub-command:  /set start <start-file>
    boolean setStart(String cmd, ArgTokenizer at, boolean argsRequired) {
        at.allowedOptions("-default", "-none");
        String fn = at.next();
        if (!checkOptionsAndRemainingInput(at)) {
            return false;
        }
        int argCount = at.optionCount() + ((fn != null) ? 1 : 0);
        if (argCount > 1 || argsRequired && argCount == 0) {
            errormsg("jshell.err.option.or.filename", at.whole());
            return false;
        }
        if (fn != null) {
            String init = readFile(fn, cmd + " start");
            if (init == null) {
                return false;
            } else {
                startup = init;
                return true;
            }
        } else if (at.hasOption("-default")) {
            startup = DEFAULT_STARTUP;
        } else if (at.hasOption("-none")) {
            startup = "";
        }
        return true;
    }

    boolean cmdClasspath(String arg) {
        if (arg.isEmpty()) {
            errormsg("jshell.err.classpath.arg");
            return false;
        } else {
            state.addToClasspath(toPathResolvingUserHome(arg).toString());
            fluffmsg("jshell.msg.classpath", arg);
            return true;
        }
    }

    boolean cmdDebug(String arg) {
        if (arg.isEmpty()) {
            debug = !debug;
            InternalDebugControl.setDebugFlags(state, debug ? DBG_GEN : 0);
            fluff("Debugging %s", debug ? "on" : "off");
        } else {
            int flags = 0;
            for (char ch : arg.toCharArray()) {
                switch (ch) {
                    case '0':
                        flags = 0;
                        debug = false;
                        fluff("Debugging off");
                        break;
                    case 'r':
                        debug = true;
                        fluff("REPL tool debugging on");
                        break;
                    case 'g':
                        flags |= DBG_GEN;
                        fluff("General debugging on");
                        break;
                    case 'f':
                        flags |= DBG_FMGR;
                        fluff("File manager debugging on");
                        break;
                    case 'c':
                        flags |= DBG_COMPA;
                        fluff("Completion analysis debugging on");
                        break;
                    case 'd':
                        flags |= DBG_DEP;
                        fluff("Dependency debugging on");
                        break;
                    case 'e':
                        flags |= DBG_EVNT;
                        fluff("Event debugging on");
                        break;
                    default:
                        hard("Unknown debugging option: %c", ch);
                        fluff("Use: 0 r g f c d");
                        return false;
                }
            }
            InternalDebugControl.setDebugFlags(state, flags);
        }
        return true;
    }

    private boolean cmdExit() {
        regenerateOnDeath = false;
        live = false;
        if (!replayableHistory.isEmpty()) {
            // Prevent history overflow by calculating what will fit, starting
            // with most recent
            int sepLen = RECORD_SEPARATOR.length();
            int length = 0;
            int first = replayableHistory.size();
            while(length < Preferences.MAX_VALUE_LENGTH && --first >= 0) {
                length += replayableHistory.get(first).length() + sepLen;
            }
            String hist =  String.join(RECORD_SEPARATOR,
                    replayableHistory.subList(first + 1, replayableHistory.size()));
            prefs.put(REPLAY_RESTORE_KEY, hist);
        }
        fluffmsg("jshell.msg.goodbye");
        return true;
    }

    boolean cmdHelp(String arg) {
        ArgTokenizer at = new ArgTokenizer("/help", arg);
        String subject = at.next();
        if (subject != null) {
            Command[] matches = commands.values().stream()
                    .filter(c -> c.command.startsWith(subject))
                    .toArray(size -> new Command[size]);
            at.mark();
            String sub = at.next();
            if (sub != null && matches.length == 1) {
                String cmd = matches[0].command;
                switch (cmd) {
                    case "/set":
                        at.rewind();
                        return printSubCommandHelp(cmd, at, "help.set.", SET_SUBCOMMANDS);
                    case "/retain":
                        at.rewind();
                        return printSubCommandHelp(cmd, at, "help.retain.", RETAIN_SUBCOMMANDS);
                }
            }
            if (matches.length > 0) {
                for (Command c : matches) {
                    hard("");
                    hard("%s", c.command);
                    hard("");
                    hardrb(c.helpKey);
                }
                return true;
            } else {
                errormsg("jshell.err.help.arg", arg);
            }
        }
        hardmsg("jshell.msg.help.begin");
        hardPairs(commands.values().stream()
                .filter(cmd -> cmd.kind.showInHelp),
                cmd -> cmd.command + " " + getResourceString(cmd.helpKey + ".args"),
                cmd -> getResourceString(cmd.helpKey + ".summary")
        );
        hardmsg("jshell.msg.help.subject");
        hardPairs(commands.values().stream()
                .filter(cmd -> cmd.kind == CommandKind.HELP_SUBJECT),
                cmd -> cmd.command,
                cmd -> getResourceString(cmd.helpKey + ".summary")
        );
        return true;
    }

    private boolean cmdHistory() {
        cmdout.println();
        for (String s : input.currentSessionHistory()) {
            // No number prefix, confusing with snippet ids
            cmdout.printf("%s\n", s);
        }
        return true;
    }

    /**
     * Avoid parameterized varargs possible heap pollution warning.
     */
    private interface SnippetPredicate<T extends Snippet> extends Predicate<T> { }

    /**
     * Apply filters to a stream until one that is non-empty is found.
     * Adapted from Stuart Marks
     *
     * @param supplier Supply the Snippet stream to filter
     * @param filters Filters to attempt
     * @return The non-empty filtered Stream, or null
     */
    @SafeVarargs
    private static <T extends Snippet> Stream<T> nonEmptyStream(Supplier<Stream<T>> supplier,
            SnippetPredicate<T>... filters) {
        for (SnippetPredicate<T> filt : filters) {
            Iterator<T> iterator = supplier.get().filter(filt).iterator();
            if (iterator.hasNext()) {
                return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
            }
        }
        return null;
    }

    private boolean inStartUp(Snippet sn) {
        return mapSnippet.get(sn).space == startNamespace;
    }

    private boolean isActive(Snippet sn) {
        return state.status(sn).isActive();
    }

    private boolean mainActive(Snippet sn) {
        return !inStartUp(sn) && isActive(sn);
    }

    private boolean matchingDeclaration(Snippet sn, String name) {
        return sn instanceof DeclarationSnippet
                && ((DeclarationSnippet) sn).name().equals(name);
    }

    /**
     * Convert user arguments to a Stream of snippets referenced by those
     * arguments (or lack of arguments).
     *
     * @param snippets the base list of possible snippets
     * @param defFilter the filter to apply to the arguments if no argument
     * @param rawargs the user's argument to the command, maybe be the empty
     * string
     * @return a Stream of referenced snippets or null if no matches are found
     */
    private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier,
            Predicate<Snippet> defFilter, String rawargs, String cmd) {
        ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim());
        at.allowedOptions("-all", "-start");
        List<String> args = new ArrayList<>();
        String s;
        while ((s = at.next()) != null) {
            args.add(s);
        }
        if (!checkOptionsAndRemainingInput(at)) {
            return null;
        }
        if (at.optionCount() > 0 && args.size() > 0) {
            errormsg("jshell.err.may.not.specify.options.and.snippets", at.whole());
            return null;
        }
        if (at.optionCount() > 1) {
            errormsg("jshell.err.conflicting.options", at.whole());
            return null;
        }
        if (at.hasOption("-all")) {
            // all snippets including start-up, failed, and overwritten
            return snippetSupplier.get();
        }
        if (at.hasOption("-start")) {
            // start-up snippets
            return snippetSupplier.get()
                    .filter(this::inStartUp);
        }
        if (args.isEmpty()) {
            // Default is all active user snippets
            return snippetSupplier.get()
                    .filter(defFilter);
        }
        return argsToSnippets(snippetSupplier, args);
    }

    /**
     * Convert user arguments to a Stream of snippets referenced by those
     * arguments.
     *
     * @param snippetSupplier the base list of possible snippets
     * @param args the user's argument to the command, maybe be the empty list
     * @return a Stream of referenced snippets or null if no matches to specific
     * arg
     */
    private <T extends Snippet> Stream<T> argsToSnippets(Supplier<Stream<T>> snippetSupplier,
            List<String> args) {
        Stream<T> result = null;
        for (String arg : args) {
            // Find the best match
            Stream<T> st = layeredSnippetSearch(snippetSupplier, arg);
            if (st == null) {
                Stream<Snippet> est = layeredSnippetSearch(state::snippets, arg);
                if (est == null) {
                    errormsg("jshell.err.no.such.snippets", arg);
                } else {
                    errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command",
                            arg, est.findFirst().get().source());
                }
                return null;
            }
            if (result == null) {
                result = st;
            } else {
                result = Stream.concat(result, st);
            }
        }
        return result;
    }

    private <T extends Snippet> Stream<T> layeredSnippetSearch(Supplier<Stream<T>> snippetSupplier, String arg) {
        return nonEmptyStream(
                // the stream supplier
                snippetSupplier,
                // look for active user declarations matching the name
                sn -> isActive(sn) && matchingDeclaration(sn, arg),
                // else, look for any declarations matching the name
                sn -> matchingDeclaration(sn, arg),
                // else, look for an id of this name
                sn -> sn.id().equals(arg)
        );
    }

    private boolean cmdDrop(String rawargs) {
        ArgTokenizer at = new ArgTokenizer("/drop", rawargs.trim());
        at.allowedOptions();
        List<String> args = new ArrayList<>();
        String s;
        while ((s = at.next()) != null) {
            args.add(s);
        }
        if (!checkOptionsAndRemainingInput(at)) {
            return false;
        }
        if (args.isEmpty()) {
            errormsg("jshell.err.drop.arg");
            return false;
        }
        Stream<PersistentSnippet> stream = argsToSnippets(this::dropableSnippets, args);
        if (stream == null) {
            // Snippet not found. Error already printed
            fluffmsg("jshell.msg.see.classes.etc");
            return false;
        }
        List<PersistentSnippet> snippets = stream.collect(toList());
        if (snippets.size() > args.size()) {
            // One of the args references more thean one snippet
            errormsg("jshell.err.drop.ambiguous");
            fluffmsg("jshell.msg.use.one.of", snippets.stream()
                    .map(sn -> String.format("\n/drop %-5s :   %s", sn.id(), sn.source().replace("\n", "\n       ")))
                    .collect(Collectors.joining(", "))
            );
            return false;
        }
        snippets.stream()
                .forEach(sn -> state.drop(sn).forEach(this::handleEvent));
        return true;
    }

    private boolean cmdEdit(String arg) {
        Stream<Snippet> stream = argsOptionsToSnippets(state::snippets,
                this::mainActive, arg, "/edit");
        if (stream == null) {
            return false;
        }
        Set<String> srcSet = new LinkedHashSet<>();
        stream.forEachOrdered(sn -> {
            String src = sn.source();
            switch (sn.subKind()) {
                case VAR_VALUE_SUBKIND:
                    break;
                case ASSIGNMENT_SUBKIND:
                case OTHER_EXPRESSION_SUBKIND:
                case TEMP_VAR_EXPRESSION_SUBKIND:
                    if (!src.endsWith(";")) {
                        src = src + ";";
                    }
                    srcSet.add(src);
                    break;
                default:
                    srcSet.add(src);
                    break;
            }
        });
        StringBuilder sb = new StringBuilder();
        for (String s : srcSet) {
            sb.append(s);
            sb.append('\n');
        }
        String src = sb.toString();
        Consumer<String> saveHandler = new SaveHandler(src, srcSet);
        Consumer<String> errorHandler = s -> hard("Edit Error: %s", s);
        if (editor == null) {
            try {
                EditPad.edit(errorHandler, src, saveHandler);
            } catch (RuntimeException ex) {
                errormsg("jshell.err.cant.launch.editor", ex);
                fluffmsg("jshell.msg.try.set.editor");
                return false;
            }
        } else {
            ExternalEditor.edit(editor, errorHandler, src, saveHandler, input,
                    editorWait, this::hardrb);
        }
        return true;
    }
    //where
    // receives editor requests to save
    private class SaveHandler implements Consumer<String> {

        String src;
        Set<String> currSrcs;

        SaveHandler(String src, Set<String> ss) {
            this.src = src;
            this.currSrcs = ss;
        }

        @Override
        public void accept(String s) {
            if (!s.equals(src)) { // quick check first
                src = s;
                try {
                    Set<String> nextSrcs = new LinkedHashSet<>();
                    boolean failed = false;
                    while (true) {
                        CompletionInfo an = analysis.analyzeCompletion(s);
                        if (!an.completeness().isComplete()) {
                            break;
                        }
                        String tsrc = trimNewlines(an.source());
                        if (!failed && !currSrcs.contains(tsrc)) {
                            failed = processCompleteSource(tsrc);
                        }
                        nextSrcs.add(tsrc);
                        if (an.remaining().isEmpty()) {
                            break;
                        }
                        s = an.remaining();
                    }
                    currSrcs = nextSrcs;
                } catch (IllegalStateException ex) {
                    hardmsg("jshell.msg.resetting");
                    resetState();
                    currSrcs = new LinkedHashSet<>(); // re-process everything
                }
            }
        }

        private String trimNewlines(String s) {
            int b = 0;
            while (b < s.length() && s.charAt(b) == '\n') {
                ++b;
            }
            int e = s.length() -1;
            while (e >= 0 && s.charAt(e) == '\n') {
                --e;
            }
            return s.substring(b, e + 1);
        }
    }

    private boolean cmdList(String arg) {
        if (arg.length() >= 2 && "-history".startsWith(arg)) {
            return cmdHistory();
        }
        Stream<Snippet> stream = argsOptionsToSnippets(state::snippets,
                this::mainActive, arg, "/list");
        if (stream == null) {
            return false;
        }

        // prevent double newline on empty list
        boolean[] hasOutput = new boolean[1];
        stream.forEachOrdered(sn -> {
            if (!hasOutput[0]) {
                cmdout.println();
                hasOutput[0] = true;
            }
            cmdout.printf("%4s : %s\n", sn.id(), sn.source().replace("\n", "\n       "));
        });
        return true;
    }

    private boolean cmdOpen(String filename) {
        return runFile(filename, "/open");
    }

    private boolean runFile(String filename, String context) {
        if (!filename.isEmpty()) {
            try {
                run(new FileScannerIOContext(toPathResolvingUserHome(filename).toString()));
                return true;
            } catch (FileNotFoundException e) {
                errormsg("jshell.err.file.not.found", context, filename, e.getMessage());
            } catch (Exception e) {
                errormsg("jshell.err.file.exception", context, filename, e);
            }
        } else {
            errormsg("jshell.err.file.filename", context);
        }
        return false;
    }

    /**
     * Read an external file. Error messages accessed via keyPrefix
     *
     * @param filename file to access or null
     * @param context printable non-natural language context for errors
     * @return contents of file as string
     */
    String readFile(String filename, String context) {
        if (filename != null) {
            try {
                byte[] encoded = Files.readAllBytes(Paths.get(filename));
                return new String(encoded);
            } catch (AccessDeniedException e) {
                errormsg("jshell.err.file.not.accessible", context, filename, e.getMessage());
            } catch (NoSuchFileException e) {
                errormsg("jshell.err.file.not.found", context, filename);
            } catch (Exception e) {
                errormsg("jshell.err.file.exception", context, filename, e);
            }
        } else {
            errormsg("jshell.err.file.filename", context);
        }
        return null;

    }

    private boolean cmdReset() {
        live = false;
        fluffmsg("jshell.msg.resetting.state");
        return true;
    }

    private boolean cmdReload(String rawargs) {
        ArgTokenizer at = new ArgTokenizer("/reload", rawargs.trim());
        at.allowedOptions("-restore", "-quiet");
        if (!checkOptionsAndRemainingInput(at)) {
            return false;
        }
        Iterable<String> history;
        if (at.hasOption("-restore")) {
            if (replayableHistoryPrevious == null) {
                errormsg("jshell.err.reload.no.previous");
                return false;
            }
            history = replayableHistoryPrevious;
            fluffmsg("jshell.err.reload.restarting.previous.state");
        } else {
            history = replayableHistory;
            fluffmsg("jshell.err.reload.restarting.state");
        }
        boolean echo = !at.hasOption("-quiet");
        resetState();
        run(new ReloadIOContext(history,
                echo ? cmdout : null));
        return true;
    }

    private boolean cmdSave(String rawargs) {
        ArgTokenizer at = new ArgTokenizer("/save", rawargs.trim());
        at.allowedOptions("-all", "-start", "-history");
        String filename = at.next();
        if (filename == null) {
            errormsg("jshell.err.file.filename", "/save");
            return false;
        }
        if (!checkOptionsAndRemainingInput(at)) {
            return false;
        }
        if (at.optionCount() > 1) {
            errormsg("jshell.err.conflicting.options", at.whole());
            return false;
        }
        try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename),
                Charset.defaultCharset(),
                CREATE, TRUNCATE_EXISTING, WRITE)) {
            if (at.hasOption("-history")) {
                for (String s : input.currentSessionHistory()) {
                    writer.write(s);
                    writer.write("\n");
                }
            } else if (at.hasOption("-start")) {
                writer.append(startup);
            } else {
                String sources = (at.hasOption("-all")
                        ? state.snippets()
                        : state.snippets().filter(this::mainActive))
                        .map(Snippet::source)
                        .collect(Collectors.joining("\n"));
                writer.write(sources);
            }
        } catch (FileNotFoundException e) {
            errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage());
            return false;
        } catch (Exception e) {
            errormsg("jshell.err.file.exception", "/save", filename, e);
            return false;
        }
        return true;
    }

    private boolean cmdVars(String arg) {
        Stream<VarSnippet> stream = argsOptionsToSnippets(this::allVarSnippets,
                this::isActive, arg, "/vars");
        if (stream == null) {
            return false;
        }
        stream.forEachOrdered(vk ->
        {
            String val = state.status(vk) == Status.VALID
                    ? state.varValue(vk)
                    : getResourceString("jshell.msg.vars.not.active");
            hard("  %s %s = %s", vk.typeName(), vk.name(), val);
        });
        return true;
    }

    private boolean cmdMethods(String arg) {
        Stream<MethodSnippet> stream = argsOptionsToSnippets(this::allMethodSnippets,
                this::isActive, arg, "/methods");
        if (stream == null) {
            return false;
        }
        stream.forEachOrdered(mk
                -> hard("  %s %s", mk.name(), mk.signature())
        );
        return true;
    }

    private boolean cmdTypes(String arg) {
        Stream<TypeDeclSnippet> stream = argsOptionsToSnippets(this::allTypeSnippets,
                this::isActive, arg, "/types");
        if (stream == null) {
            return false;
        }
        stream.forEachOrdered(ck
        -> {
            String kind;
            switch (ck.subKind()) {
                case INTERFACE_SUBKIND:
                    kind = "interface";
                    break;
                case CLASS_SUBKIND:
                    kind = "class";
                    break;
                case ENUM_SUBKIND:
                    kind = "enum";
                    break;
                case ANNOTATION_TYPE_SUBKIND:
                    kind = "@interface";
                    break;
                default:
                    assert false : "Wrong kind" + ck.subKind();
                    kind = "class";
                    break;
            }
            hard("  %s %s", kind, ck.name());
        });
        return true;
    }

    private boolean cmdImports() {
        state.imports().forEach(ik -> {
            hard("  import %s%s", ik.isStatic() ? "static " : "", ik.fullname());
        });
        return true;
    }

    private boolean cmdUseHistoryEntry(int index) {
        List<Snippet> keys = state.snippets().collect(toList());
        if (index < 0)
            index += keys.size();
        else
            index--;
        if (index >= 0 && index < keys.size()) {
            rerunSnippet(keys.get(index));
        } else {
            errormsg("jshell.err.out.of.range");
            return false;
        }
        return true;
    }

    boolean checkOptionsAndRemainingInput(ArgTokenizer at) {
        String junk = at.remainder();
        if (!junk.isEmpty()) {
            errormsg("jshell.err.unexpected.at.end", junk, at.whole());
            return false;
        } else {
            String bad = at.badOptions();
            if (!bad.isEmpty()) {
                errormsg("jshell.err.unknown.option", bad, at.whole());
                return false;
            }
        }
        return true;
    }

    private boolean rerunHistoryEntryById(String id) {
        Optional<Snippet> snippet = state.snippets()
            .filter(s -> s.id().equals(id))
            .findFirst();
        return snippet.map(s -> {
            rerunSnippet(s);
            return true;
        }).orElse(false);
    }

    private void rerunSnippet(Snippet snippet) {
        String source = snippet.source();
        cmdout.printf("%s\n", source);
        input.replaceLastHistoryEntry(source);
        processSourceCatchingReset(source);
    }

    /**
     * Filter diagnostics for only errors (no warnings, ...)
     * @param diagnostics input list
     * @return filtered list
     */
    List<Diag> errorsOnly(List<Diag> diagnostics) {
        return diagnostics.stream()
                .filter(d -> d.isError())
                .collect(toList());
    }

    void displayDiagnostics(String source, Diag diag, List<String> toDisplay) {
        for (String line : diag.getMessage(null).split("\\r?\\n")) { // TODO: Internationalize
            if (!line.trim().startsWith("location:")) {
                toDisplay.add(line);
            }
        }

        int pstart = (int) diag.getStartPosition();
        int pend = (int) diag.getEndPosition();
        Matcher m = LINEBREAK.matcher(source);
        int pstartl = 0;
        int pendl = -2;
        while (m.find(pstartl)) {
            pendl = m.start();
            if (pendl >= pstart) {
                break;
            } else {
                pstartl = m.end();
            }
        }
        if (pendl < pstart) {
            pendl = source.length();
        }
        toDisplay.add(source.substring(pstartl, pendl));

        StringBuilder sb = new StringBuilder();
        int start = pstart - pstartl;
        for (int i = 0; i < start; ++i) {
            sb.append(' ');
        }
        sb.append('^');
        boolean multiline = pend > pendl;
        int end = (multiline ? pendl : pend) - pstartl - 1;
        if (end > start) {
            for (int i = start + 1; i < end; ++i) {
                sb.append('-');
            }
            if (multiline) {
                sb.append("-...");
            } else {
                sb.append('^');
            }
        }
        toDisplay.add(sb.toString());

        debug("printDiagnostics start-pos = %d ==> %d -- wrap = %s", diag.getStartPosition(), start, this);
        debug("Code: %s", diag.getCode());
        debug("Pos: %d (%d - %d)", diag.getPosition(),
                diag.getStartPosition(), diag.getEndPosition());
    }

    private String processSource(String srcInput) throws IllegalStateException {
        while (true) {
            CompletionInfo an = analysis.analyzeCompletion(srcInput);
            if (!an.completeness().isComplete()) {
                return an.remaining();
            }
            boolean failed = processCompleteSource(an.source());
            if (failed || an.remaining().isEmpty()) {
                return "";
            }
            srcInput = an.remaining();
        }
    }
    //where
    private boolean processCompleteSource(String source) throws IllegalStateException {
        debug("Compiling: %s", source);
        boolean failed = false;
        boolean isActive = false;
        List<SnippetEvent> events = state.eval(source);
        for (SnippetEvent e : events) {
            // Report the event, recording failure
            failed |= handleEvent(e);

            // If any main snippet is active, this should be replayable
            // also ignore var value queries
            isActive |= e.causeSnippet() == null &&
                    e.status().isActive() &&
                    e.snippet().subKind() != VAR_VALUE_SUBKIND;
        }
        // If this is an active snippet and it didn't cause the backend to die,
        // add it to the replayable history
        if (isActive && live) {
            addToReplayHistory(source);
        }

        return failed;
    }

    // Handle incoming snippet events -- return true on failure
    private boolean handleEvent(SnippetEvent ste) {
        Snippet sn = ste.snippet();
        if (sn == null) {
            debug("Event with null key: %s", ste);
            return false;
        }
        List<Diag> diagnostics = state.diagnostics(sn).collect(toList());
        String source = sn.source();
        if (ste.causeSnippet() == null) {
            // main event
            for (Diag d : diagnostics) {
                hardmsg(d.isError()? "jshell.msg.error" : "jshell.msg.warning");
                List<String> disp = new ArrayList<>();
                displayDiagnostics(source, d, disp);
                disp.stream()
                        .forEach(l -> hard("%s", l));
            }

            if (ste.status() != Status.REJECTED) {
                if (ste.exception() != null) {
                    if (ste.exception() instanceof EvalException) {
                        printEvalException((EvalException) ste.exception());
                        return true;
                    } else if (ste.exception() instanceof UnresolvedReferenceException) {
                        printUnresolvedException((UnresolvedReferenceException) ste.exception());
                    } else {
                        hard("Unexpected execution exception: %s", ste.exception());
                        return true;
                    }
                } else {
                    new DisplayEvent(ste, false, ste.value(), diagnostics).displayDeclarationAndValue();
                }
            } else {
                if (diagnostics.isEmpty()) {
                    errormsg("jshell.err.failed");
                }
                return true;
            }
        } else {
            // Update
            if (sn instanceof DeclarationSnippet) {
                List<Diag> other = errorsOnly(diagnostics);

                // display update information
                new DisplayEvent(ste, true, ste.value(), other).displayDeclarationAndValue();
            }
        }
        return false;
    }
    //where
    void printStackTrace(StackTraceElement[] stes) {
        for (StackTraceElement ste : stes) {
            StringBuilder sb = new StringBuilder();
            String cn = ste.getClassName();
            if (!cn.isEmpty()) {
                int dot = cn.lastIndexOf('.');
                if (dot > 0) {
                    sb.append(cn.substring(dot + 1));
                } else {
                    sb.append(cn);
                }
                sb.append(".");
            }
            if (!ste.getMethodName().isEmpty()) {
                sb.append(ste.getMethodName());
                sb.append(" ");
            }
            String fileName = ste.getFileName();
            int lineNumber = ste.getLineNumber();
            String loc = ste.isNativeMethod()
                    ? getResourceString("jshell.msg.native.method")
                    : fileName == null
                            ? getResourceString("jshell.msg.unknown.source")
                            : lineNumber >= 0
                                    ? fileName + ":" + lineNumber
                                    : fileName;
            hard("      at %s(%s)", sb, loc);

        }
    }
    //where
    void printUnresolvedException(UnresolvedReferenceException ex) {
        DeclarationSnippet corralled =  ex.getSnippet();
        List<Diag> otherErrors = errorsOnly(state.diagnostics(corralled).collect(toList()));
        new DisplayEvent(corralled, state.status(corralled), FormatAction.USED, true, null, otherErrors)
                .displayDeclarationAndValue();
    }
    //where
    void printEvalException(EvalException ex) {
        if (ex.getMessage() == null) {
            hard("%s thrown", ex.getExceptionClassName());
        } else {
            hard("%s thrown: %s", ex.getExceptionClassName(), ex.getMessage());
        }
        printStackTrace(ex.getStackTrace());
    }

    private FormatAction toAction(Status status, Status previousStatus, boolean isSignatureChange) {
        FormatAction act;
        switch (status) {
            case VALID:
            case RECOVERABLE_DEFINED:
            case RECOVERABLE_NOT_DEFINED:
                if (previousStatus.isActive()) {
                    act = isSignatureChange
                            ? FormatAction.REPLACED
                            : FormatAction.MODIFIED;
                } else {
                    act = FormatAction.ADDED;
                }
                break;
            case OVERWRITTEN:
                act = FormatAction.OVERWROTE;
                break;
            case DROPPED:
                act = FormatAction.DROPPED;
                break;
            case REJECTED:
            case NONEXISTENT:
            default:
                // Should not occur
                error("Unexpected status: " + previousStatus.toString() + "=>" + status.toString());
                act = FormatAction.DROPPED;
        }
        return act;
    }

    class DisplayEvent {
        private final Snippet sn;
        private final FormatAction action;
        private final boolean update;
        private final String value;
        private final List<String> errorLines;
        private final FormatResolve resolution;
        private final String unresolved;
        private final FormatUnresolved unrcnt;
        private final FormatErrors errcnt;

        DisplayEvent(SnippetEvent ste, boolean update, String value, List<Diag> errors) {
            this(ste.snippet(), ste.status(), toAction(ste.status(), ste.previousStatus(), ste.isSignatureChange()), update, value, errors);
        }

        DisplayEvent(Snippet sn, Status status, FormatAction action, boolean update, String value, List<Diag> errors) {
            this.sn = sn;
            this.action = action;
            this.update = update;
            this.value = value;
            this.errorLines = new ArrayList<>();
            for (Diag d : errors) {
                displayDiagnostics(sn.source(), d, errorLines);
            }
            long unresolvedCount;
            if (sn instanceof DeclarationSnippet && (status == Status.RECOVERABLE_DEFINED || status == Status.RECOVERABLE_NOT_DEFINED)) {
                resolution = (status == Status.RECOVERABLE_NOT_DEFINED)
                        ? FormatResolve.NOTDEFINED
                        : FormatResolve.DEFINED;
                unresolved = unresolved((DeclarationSnippet) sn);
                unresolvedCount = state.unresolvedDependencies((DeclarationSnippet) sn).count();
            } else {
                resolution = FormatResolve.OK;
                unresolved = "";
                unresolvedCount = 0;
            }
            unrcnt = unresolvedCount == 0
                    ? FormatUnresolved.UNRESOLVED0
                    : unresolvedCount == 1
                        ? FormatUnresolved.UNRESOLVED1
                        : FormatUnresolved.UNRESOLVED2;
            errcnt = errors.isEmpty()
                    ? FormatErrors.ERROR0
                    : errors.size() == 1
                        ? FormatErrors.ERROR1
                        : FormatErrors.ERROR2;
        }

        private String unresolved(DeclarationSnippet key) {
            List<String> unr = state.unresolvedDependencies(key).collect(toList());
            StringBuilder sb = new StringBuilder();
            int fromLast = unr.size();
            if (fromLast > 0) {
                sb.append(" ");
            }
            for (String u : unr) {
                --fromLast;
                sb.append(u);
                switch (fromLast) {
                    // No suffix
                    case 0:
                        break;
                    case 1:
                        sb.append(", and ");
                        break;
                    default:
                        sb.append(", ");
                        break;
                }
            }
            return sb.toString();
        }

        private void custom(FormatCase fcase, String name) {
            custom(fcase, name, null);
        }

        private void custom(FormatCase fcase, String name, String type) {
            String display = feedback.format(fcase, action, (update ? FormatWhen.UPDATE : FormatWhen.PRIMARY),
                    resolution, unrcnt, errcnt,
                    name, type, value, unresolved, errorLines);
            if (interactive()) {
                cmdout.print(display);
            }
        }

        @SuppressWarnings("fallthrough")
        private void displayDeclarationAndValue() {
            switch (sn.subKind()) {
                case CLASS_SUBKIND:
                    custom(FormatCase.CLASS, ((TypeDeclSnippet) sn).name());
                    break;
                case INTERFACE_SUBKIND:
                    custom(FormatCase.INTERFACE, ((TypeDeclSnippet) sn).name());
                    break;
                case ENUM_SUBKIND:
                    custom(FormatCase.ENUM, ((TypeDeclSnippet) sn).name());
                    break;
                case ANNOTATION_TYPE_SUBKIND:
                    custom(FormatCase.ANNOTATION, ((TypeDeclSnippet) sn).name());
                    break;
                case METHOD_SUBKIND:
                    custom(FormatCase.METHOD, ((MethodSnippet) sn).name(), ((MethodSnippet) sn).parameterTypes());
                    break;
                case VAR_DECLARATION_SUBKIND: {
                    VarSnippet vk = (VarSnippet) sn;
                    custom(FormatCase.VARDECL, vk.name(), vk.typeName());
                    break;
                }
                case VAR_DECLARATION_WITH_INITIALIZER_SUBKIND: {
                    VarSnippet vk = (VarSnippet) sn;
                    custom(FormatCase.VARINIT, vk.name(), vk.typeName());
                    break;
                }
                case TEMP_VAR_EXPRESSION_SUBKIND: {
                    VarSnippet vk = (VarSnippet) sn;
                    custom(FormatCase.EXPRESSION, vk.name(), vk.typeName());
                    break;
                }
                case OTHER_EXPRESSION_SUBKIND:
                    error("Unexpected expression form -- value is: %s", (value));
                    break;
                case VAR_VALUE_SUBKIND: {
                    ExpressionSnippet ek = (ExpressionSnippet) sn;
                    custom(FormatCase.VARVALUE, ek.name(), ek.typeName());
                    break;
                }
                case ASSIGNMENT_SUBKIND: {
                    ExpressionSnippet ek = (ExpressionSnippet) sn;
                    custom(FormatCase.ASSIGNMENT, ek.name(), ek.typeName());
                    break;
                }
                case SINGLE_TYPE_IMPORT_SUBKIND:
                case TYPE_IMPORT_ON_DEMAND_SUBKIND:
                case SINGLE_STATIC_IMPORT_SUBKIND:
                case STATIC_IMPORT_ON_DEMAND_SUBKIND:
                    custom(FormatCase.IMPORT, ((ImportSnippet) sn).name());
                    break;
                case STATEMENT_SUBKIND:
                    custom(FormatCase.STATEMENT, null);
                    break;
            }
        }
    }

    /** The current version number as a string.
     */
    String version() {
        return version("release");  // mm.nn.oo[-milestone]
    }

    /** The current full version number as a string.
     */
    String fullVersion() {
        return version("full"); // mm.mm.oo[-milestone]-build
    }

    private String version(String key) {
        if (versionRB == null) {
            try {
                versionRB = ResourceBundle.getBundle(VERSION_RB_NAME, locale);
            } catch (MissingResourceException e) {
                return "(version info not available)";
            }
        }
        try {
            return versionRB.getString(key);
        }
        catch (MissingResourceException e) {
            return "(version info not available)";
        }
    }

    class NameSpace {
        final String spaceName;
        final String prefix;
        private int nextNum;

        NameSpace(String spaceName, String prefix) {
            this.spaceName = spaceName;
            this.prefix = prefix;
            this.nextNum = 1;
        }

        String tid(Snippet sn) {
            String tid = prefix + nextNum++;
            mapSnippet.put(sn, new SnippetInfo(sn, this, tid));
            return tid;
        }

        String tidNext() {
            return prefix + nextNum;
        }
    }

    static class SnippetInfo {
        final Snippet snippet;
        final NameSpace space;
        final String tid;

        SnippetInfo(Snippet snippet, NameSpace space, String tid) {
            this.snippet = snippet;
            this.space = space;
            this.tid = tid;
        }
    }

    static class ArgSuggestion implements Suggestion {

        private final String continuation;

        /**
         * Create a {@code Suggestion} instance.
         *
         * @param continuation a candidate continuation of the user's input
         */
        public ArgSuggestion(String continuation) {
            this.continuation = continuation;
        }

        /**
         * The candidate continuation of the given user's input.
         *
         * @return the continuation string
         */
        @Override
        public String continuation() {
            return continuation;
        }

        /**
         * Indicates whether input continuation matches the target type and is thus
         * more likely to be the desired continuation. A matching continuation is
         * preferred.
         *
         * @return {@code false}, non-types analysis
         */
        @Override
        public boolean matchesType() {
            return false;
        }
    }
}

abstract class NonInteractiveIOContext extends IOContext {

    @Override
    public boolean interactiveOutput() {
        return false;
    }

    @Override
    public Iterable<String> currentSessionHistory() {
        return Collections.emptyList();
    }

    @Override
    public boolean terminalEditorRunning() {
        return false;
    }

    @Override
    public void suspend() {
    }

    @Override
    public void resume() {
    }

    @Override
    public void beforeUserCode() {
    }

    @Override
    public void afterUserCode() {
    }

    @Override
    public void replaceLastHistoryEntry(String source) {
    }
}

class ScannerIOContext extends NonInteractiveIOContext {
    private final Scanner scannerIn;

    ScannerIOContext(Scanner scannerIn) {
        this.scannerIn = scannerIn;
    }

    @Override
    public String readLine(String prompt, String prefix) {
        if (scannerIn.hasNextLine()) {
            return scannerIn.nextLine();
        } else {
            return null;
        }
    }

    @Override
    public void close() {
        scannerIn.close();
    }

    @Override
    public int readUserInput() {
        return -1;
    }
}

class FileScannerIOContext extends ScannerIOContext {

    FileScannerIOContext(String fn) throws FileNotFoundException {
        this(new FileReader(fn));
    }

    FileScannerIOContext(Reader rdr) throws FileNotFoundException {
        super(new Scanner(rdr));
    }
}

class ReloadIOContext extends NonInteractiveIOContext {
    private final Iterator<String> it;
    private final PrintStream echoStream;

    ReloadIOContext(Iterable<String> history, PrintStream echoStream) {
        this.it = history.iterator();
        this.echoStream = echoStream;
    }

    @Override
    public String readLine(String prompt, String prefix) {
        String s = it.hasNext()
                ? it.next()
                : null;
        if (echoStream != null && s != null) {
            String p = "-: ";
            String p2 = "\n   ";
            echoStream.printf("%s%s\n", p, s.replace("\n", p2));
        }
        return s;
    }

    @Override
    public void close() {
    }

    @Override
    public int readUserInput() {
        return -1;
    }
}