langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/Feedback.java
changeset 36494 4175f47b2a50
child 36718 bf40906bf49d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/Feedback.java	Tue Mar 08 11:53:35 2016 -0800
@@ -0,0 +1,1049 @@
+/*
+ * Copyright (c) 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Feedback customization support
+ *
+ * @author Robert Field
+ */
+class Feedback {
+
+    // Patern for substituted fields within a customized format string
+    private static final Pattern FIELD_PATTERN = Pattern.compile("\\{(.*?)\\}");
+
+    // Current mode
+    private Mode mode = new Mode("", false); // initial value placeholder during start-up
+
+    // Mapping of mode names to mode modes
+    private final Map<String, Mode> modeMap = new HashMap<>();
+
+    public boolean shouldDisplayCommandFluff() {
+        return mode.commandFluff;
+    }
+
+    public String getPre() {
+        return mode.pre;
+    }
+
+    public String getPost() {
+        return mode.post;
+    }
+
+    public String getErrorPre() {
+        return mode.errorPre;
+    }
+
+    public String getErrorPost() {
+        return mode.errorPost;
+    }
+
+    public String getFormat(FormatCase fc, FormatWhen fw, FormatAction fa, FormatResolve fr,
+            boolean hasName, boolean hasType, boolean hasResult) {
+        return mode.getFormat(fc, fw, fa, fr, hasName, hasType, hasResult);
+    }
+
+    public String getPrompt(String nextId) {
+        return mode.getPrompt(nextId);
+    }
+
+    public String getContinuationPrompt(String nextId) {
+        return mode.getContinuationPrompt(nextId);
+    }
+
+    public boolean setFeedback(JShellTool tool, ArgTokenizer at) {
+        return new FormatSetter(tool, at).setFeedback();
+    }
+
+    public boolean setField(JShellTool tool, ArgTokenizer at) {
+        return new FormatSetter(tool, at).setField();
+    }
+
+    public boolean setFormat(JShellTool tool, ArgTokenizer at) {
+        return new FormatSetter(tool, at).setFormat();
+    }
+
+    public boolean setNewMode(JShellTool tool, ArgTokenizer at) {
+        return new FormatSetter(tool, at).setNewMode();
+    }
+
+    public boolean setPrompt(JShellTool tool, ArgTokenizer at) {
+        return new FormatSetter(tool, at).setPrompt();
+    }
+
+    public void printFeedbackHelp(JShellTool tool) {
+        new FormatSetter(tool, null).printFeedbackHelp();
+    }
+
+    public void printFieldHelp(JShellTool tool) {
+        new FormatSetter(tool, null).printFieldHelp();
+    }
+
+    public void printFormatHelp(JShellTool tool) {
+        new FormatSetter(tool, null).printFormatHelp();
+    }
+
+    public void printNewModeHelp(JShellTool tool) {
+        new FormatSetter(tool, null).printNewModeHelp();
+    }
+
+    public void printPromptHelp(JShellTool tool) {
+        new FormatSetter(tool, null).printPromptHelp();
+    }
+
+    /**
+     * Holds all the context of a mode mode
+     */
+    private class Mode {
+
+        // Use name of mode mode
+
+        final String name;
+
+        // Display command verification/information
+        final boolean commandFluff;
+
+        // event cases: class, method
+        final EnumMap<FormatCase, EnumMap<FormatAction, EnumMap<FormatWhen, String>>> cases;
+
+        // action names: add. modified, replaced, ...
+        final EnumMap<FormatAction, EnumMap<FormatWhen, String>> actions;
+
+        // resolution status description format with %s for unresolved
+        final EnumMap<FormatResolve, EnumMap<FormatWhen, String>> resolves;
+
+        // primary snippet vs update
+        final EnumMap<FormatWhen, String> whens;
+
+        // fixed map of how to get format string for a field, given a specific formatting contet
+        final EnumMap<FormatField, Function<Context, String>> fields;
+
+        // format wrappers for name, type, and result
+        String fname = "%s";
+        String ftype = "%s";
+        String fresult = "%s";
+
+        // start and end, also used by hard-coded output
+        String pre = "|  ";
+        String post = "\n";
+        String errorPre = "|  Error: ";
+        String errorPost = "\n";
+
+        String prompt = "\n-> ";
+        String continuationPrompt = ">> ";
+
+        /**
+         * The context of a specific mode to potentially display.
+         */
+        class Context {
+
+            final FormatCase fc;
+            final FormatAction fa;
+            final FormatResolve fr;
+            final FormatWhen fw;
+            final boolean hasName;
+            final boolean hasType;
+            final boolean hasResult;
+
+            Context(FormatCase fc, FormatWhen fw, FormatAction fa, FormatResolve fr,
+                    boolean hasName, boolean hasType, boolean hasResult) {
+                this.fc = fc;
+                this.fa = fa;
+                this.fr = fr;
+                this.fw = fw;
+                this.hasName = hasName;
+                this.hasType = hasType;
+                this.hasResult = hasResult;
+            }
+
+            String when() {
+                return whens.get(fw);
+            }
+
+            String action() {
+                return actions.get(fa).get(fw);
+            }
+
+            String resolve() {
+                return String.format(resolves.get(fr).get(fw), FormatField.RESOLVE.form);
+            }
+
+            String name() {
+                return hasName
+                        ? String.format(fname, FormatField.NAME.form)
+                        : "";
+            }
+
+            String type() {
+                return hasType
+                        ? String.format(ftype, FormatField.TYPE.form)
+                        : "";
+            }
+
+            String result() {
+                return hasResult
+                        ? String.format(fresult, FormatField.RESULT.form)
+                        : "";
+            }
+
+            /**
+             * Lookup format based on case, action, and whether it update.
+             * Replace fields with context specific formats.
+             *
+             * @return format string
+             */
+            String format() {
+                String format = cases.get(fc).get(fa).get(fw);
+                if (format == null) {
+                    return "";
+                }
+                Matcher m = FIELD_PATTERN.matcher(format);
+                StringBuffer sb = new StringBuffer(format.length());
+                while (m.find()) {
+                    String fieldName = m.group(1).toUpperCase(Locale.US);
+                    String sub = null;
+                    for (FormatField f : FormatField.values()) {
+                        if (f.name().startsWith(fieldName)) {
+                            sub = fields.get(f).apply(this);
+                            break;
+                        }
+                    }
+                    if (sub != null) {
+                        m.appendReplacement(sb, Matcher.quoteReplacement(sub));
+                    }
+                }
+                m.appendTail(sb);
+                return sb.toString();
+            }
+        }
+
+        {
+            // set fixed mappings of fields
+            fields = new EnumMap<>(FormatField.class);
+            fields.put(FormatField.WHEN, c -> c.when());
+            fields.put(FormatField.ACTION, c -> c.action());
+            fields.put(FormatField.RESOLVE, c -> c.resolve());
+            fields.put(FormatField.NAME, c -> c.name());
+            fields.put(FormatField.TYPE, c -> c.type());
+            fields.put(FormatField.RESULT, c -> c.result());
+            fields.put(FormatField.PRE, c -> pre);
+            fields.put(FormatField.POST, c -> post);
+            fields.put(FormatField.ERRORPRE, c -> errorPre);
+            fields.put(FormatField.ERRORPOST, c -> errorPost);
+        }
+
+        /**
+         * Set up an empty mode.
+         *
+         * @param name
+         * @param commandFluff True if should display command fluff messages
+         */
+        Mode(String name, boolean commandFluff) {
+            this.name = name;
+            this.commandFluff = commandFluff;
+            cases = new EnumMap<>(FormatCase.class);
+            for (FormatCase fc : FormatCase.values()) {
+                EnumMap<FormatAction, EnumMap<FormatWhen, String>> ac = new EnumMap<>(FormatAction.class);
+                cases.put(fc, ac);
+                for (FormatAction fa : FormatAction.values()) {
+                    EnumMap<FormatWhen, String> aw = new EnumMap<>(FormatWhen.class);
+                    ac.put(fa, aw);
+                    for (FormatWhen fw : FormatWhen.values()) {
+                        aw.put(fw, "");
+                    }
+                }
+            }
+
+            actions = new EnumMap<>(FormatAction.class);
+            for (FormatAction fa : FormatAction.values()) {
+                EnumMap<FormatWhen, String> afw = new EnumMap<>(FormatWhen.class);
+                actions.put(fa, afw);
+                for (FormatWhen fw : FormatWhen.values()) {
+                    afw.put(fw, fa.name() + "-" + fw.name());
+                }
+            }
+
+            resolves = new EnumMap<>(FormatResolve.class);
+            for (FormatResolve fr : FormatResolve.values()) {
+                EnumMap<FormatWhen, String> arw = new EnumMap<>(FormatWhen.class);
+                resolves.put(fr, arw);
+                for (FormatWhen fw : FormatWhen.values()) {
+                    arw.put(fw, fr.name() + "-" + fw.name() + ": %s");
+                }
+            }
+
+            whens = new EnumMap<>(FormatWhen.class);
+            for (FormatWhen fw : FormatWhen.values()) {
+                whens.put(fw, fw.name());
+            }
+        }
+
+        /**
+         * Set up a copied mode.
+         *
+         * @param name
+         * @param commandFluff True if should display command fluff messages
+         * @param m Mode to copy
+         */
+        Mode(String name, boolean commandFluff, Mode m) {
+            this.name = name;
+            this.commandFluff = commandFluff;
+            cases = new EnumMap<>(FormatCase.class);
+            for (FormatCase fc : FormatCase.values()) {
+                EnumMap<FormatAction, EnumMap<FormatWhen, String>> ac = new EnumMap<>(FormatAction.class);
+                EnumMap<FormatAction, EnumMap<FormatWhen, String>> mc = m.cases.get(fc);
+                cases.put(fc, ac);
+                for (FormatAction fa : FormatAction.values()) {
+                    EnumMap<FormatWhen, String> aw = new EnumMap<>(mc.get(fa));
+                    ac.put(fa, aw);
+                }
+            }
+
+            actions = new EnumMap<>(FormatAction.class);
+            for (FormatAction fa : FormatAction.values()) {
+                EnumMap<FormatWhen, String> afw = new EnumMap<>(m.actions.get(fa));
+                actions.put(fa, afw);
+            }
+
+            resolves = new EnumMap<>(FormatResolve.class);
+            for (FormatResolve fr : FormatResolve.values()) {
+                EnumMap<FormatWhen, String> arw = new EnumMap<>(m.resolves.get(fr));
+                resolves.put(fr, arw);
+            }
+
+            whens = new EnumMap<>(m.whens);
+
+            this.fname = m.fname;
+            this.ftype = m.ftype;
+            this.fresult = m.fresult;
+            this.pre = m.pre;
+            this.post = m.post;
+            this.errorPre = m.errorPre;
+            this.errorPost = m.errorPost;
+            this.prompt = m.prompt;
+            this.continuationPrompt = m.continuationPrompt;
+        }
+
+        String getFormat(FormatCase fc, FormatWhen fw, FormatAction fa, FormatResolve fr,
+                boolean hasName, boolean hasType, boolean hasResult) {
+            Context context = new Context(fc, fw, fa, fr,
+                    hasName, hasType, hasResult);
+            return context.format();
+        }
+
+        void setCases(String format, Collection<FormatCase> cc, Collection<FormatAction> ca, Collection<FormatWhen> cw) {
+            for (FormatCase fc : cc) {
+                EnumMap<FormatAction, EnumMap<FormatWhen, String>> ma = cases.get(fc);
+                for (FormatAction fa : ca) {
+                    EnumMap<FormatWhen, String> mw = ma.get(fa);
+                    for (FormatWhen fw : cw) {
+                        mw.put(fw, format);
+                    }
+                }
+            }
+        }
+
+        void setActions(String format, Collection<FormatAction> ca, Collection<FormatWhen> cw) {
+            for (FormatAction fa : ca) {
+                EnumMap<FormatWhen, String> mw = actions.get(fa);
+                for (FormatWhen fw : cw) {
+                    mw.put(fw, format);
+                }
+            }
+        }
+
+        void setResolves(String format, Collection<FormatResolve> cr, Collection<FormatWhen> cw) {
+            for (FormatResolve fr : cr) {
+                EnumMap<FormatWhen, String> mw = resolves.get(fr);
+                for (FormatWhen fw : cw) {
+                    mw.put(fw, format);
+                }
+            }
+        }
+
+        void setWhens(String format, Collection<FormatWhen> cw) {
+            for (FormatWhen fw : cw) {
+                whens.put(fw, format);
+            }
+        }
+
+        void setName(String s) {
+            fname = s;
+        }
+
+        void setType(String s) {
+            ftype = s;
+        }
+
+        void setResult(String s) {
+            fresult = s;
+        }
+
+        void setPre(String s) {
+            pre = s;
+        }
+
+        void setPost(String s) {
+            post = s;
+        }
+
+        void setErrorPre(String s) {
+            errorPre = s;
+        }
+
+        void setErrorPost(String s) {
+            errorPost = s;
+        }
+
+        String getPre() {
+            return pre;
+        }
+
+        String getPost() {
+            return post;
+        }
+
+        String getErrorPre() {
+            return errorPre;
+        }
+
+        String getErrorPost() {
+            return errorPost;
+        }
+
+        void setPrompts(String prompt, String continuationPrompt) {
+            this.prompt = prompt;
+            this.continuationPrompt = continuationPrompt;
+        }
+
+        String getPrompt(String nextId) {
+            return String.format(prompt, nextId);
+        }
+
+        String getContinuationPrompt(String nextId) {
+            return String.format(continuationPrompt, nextId);
+        }
+    }
+
+    /**
+     * The brace delimited substitutions
+     */
+    public enum FormatField {
+        WHEN,
+        ACTION,
+        RESOLVE("%1$s"),
+        NAME("%2$s"),
+        TYPE("%3$s"),
+        RESULT("%4$s"),
+        PRE,
+        POST,
+        ERRORPRE,
+        ERRORPOST;
+        String form;
+
+        FormatField(String s) {
+            this.form = s;
+        }
+
+        FormatField() {
+            this.form = null;
+        }
+    }
+
+    /**
+     * The event cases
+     */
+    public enum FormatCase {
+        IMPORT("import declaration: {action} {name}"),
+        CLASS("class, interface, enum, or annotation declaration: {action} {name} {resolve}"),
+        INTERFACE("class, interface, enum, or annotation declaration: {action} {name} {resolve}"),
+        ENUM("class, interface, enum, or annotation declaration: {action} {name} {resolve}"),
+        ANNOTATION("annotation interface declaration: {action} {name} {resolve}"),
+        METHOD("method declaration: {action} {name} {type}==parameter-types {resolve}"),
+        VARDECL("variable declaration: {action} {name} {type} {resolve}"),
+        VARDECLRECOVERABLE("recoverably failed variable declaration: {action} {name} {resolve}"),
+        VARINIT("variable declaration with init: {action} {name} {type} {resolve} {result}"),
+        VARRESET("variable reset on update: {action} {name}"),
+        EXPRESSION("expression: {action}=='Saved to scratch variable' {name} {type} {result}"),
+        VARVALUE("variable value expression: {action} {name} {type} {result}"),
+        ASSIGNMENT("assign variable: {action} {name} {type} {result}"),
+        STATEMENT("statement: {action}");
+        String doc;
+
+        private FormatCase(String doc) {
+            this.doc = doc;
+        }
+    }
+
+    /**
+     * The event actions
+     */
+    public enum FormatAction {
+        ADDED("snippet has been added"),
+        MODIFIED("an existing snippet has been modified"),
+        REPLACED("an existing snippet has been replaced with a new snippet"),
+        OVERWROTE("an existing snippet has been overwritten"),
+        DROPPED("snippet has been dropped"),
+        REJECTED("snippet has failed and been rejected");
+        String doc;
+
+        private FormatAction(String doc) {
+            this.doc = doc;
+        }
+    }
+
+    /**
+     * When the event occurs: primary or update
+     */
+    public enum FormatWhen {
+        PRIMARY("the entered snippet"),
+        UPDATE("an update to a dependent snippet");
+        String doc;
+
+        private FormatWhen(String doc) {
+            this.doc = doc;
+        }
+    }
+
+    /**
+     * Resolution problems with event
+     */
+    public enum FormatResolve {
+        OK("resolved correctly"),
+        DEFINED("defined despite recoverably unresolved references"),
+        NOTDEFINED("not defined because of recoverably unresolved references");
+        String doc;
+
+        private FormatResolve(String doc) {
+            this.doc = doc;
+        }
+    }
+
+    // Class used to set custom eval output formats
+    // For both /set format  and /set field -- Parse arguments, setting custom format, or printing error
+    private class FormatSetter {
+
+        private final ArgTokenizer at;
+        private final JShellTool tool;
+        boolean valid = true;
+
+        class Case<E1 extends Enum<E1>, E2 extends Enum<E2>, E3 extends Enum<E3>> {
+
+            Set<E1> e1;
+            Set<E2> e2;
+            Set<E3> e3;
+
+            Case(Set<E1> e1, Set<E2> e2, Set<E3> e3) {
+                this.e1 = e1;
+                this.e2 = e2;
+                this.e3 = e3;
+            }
+
+            Case(Set<E1> e1, Set<E2> e2) {
+                this.e1 = e1;
+                this.e2 = e2;
+            }
+        }
+
+        FormatSetter(JShellTool tool, ArgTokenizer at) {
+            this.tool = tool;
+            this.at = at;
+        }
+
+        void hard(String format, Object... args) {
+            tool.hard(format, args);
+        }
+
+        <E extends Enum<E>> void hardEnums(EnumSet<E> es, Function<E, String> e2s) {
+            hardPairs(es.stream(), ev -> ev.name().toLowerCase(Locale.US), e2s);
+        }
+
+        <T> void hardPairs(Stream<T> stream, Function<T, String> a, Function<T, String> b) {
+            tool.hardPairs(stream, a, b);
+        }
+
+        void fluff(String format, Object... args) {
+            tool.fluff(format, args);
+        }
+
+        void error(String format, Object... args) {
+            tool.error(format, args);
+        }
+
+        void errorat(String format, Object... args) {
+            Object[] a2 = Arrays.copyOf(args, args.length + 1);
+            a2[args.length] = at.whole();
+            tool.error(format + " -- /set %s", a2);
+        }
+
+        void fluffRaw(String format, Object... args) {
+            tool.fluffRaw(format, args);
+        }
+
+        // For /set prompt <mode> "<prompt>" "<continuation-prompt>"
+        boolean setPrompt() {
+            Mode m = nextMode();
+            String prompt = nextFormat();
+            String continuationPrompt = nextFormat();
+            if (valid) {
+                m.setPrompts(prompt, continuationPrompt);
+            } else {
+                fluff("See '/help /set prompt' for help");
+            }
+            return valid;
+        }
+
+        // For /set newmode <new-mode> [command|quiet [<old-mode>]]
+        boolean setNewMode() {
+            String umode = at.next();
+            if (umode == null) {
+                errorat("Expected new feedback mode");
+                valid = false;
+            }
+            if (modeMap.containsKey(umode)) {
+                errorat("Expected a new feedback mode name. %s is a known feedback mode", umode);
+                valid = false;
+            }
+            String[] fluffOpt = at.next("command", "quiet");
+            boolean fluff = fluffOpt == null || fluffOpt.length != 1 || "command".equals(fluffOpt[0]);
+            if (fluffOpt != null && fluffOpt.length != 1) {
+                errorat("Specify either 'command' or 'quiet'");
+                valid = false;
+            }
+            Mode om = null;
+            String omode = at.next();
+            if (omode != null) {
+                om = toMode(omode);
+            }
+            if (valid) {
+                Mode nm = (om != null)
+                        ? new Mode(umode, fluff, om)
+                        : new Mode(umode, fluff);
+                modeMap.put(umode, nm);
+                fluff("Created new feedback mode: %s", nm.name);
+            } else {
+                fluff("See '/help /set newmode' for help");
+            }
+            return valid;
+        }
+
+        // For /set feedback <mode>
+        boolean setFeedback() {
+            Mode m = nextMode();
+            if (valid && m != null) {
+                mode = m;
+                fluff("Feedback mode: %s", mode.name);
+            } else {
+                fluff("See '/help /set feedback' for help");
+            }
+            return valid;
+        }
+
+        // For /set format <mode> "<format>" <selector>...
+        boolean setFormat() {
+            Mode m = nextMode();
+            String format = nextFormat();
+            if (valid) {
+                List<Case<FormatCase, FormatAction, FormatWhen>> specs = new ArrayList<>();
+                String s;
+                while ((s = at.next()) != null) {
+                    String[] d = s.split("-");
+                    specs.add(new Case<>(
+                            parseFormatCase(d, 0),
+                            parseFormatAction(d, 1),
+                            parseFormatWhen(d, 2)
+                    ));
+                }
+                if (valid && specs.isEmpty()) {
+                    errorat("At least one selector required");
+                    valid = false;
+                }
+                if (valid) {
+                    // set the format in the specified cases
+                    specs.stream()
+                            .forEach(c -> m.setCases(format, c.e1, c.e2, c.e3));
+                }
+            }
+            if (!valid) {
+                fluff("See '/help /set format' for help");
+            }
+            return valid;
+        }
+
+        // For /set field mode <field> "<format>" <selector>...
+        boolean setField() {
+            Mode m = nextMode();
+            String fieldName = at.next();
+            FormatField field = parseFormatSelector(fieldName, EnumSet.allOf(FormatField.class), "field");
+            String format = nextFormat();
+            if (valid) {
+                switch (field) {
+                    case ACTION: {
+                        List<Case<FormatAction, FormatWhen, FormatWhen>> specs = new ArrayList<>();
+                        String s;
+                        while ((s = at.next()) != null) {
+                            String[] d = s.split("-");
+                            specs.add(new Case<>(
+                                    parseFormatAction(d, 0),
+                                    parseFormatWhen(d, 1)
+                            ));
+                        }
+                        if (valid && specs.isEmpty()) {
+                            errorat("At least one selector required");
+                            valid = false;
+                        }
+                        if (valid) {
+                            // set the format of the specified actions
+                            specs.stream()
+                                    .forEach(c -> m.setActions(format, c.e1, c.e2));
+                        }
+                        break;
+                    }
+                    case RESOLVE: {
+                        List<Case<FormatResolve, FormatWhen, FormatWhen>> specs = new ArrayList<>();
+                        String s;
+                        while ((s = at.next()) != null) {
+                            String[] d = s.split("-");
+                            specs.add(new Case<>(
+                                    parseFormatResolve(d, 0),
+                                    parseFormatWhen(d, 1)
+                            ));
+                        }
+                        if (valid && specs.isEmpty()) {
+                            errorat("At least one selector required");
+                            valid = false;
+                        }
+                        if (valid) {
+                            // set the format of the specified resolves
+                            specs.stream()
+                                    .forEach(c -> m.setResolves(format, c.e1, c.e2));
+                        }
+                        break;
+                    }
+                    case WHEN: {
+                        List<Case<FormatWhen, FormatWhen, FormatWhen>> specs = new ArrayList<>();
+                        String s;
+                        while ((s = at.next()) != null) {
+                            String[] d = s.split("-");
+                            specs.add(new Case<>(
+                                    parseFormatWhen(d, 1),
+                                    null
+                            ));
+                        }
+                        if (valid && specs.isEmpty()) {
+                            errorat("At least one selector required");
+                            valid = false;
+                        }
+                        if (valid) {
+                            // set the format of the specified whens
+                            specs.stream()
+                                    .forEach(c -> m.setWhens(format, c.e1));
+                        }
+                        break;
+                    }
+                    case NAME: {
+                        m.setName(format);
+                        break;
+                    }
+                    case TYPE: {
+                        m.setType(format);
+                        break;
+                    }
+                    case RESULT: {
+                        m.setResult(format);
+                        break;
+                    }
+                    case PRE: {
+                        m.setPre(format);
+                        break;
+                    }
+                    case POST: {
+                        m.setPost(format);
+                        break;
+                    }
+                    case ERRORPRE: {
+                        m.setErrorPre(format);
+                        break;
+                    }
+                    case ERRORPOST: {
+                        m.setErrorPost(format);
+                        break;
+                    }
+                }
+            }
+            if (!valid) {
+                fluff("See '/help /set field' for help");
+            }
+            return valid;
+        }
+
+        Mode nextMode() {
+            String umode = at.next();
+            return toMode(umode);
+        }
+
+        Mode toMode(String umode) {
+            if (umode == null) {
+                errorat("Expected a feedback mode");
+                valid = false;
+                return null;
+            }
+            Mode m = modeMap.get(umode);
+            if (m != null) {
+                return m;
+            }
+            // Failing an exact match, go searching
+            Mode[] matches = modeMap.entrySet().stream()
+                    .filter(e -> e.getKey().startsWith(umode))
+                    .map(e -> e.getValue())
+                    .toArray(size -> new Mode[size]);
+            if (matches.length == 1) {
+                return matches[0];
+            } else {
+                valid = false;
+                if (matches.length == 0) {
+                    errorat("Does not match any current feedback mode: %s", umode);
+                } else {
+                    errorat("Matchs more then one current feedback mode: %s", umode);
+                }
+                fluff("The feedback mode should be one of the following:");
+                modeMap.keySet().stream()
+                        .forEach(mk -> fluff("   %s", mk));
+                fluff("You may also use just enough letters to make it unique.");
+                return null;
+            }
+        }
+
+        // Test if the format string is correctly
+        final String nextFormat() {
+            String format = at.next();
+            if (format == null) {
+                errorat("Expected format missing");
+                valid = false;
+                return null;
+            }
+            if (!at.isQuoted()) {
+                errorat("Format '%s' must be quoted", format);
+                valid = false;
+                return null;
+            }
+            return format;
+        }
+
+        final Set<FormatCase> parseFormatCase(String[] s, int i) {
+            return parseFormatSelectorStar(s, i, FormatCase.class, EnumSet.allOf(FormatCase.class), "case");
+        }
+
+        final Set<FormatAction> parseFormatAction(String[] s, int i) {
+            return parseFormatSelectorStar(s, i, FormatAction.class,
+                    EnumSet.of(FormatAction.ADDED, FormatAction.MODIFIED, FormatAction.REPLACED), "action");
+        }
+
+        final Set<FormatResolve> parseFormatResolve(String[] s, int i) {
+            return parseFormatSelectorStar(s, i, FormatResolve.class,
+                    EnumSet.of(FormatResolve.DEFINED, FormatResolve.NOTDEFINED), "resolve");
+        }
+
+        final Set<FormatWhen> parseFormatWhen(String[] s, int i) {
+            return parseFormatSelectorStar(s, i, FormatWhen.class, EnumSet.of(FormatWhen.PRIMARY), "when");
+        }
+
+        /**
+         * In a selector x-y-z , parse x, y, or z -- whether they are missing,
+         * or a comma separated list of identifiers and stars.
+         *
+         * @param <E> The enum this selector should belong to
+         * @param sa The array of selector strings
+         * @param i The index of which selector string to use
+         * @param klass The class of the enum that should be used
+         * @param defaults The set of enum values to use if the selector is
+         * missing
+         * @return The set of enum values specified by this selector
+         */
+        final <E extends Enum<E>> Set<E> parseFormatSelectorStar(String[] sa, int i, Class<E> klass, EnumSet<E> defaults, String label) {
+            String s = sa.length > i
+                    ? sa[i]
+                    : null;
+            if (s == null || s.isEmpty()) {
+                return defaults;
+            }
+            Set<E> set = EnumSet.noneOf(klass);
+            EnumSet<E> values = EnumSet.allOf(klass);
+            for (String as : s.split(",")) {
+                if (as.equals("*")) {
+                    set.addAll(values);
+                } else if (!as.isEmpty()) {
+                    set.add(parseFormatSelector(as, values, label));
+                }
+            }
+            return set;
+        }
+
+        /**
+         * In a x-y-a,b selector, parse an x, y, a, or b -- that is an
+         * identifier
+         *
+         * @param <E> The enum this selector should belong to
+         * @param s The string to parse: x, y, or z
+         * @param values The allowed of this enum
+         * @return The enum value
+         */
+        final <E extends Enum<E>> E parseFormatSelector(String s, EnumSet<E> values, String label) {
+            if (s == null) {
+                valid = false;
+                return null;
+            }
+            String u = s.toUpperCase(Locale.US);
+            for (E c : values) {
+                if (c.name().startsWith(u)) {
+                    return c;
+                }
+            }
+
+            errorat("Not a valid %s: %s, must be one of: %s", label, s,
+                    values.stream().map(v -> v.name().toLowerCase(Locale.US)).collect(Collectors.joining(" ")));
+            valid = false;
+            return values.iterator().next();
+        }
+
+        final void printFormatHelp() {
+            hard("Set the format for reporting a snippet event.");
+            hard("");
+            hard("/set format <mode> \"<format>\" <selector>...");
+            hard("");
+            hard("Where <mode> is the name of a previously defined feedback mode -- see '/help /set newmode'.");
+            hard("Where <format> is a quoted string which will have these field substitutions:");
+            hard("   {action}    == The action, e.g.: Added, Modified, Assigned, ...");
+            hard("   {name}      == The name, e.g.: the variable name, ...");
+            hard("   {type}      == The type name");
+            hard("   {resolve}   == Unresolved info, e.g.: ', however, it cannot be invoked until'");
+            hard("   {result}    == The result value");
+            hard("   {when}      == The entered snippet or a resultant update");
+            hard("   {pre}       == The feedback prefix");
+            hard("   {post}      == The feedback postfix");
+            hard("   {errorpre}  == The error prefix");
+            hard("   {errorpost} == The error postfix");
+            hard("Use '/set field' to set the format of these substitutions.");
+            hard("Where <selector> is the context in which the format is applied.");
+            hard("The structure of selector is: <case>[-<action>[-<when>]]");
+            hard("Where each field component may be missing (indicating defaults),");
+            hard("star (indicating all), or a comma separated list of field values.");
+            hard("For case, the field values are:");
+            hardEnums(EnumSet.allOf(FormatCase.class), ev -> ev.doc);
+            hard("For action, the field values are:");
+            hardEnums(EnumSet.allOf(FormatAction.class), ev -> ev.doc);
+            hard("For when, the field values are:");
+            hardEnums(EnumSet.allOf(FormatWhen.class), ev -> ev.doc);
+            hard("");
+            hard("Example:");
+            hard("   /set format example '{pre}{action} variable {name}, reset to null{post}' varreset-*-update");
+        }
+
+        final void printFieldHelp() {
+            hard("Set the format of a field substitution as used in '/set format'.");
+            hard("");
+            hard("/set field <mode> <field> \"<format>\" <selector>...");
+            hard("");
+            hard("Where <mode> is the name of a previously defined feedback mode -- see '/set newmode'.");
+            hard("Where <field> is context-specific format to set, each with its own selector structure:");
+            hard("   action    == The action. The selector: <action>-<when>.");
+            hard("   name      == The name.  '%%s' is the name.  No selectors.");
+            hard("   type      == The type name.  '%%s' is the type. No selectors.");
+            hard("   resolve   == Unresolved info.  '%%s' is the unresolved list. The selector: <resolve>-<when>.");
+            hard("   result    == The result value.  '%%s' is the result value. No selectors.");
+            hard("   when      == The entered snippet or a resultant update. The selector: <when>");
+            hard("   pre       == The feedback prefix. No selectors.");
+            hard("   post      == The feedback postfix. No selectors.");
+            hard("   errorpre  == The error prefix. No selectors.");
+            hard("   errorpost == The error postfix. No selectors.");
+            hard("Where <format> is a quoted string -- see the description specific to the field (above).");
+            hard("Where <selector> is the context in which the format is applied (see above).");
+            hard("For action, the field values are:");
+            hardEnums(EnumSet.allOf(FormatAction.class), ev -> ev.doc);
+            hard("For when, the field values are:");
+            hardEnums(EnumSet.allOf(FormatWhen.class), ev -> ev.doc);
+            hard("For resolve, the field values are:");
+            hardEnums(EnumSet.allOf(FormatResolve.class), ev -> ev.doc);
+            hard("");
+            hard("Example:");
+            hard("   /set field example resolve ' which cannot be invoked until%%s is declared' defined-update");
+        }
+
+        final void printFeedbackHelp() {
+            hard("Set the feedback mode describing displayed feedback for entered snippets and commands.");
+            hard("");
+            hard("/set feedback <mode>");
+            hard("");
+            hard("Where <mode> is the name of a previously defined feedback mode.");
+            hard("Currently defined feedback modes:");
+            modeMap.keySet().stream()
+                    .forEach(m -> hard("   %s", m));
+            hard("User-defined modes can be added, see '/help /set newmode'");
+        }
+
+        final void printNewModeHelp() {
+            hard("Create a user-defined feedback mode, optionally copying from an existing mode.");
+            hard("");
+            hard("/set newmode <new-mode> [command|quiet [<old-mode>]]");
+            hard("");
+            hard("Where <new-mode> is the name of a mode you wish to create.");
+            hard("Where <old-mode> is the name of a previously defined feedback mode.");
+            hard("If <old-mode> is present, its settings are copied to the new mode.");
+            hard("'command' vs 'quiet' determines if informative/verifying command feedback is displayed.");
+            hard("");
+            hard("Once the new mode is created, use '/set format', '/set field', and '/set prompt' to configure it.");
+            hard("Use '/set feedback' to use the new mode.");
+        }
+
+        final void printPromptHelp() {
+            hard("Set the prompts.  Both the normal prompt and the continuation-prompt must be set.");
+            hard("");
+            hard("/set prompt <mode> \"<prompt>\" \"<continuation-propmt>\"");
+            hard("");
+            hard("Where <mode> is the name of a previously defined feedback mode.");
+            hard("Where <prompt> and <continuation-propmt> are quoted strings printed as input promptds;");
+            hard("Both may optionally contain '%%s' which will be substituted with the next snippet id --");
+            hard("note that what is entered may not be assigned that id, for example it may be an error or command.");
+            hard("The continuation-prompt is used on the second and subsequent lines of a multi-line snippet.");
+        }
+    }
+}