8157200: jshell tool: Add /retain command to persist settings
authorrfield
Fri, 20 May 2016 11:55:46 -0700
changeset 38531 c449daa25b45
parent 38530 8e89d567748c
child 38532 24f77d64bb1f
8157200: jshell tool: Add /retain command to persist settings 8156910: jshell tool: crash when code with syntax error contains format specifier Reviewed-by: jlahoda
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ArgTokenizer.java
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/Feedback.java
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java
langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties
langtools/test/jdk/jshell/CommandCompletionTest.java
langtools/test/jdk/jshell/ToolBasicTest.java
langtools/test/jdk/jshell/ToolFormatTest.java
langtools/test/jdk/jshell/ToolRetainTest.java
langtools/test/jdk/jshell/ToolSimpleTest.java
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ArgTokenizer.java	Fri May 20 09:47:00 2016 -0700
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ArgTokenizer.java	Fri May 20 11:55:46 2016 -0700
@@ -93,6 +93,13 @@
         return isQuoted;
     }
 
+    boolean isIdentifier() {
+        if (isQuoted) {
+            return false;
+        }
+        return sval.codePoints().allMatch(cp -> Character.isJavaIdentifierPart(cp));
+    }
+
     String whole() {
         return prefix + str;
     }
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/Feedback.java	Fri May 20 09:47:00 2016 -0700
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/Feedback.java	Fri May 20 11:55:46 2016 -0700
@@ -30,9 +30,11 @@
 import java.util.Collection;
 import java.util.EnumSet;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import static java.util.stream.Collectors.joining;
@@ -50,12 +52,18 @@
     // Internal field name for truncation length
     private static final String TRUNCATION_FIELD = "<truncation>";
 
+    // For encoding to Properties String
+    private static final String RECORD_SEPARATOR = "\u241E";
+
     // Current mode
     private Mode mode = new Mode("", false); // initial value placeholder during start-up
 
-    // Mapping of mode names to mode modes
+    // Mapping of mode name to mode
     private final Map<String, Mode> modeMap = new HashMap<>();
 
+    // Mapping of mode names to encoded retained mode
+    private final Map<String, String> retainedMap = new HashMap<>();
+
     // Mapping selector enum names to enums
     private final Map<String, Selector<?>> selectorMap = new HashMap<>();
 
@@ -118,6 +126,23 @@
         return new Setter(messageHandler, at).setPrompt();
     }
 
+    public String retainFeedback(MessageHandler messageHandler, ArgTokenizer at) {
+        return new Setter(messageHandler, at).retainFeedback();
+    }
+
+    public String retainMode(MessageHandler messageHandler, ArgTokenizer at) {
+        return new Setter(messageHandler, at).retainMode();
+    }
+
+    public boolean restoreEncodedModes(MessageHandler messageHandler, String encoded) {
+        return new Setter(messageHandler, new ArgTokenizer("")).restoreEncodedModes(encoded);
+    }
+
+    public void markModesReadOnly() {
+        modeMap.values().stream()
+                .forEach(m -> m.readOnly = true);
+    }
+
     {
         for (FormatCase e : EnumSet.allOf(FormatCase.class))
             selectorMap.put(e.name().toLowerCase(Locale.US), e);
@@ -147,6 +172,8 @@
         // Event cases: class, method, expression, ...
         final Map<String, List<Setting>> cases;
 
+        boolean readOnly = false;
+
         String prompt = "\n-> ";
         String continuationPrompt = ">> ";
 
@@ -204,6 +231,57 @@
             this.continuationPrompt = m.continuationPrompt;
         }
 
+        /**
+         * Set up a mode reconstituted from a preferences string.
+         *
+         * @param it the encoded Mode broken into String chunks, may contain
+         * subsequent encoded modes
+         */
+        Mode(Iterator<String> it) {
+            this.name = it.next();
+            this.commandFluff = Boolean.parseBoolean(it.next());
+            this.prompt = it.next();
+            this.continuationPrompt = it.next();
+            cases = new HashMap<>();
+            String field;
+            while (!(field = it.next()).equals("***")) {
+                String open = it.next();
+                assert open.equals("(");
+                List<Setting> settings = new ArrayList<>();
+                String bits;
+                while (!(bits = it.next()).equals(")")) {
+                    String format = it.next();
+                    Setting ing = new Setting(Long.parseLong(bits), format);
+                    settings.add(ing);
+                }
+                cases.put(field, settings);
+            }
+        }
+
+        /**
+         * Encodes the mode into a String so it can be saved in Preferences.
+         *
+         * @return the string representation
+         */
+        String encode() {
+            List<String> el = new ArrayList<>();
+            el.add(name);
+            el.add(String.valueOf(commandFluff));
+            el.add(prompt);
+            el.add(continuationPrompt);
+            for (Entry<String, List<Setting>> es : cases.entrySet()) {
+                el.add(es.getKey());
+                el.add("(");
+                for (Setting ing : es.getValue()) {
+                    el.add(String.valueOf(ing.enumBits));
+                    el.add(ing.format);
+                }
+                el.add(")");
+            }
+            el.add("***");
+            return String.join(RECORD_SEPARATOR, el);
+        }
+
         private boolean add(String field, Setting ing) {
             List<Setting> settings =  cases.computeIfAbsent(field, k -> new ArrayList<>());
             if (settings == null) {
@@ -590,8 +668,12 @@
         // For /set prompt <mode> "<prompt>" "<continuation-prompt>"
         boolean setPrompt() {
             Mode m = nextMode();
-            String prompt = nextFormat();
-            String continuationPrompt = nextFormat();
+            if (valid && m.readOnly) {
+                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
+                valid = false;
+            }
+            String prompt = valid ? nextFormat() : null;
+            String continuationPrompt = valid ? nextFormat() : null;
             if (valid) {
                 m.setPrompts(prompt, continuationPrompt);
             } else {
@@ -603,7 +685,7 @@
         // For /set newmode <new-mode> [-command|-quiet [<old-mode>]]
         boolean setNewMode() {
             String umode = at.next();
-            if (umode == null) {
+            if (umode == null || !at.isIdentifier()) {
                 errorat("jshell.err.feedback.expected.new.feedback.mode");
                 valid = false;
             }
@@ -637,7 +719,7 @@
         // For /set feedback <mode>
         boolean setFeedback() {
             Mode m = nextMode();
-            if (valid && m != null) {
+            if (valid) {
                 mode = m;
                 fluffmsg("jshell.msg.feedback.mode", mode.name);
             } else {
@@ -650,8 +732,12 @@
         // For /set format <mode> "<format>" <selector>...
         boolean setFormat() {
             Mode m = nextMode();
+            if (valid && m.readOnly) {
+                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
+                valid = false;
+            }
             String field = at.next();
-            if (field == null || at.isQuoted()) {
+            if (field == null || !at.isIdentifier()) {
                 errorat("jshell.err.feedback.expected.field");
                 valid = false;
             }
@@ -662,6 +748,10 @@
         // For /set truncation <mode> <length> <selector>...
         boolean setTruncation() {
             Mode m = nextMode();
+            if (valid && m.readOnly) {
+                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
+                valid = false;
+            }
             String length = at.next();
             if (length == null) {
                 errorat("jshell.err.truncation.expected.length");
@@ -679,6 +769,54 @@
             return installFormat(m, TRUNCATION_FIELD, length, "/help /set truncation");
         }
 
+        String retainFeedback() {
+            String umode = at.next();
+            if (umode != null) {
+                Mode m = toMode(umode);
+                if (valid && !m.readOnly && !retainedMap.containsKey(m.name)) {
+                    errorat("jshell.err.retained.feedback.mode.must.be.retained.or.predefined");
+                    valid = false;
+                }
+                if (valid) {
+                    mode = m;
+                    fluffmsg("jshell.msg.feedback.mode", mode.name);
+                } else {
+                    fluffmsg("jshell.msg.see", "/help /retain feedback");
+                    return null;
+                }
+            }
+            return mode.name;
+        }
+
+        String retainMode() {
+            Mode m = nextMode();
+            if (valid && m.readOnly) {
+                errorat("jshell.err.not.valid.with.predefined.mode", m.name);
+                valid = false;
+            }
+            if (valid) {
+                retainedMap.put(m.name, m.encode());
+                return String.join(RECORD_SEPARATOR, retainedMap.values());
+            } else {
+                fluffmsg("jshell.msg.see", "/help /retain mode");
+                return null;
+            }
+        }
+
+        boolean restoreEncodedModes(String allEncoded) {
+            // Iterate over each record in each encoded mode
+            String[] ms = allEncoded.split(RECORD_SEPARATOR);
+            Iterator<String> itr = Arrays.asList(ms).iterator();
+            while (itr.hasNext()) {
+                // Reconstruct the encoded mode
+                Mode m = new Mode(itr);
+                modeMap.put(m.name, m);
+                // Continue to retain it a new retains occur
+                retainedMap.put(m.name, m.encode());
+            }
+            return true;
+        }
+
         // install the format of a field under parsed selectors
         boolean installFormat(Mode m, String field, String format, String help) {
             String slRaw;
@@ -712,7 +850,7 @@
         }
 
         Mode toMode(String umode) {
-            if (umode == null) {
+            if (umode == null || !at.isIdentifier()) {
                 errorat("jshell.err.feedback.expected.mode");
                 valid = false;
                 return null;
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Fri May 20 09:47:00 2016 -0700
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Fri May 20 11:55:46 2016 -0700
@@ -166,7 +166,7 @@
     private boolean regenerateOnDeath = true;
     private boolean live = false;
     private boolean feedbackInitialized = false;
-    private String initialMode = null;
+    private String commandLineFeedbackMode = null;
     private List<String> remoteVMOptions = new ArrayList<>();
 
     SourceCodeAnalysis analysis;
@@ -176,14 +176,17 @@
     private boolean debug = false;
     public boolean testPrompt = false;
     private String cmdlineClasspath = null;
-    private String cmdlineStartup = null;
+    private String startup = null;
     private String[] editor = null;
 
     // Commands and snippets which should be replayed
     private List<String> replayableHistory;
     private List<String> replayableHistoryPrevious;
 
-    static final String STARTUP_KEY = "STARTUP";
+    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 =
@@ -454,6 +457,12 @@
     }
 
     private void start(IOContext in, List<String> loadList) {
+        // Read retained editor setting (if any)
+        String editorString = prefs.get(EDITOR_KEY, null);
+        if (editorString != null) {
+            editor = editorString.split(RECORD_SEPARATOR);
+        }
+
         resetState(); // Initialize
 
         // Read replay history from last jshell session into previous history
@@ -519,37 +528,37 @@
                         return null;
                     case "-feedback":
                         if (ai.hasNext()) {
-                            initialMode = ai.next();
+                            commandLineFeedbackMode = ai.next();
                         } else {
                             startmsg("jshell.err.opt.feedback.arg");
                             return null;
                         }
                         break;
                     case "-q":
-                        initialMode = "concise";
+                        commandLineFeedbackMode = "concise";
                         break;
                     case "-qq":
-                        initialMode = "silent";
+                        commandLineFeedbackMode = "silent";
                         break;
                     case "-v":
-                        initialMode = "verbose";
+                        commandLineFeedbackMode = "verbose";
                         break;
                     case "-startup":
-                        if (cmdlineStartup != null) {
+                        if (startup != null) {
                             startmsg("jshell.err.opt.startup.conflict");
                             return null;
                         }
-                        cmdlineStartup = readFile(ai.hasNext()? ai.next() : null, "'-startup'");
-                        if (cmdlineStartup == null) {
+                        startup = readFile(ai.hasNext()? ai.next() : null, "'-startup'");
+                        if (startup == null) {
                             return null;
                         }
                         break;
                     case "-nostartup":
-                        if (cmdlineStartup != null && !cmdlineStartup.isEmpty()) {
+                        if (startup != null && !startup.isEmpty()) {
                             startmsg("jshell.err.opt.startup.conflict");
                             return null;
                         }
-                        cmdlineStartup = "";
+                        startup = "";
                         break;
                     default:
                         if (arg.startsWith("-R")) {
@@ -571,6 +580,27 @@
         cmdout.print(getResourceString("help.usage"));
     }
 
+    /**
+     * 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();
 
@@ -604,8 +634,9 @@
         analysis = state.sourceCodeAnalysis();
         live = true;
         if (!feedbackInitialized) {
-            startUpRun(getResourceString("startup.feedback"));
+            // One time per run feedback initialization
             feedbackInitialized = true;
+            initFeedback();
         }
 
         if (cmdlineClasspath != null) {
@@ -613,38 +644,46 @@
         }
 
         String start;
-        if (cmdlineStartup == null) {
-            start = prefs.get(STARTUP_KEY, "<nada>");
-            if (start.equals("<nada>")) {
+        if (startup == null) {
+            start = startup = prefs.get(STARTUP_KEY, null);
+            if (start == null) {
                 start = DEFAULT_STARTUP;
-                prefs.put(STARTUP_KEY, DEFAULT_STARTUP);
             }
         } else {
-            start = cmdlineStartup;
+            start = startup;
         }
         startUpRun(start);
-        if (initialMode != null) {
-            MessageHandler mh = new MessageHandler() {
-                @Override
-                public void fluff(String format, Object... args) {
-                }
-
-                @Override
-                public void fluffmsg(String messageKey, Object... args) {
-                }
-
-                @Override
-                public void errormsg(String messageKey, Object... args) {
-                    startmsg(messageKey, args);
-                }
-            };
-            if (!feedback.setFeedback(mh, new ArgTokenizer("-feedback ", initialMode))) {
+        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) {
+            feedback.restoreEncodedModes(initmh, encoded);
+        }
+        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;
             }
-            initialMode = null;
+            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.setFeedback(initmh, new ArgTokenizer("/retain feedback ", fb));
+            }
         }
-        currentNameSpace = mainNamespace;
     }
+
     //where
     private void startUpRun(String start) {
         try (IOContext suin = new FileScannerIOContext(new StringReader(start))) {
@@ -1052,6 +1091,9 @@
         registerCommand(new Command("/set",
                 arg -> cmdSet(arg),
                 new FixedCompletionProvider(SET_SUBCOMMANDS)));
+        registerCommand(new Command("/retain",
+                arg -> cmdRetain(arg),
+                new FixedCompletionProvider(RETAIN_SUBCOMMANDS)));
         registerCommand(new Command("/?",
                 "help.quest",
                 arg -> cmdHelp(arg),
@@ -1128,9 +1170,13 @@
     private static final String[] SET_SUBCOMMANDS = new String[]{
         "format", "truncation", "feedback", "newmode", "prompt", "editor", "start"};
 
+    private static final String[] RETAIN_SUBCOMMANDS = new String[]{
+        "feedback", "mode", "editor", "start"};
+
     final boolean cmdSet(String arg) {
-        ArgTokenizer at = new ArgTokenizer("/set ", arg.trim());
-        String which = setSubCommand(at);
+        String cmd = "/set";
+        ArgTokenizer at = new ArgTokenizer(cmd +" ", arg.trim());
+        String which = subCommand(cmd, at, SET_SUBCOMMANDS);
         if (which == null) {
             return false;
         }
@@ -1151,56 +1197,106 @@
                     errormsg("jshell.err.set.editor.arg");
                     return false;
                 } else {
-                    List<String> ed = new ArrayList<>();
-                    ed.add(prog);
-                    String n;
-                    while ((n = at.next()) != null) {
-                        ed.add(n);
-                    }
-                    editor = ed.toArray(new String[ed.size()]);
-                    fluffmsg("jshell.msg.set.editor.set", prog);
-                    return true;
+                    return setEditor(cmd, prog, at);
                 }
             }
             case "start": {
-                String init = readFile(at.next(), "/set start");
-                if (init == null) {
-                    return false;
-                } else {
-                    prefs.put(STARTUP_KEY, init);
-                    return true;
-                }
+                return setStart(cmd, at.next());
             }
             default:
-                errormsg("jshell.err.arg", "/set", at.val());
+                errormsg("jshell.err.arg", cmd, at.val());
                 return false;
         }
     }
 
-    boolean printSetHelp(ArgTokenizer at) {
-        String which = setSubCommand(at);
+    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;
         }
-        hardrb("help.set." + which);
+        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": {
+                String prog = at.next();
+                if (prog != null) {
+                    // If the editor is specified, first run /set editor ...
+                    if(!setEditor(cmd, prog, at)) {
+                        return false;
+                    }
+                }
+                if (editor != null) {
+                    // If an editor has been set now, or in the past, retain it
+                    prefs.put(EDITOR_KEY, String.join(RECORD_SEPARATOR, editor));
+                    return true;
+                }
+                return false;
+            }
+            case "start": {
+                String fn = at.next();
+                if (fn != null) {
+                    if (!setStart(cmd, fn)) {
+                        return false;
+                    }
+                }
+                if (startup != null) {
+                    prefs.put(STARTUP_KEY, startup);
+                    return true;
+                }
+                return false;
+            }
+            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;
     }
 
-    String setSubCommand(ArgTokenizer at) {
-        String[] matches = at.next(SET_SUBCOMMANDS);
+    // Find which, if any, sub-command matches
+    String subCommand(String cmd, ArgTokenizer at, String[] subs) {
+        String[] matches = at.next(subs);
         if (matches == null) {
-            errormsg("jshell.err.set.arg");
+            // No sub-command was given
+            errormsg("jshell.err.sub.arg", cmd);
             return null;
         }
         if (matches.length == 0) {
-            errormsg("jshell.err.arg", "/set", at.val());
-            fluffmsg("jshell.msg.use.one.of", Arrays.stream(SET_SUBCOMMANDS)
+            // 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) {
-            errormsg("jshell.err.set.ambiguous", at.val());
+            // 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(", "))
             );
@@ -1209,6 +1305,31 @@
         return matches[0];
     }
 
+    // The sub-command:  /set editor <editor-command-line>>
+    boolean setEditor(String cmd, String prog, ArgTokenizer at) {
+        List<String> ed = new ArrayList<>();
+        ed.add(prog);
+        String n;
+        while ((n = at.next()) != null) {
+            ed.add(n);
+        }
+        editor = ed.toArray(new String[ed.size()]);
+        fluffmsg("jshell.msg.set.editor.set", prog);
+        return true;
+    }
+
+    // The sub-command:  /set start <start-file>
+    boolean setStart(String cmd, String fn) {
+        String init = readFile(fn, cmd + " start");
+        if (init == null) {
+            return false;
+        } else {
+            startup = init;
+            //prefs.put(STARTUP_KEY, init);
+            return true;
+        }
+    }
+
     boolean cmdClasspath(String arg) {
         if (arg.isEmpty()) {
             errormsg("jshell.err.classpath.arg");
@@ -1301,9 +1422,16 @@
                     .toArray(size -> new Command[size]);
             at.mark();
             String sub = at.next();
-            if (sub != null && matches.length == 1 && matches[0].command.equals("/set")) {
-                at.rewind();
-                return printSetHelp(at);
+            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) {
@@ -1964,7 +2092,7 @@
                 List<String> disp = new ArrayList<>();
                 displayDiagnostics(source, d, disp);
                 disp.stream()
-                        .forEach(l -> hard(l));
+                        .forEach(l -> hard("%s", l));
             }
 
             if (ste.status() != Status.REJECTED) {
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties	Fri May 20 09:47:00 2016 -0700
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties	Fri May 20 11:55:46 2016 -0700
@@ -74,8 +74,8 @@
 jshell.msg.error = Error:
 jshell.msg.warning = Warning:
 
-jshell.err.set.arg = The ''/set'' command requires a sub-command and arguments. See: ''/help /set''
-jshell.err.set.ambiguous = Ambiguous sub-command argument to ''/set'': {0}
+jshell.err.sub.arg = The ''{0}'' command requires a sub-command. See: ''/help {0}''
+jshell.err.sub.ambiguous = Ambiguous sub-command argument to ''{0}'': {1}
 
 jshell.err.classpath.arg = The /classpath command requires a path argument.
 jshell.msg.classpath = Path ''{0}'' added to classpath
@@ -120,6 +120,10 @@
 jshell.err.truncation.expected.length = Expected truncation length -- {0}
 jshell.err.truncation.length.not.integer = Truncation length must be an integer: {0} -- {1}
 
+jshell.err.not.valid.with.predefined.mode = Not valid with predefined modes: {0} -- {1}
+jshell.err.retained.feedback.mode.must.be.retained.or.predefined = \
+''/retain feedback <mode>'' requires that <mode> is predefined or has been retained with ''/retain mode'' -- {0}
+
 jshell.console.see.more = <press tab to see more>
 jshell.console.do.nothing = Do nothing
 jshell.console.choice = Choice: \
@@ -355,12 +359,31 @@
 /set prompt <mode> "<prompt>" "<continuation-prompt>"\n\t\
      Set the displayed prompts for a given feedback mode.\n\n\
 /set truncation <mode> <length> <selector>...\n\t\
-     Set the maximum length of a displayed value\n\t\
+     Set the maximum length of a displayed value\n\
 /set format <mode> <field> "<format>" <selector>...\n\t\
      Configure a feedback mode by setting the format of a field when the selector matchs.\n\n\
 To get more information about one of these forms, use /help with the form specified.\n\
 For example:   /help /set format
 
+help.retain.summary = retain jshell configuration information for subsequent sessions
+help.retain.args = editor|start|feedback|mode
+help.retain =\
+Retain jshell configuration information for future invocations of the jshell tool,\n\
+including: the external editor to use, the start-up definitions to use, the\n\
+configuration of a feedback mode, or the feedback mode to use.\n\
+\n\
+/retain editor [<command> <optional-arg>...]\n\t\
+     Specify the command to launch for the /edit command.\n\t\
+     The <command> is an operating system dependent string.\n\n\
+/retain start [<file>]\n\t\
+     The contents of the specified <file> become the default start-up snippets and commands.\n\n\
+/retain feedback [<mode>]\n\t\
+     Set the feedback mode describing displayed feedback for entered snippets and commands.\n\n\
+/retain mode <mode>\n\t\
+     Create a user-defined feedback mode, optionally copying from an existing mode.\n\n\
+To get more information about one of these forms, use /help with the form specified.\n\
+For example:   /help /retain feedback
+
 help.quest.summary = get information about jshell
 help.quest.args = [<command>|<subject>]
 help.quest =\
@@ -568,10 +591,10 @@
 help.set.editor =\
 Specify the command to launch for the /edit command.\n\
 \n\t\
-/set editor <command> <optional-arg>...\n\
+/set editor <command>\n\
 \n\
 The <command> is an operating system dependent string.\n\
-The <command> may include space-separated arguments (such as flags) -- <optional-arg>....\n\
+The <command> may include space-separated arguments (such as flags)\n\
 When /edit is used, the temporary file to edit will be appended as the last argument.
 
 help.set.start =\
@@ -579,8 +602,54 @@
 \n\t\
 /set start <file>\n\
 \n\
+The contents of the specified <file> become the start-up snippets and commands used\n\
+when the /reset or /reload commands are used in this session.\n\
+This command is good for testing the start-up settings.  To retain them for future\n\
+runs of the jshell tool use the command:\n\t\
+/retain start\n
+
+help.retain.feedback = \
+Retain which feedback mode to use for displayed feedback for entered snippets and commands.\n\
+This feedback mode will be used in this and future sessions of the jshell tool.\n\
+\n\t\
+/retain feedback [<mode>]\n\
+\n\
+Where <mode> is the name of a previously defined feedback mode.\n\
+You may use just enough letters to make it unique.\n\
+If the <mode> is not specified, this command retains the current mode (as set\n\
+with the most recent /set feedback or /retain feedback command.)\n\
+
+help.retain.mode = \
+Retain the existence and configuration of a user-defined feedback mode.\n\
+This mode will be available in this and future sessions of the jshell tool.
+\n\t\
+/retain mode <mode>\n\
+\n\
+Where <mode> is the name of a mode you wish to retain.\n\
+The <mode> must previously have been created with /set newmode and\n\
+configured as desired with /set prompt, /set format, and /set truncation.\n
+
+help.retain.editor =\
+Retain the command to launch for the /edit command.  This command will be invoked when\n\
+the /edit command is used in this and future sessions of the jshell tool.\n\
+\n\t\
+/retain editor [<command>]\n\
+\n\
+The <command> is an operating system dependent string.\n\
+The <command> may include space-separated arguments (such as flags)\n\
+When /edit is used, the temporary file to edit will be appended as the last argument.\n\
+If <command> is not specified, the command last specified in a /set editor or\n\
+/retain editor command will be retained.\n
+
+help.retain.start =\
+Retain the start-up configuration -- a sequence of snippets and commands read at start-up.\n\
+\n\t\
+/retain start [<file>]\n\
+\n\
 The contents of the specified <file> become the default start-up snippets and commands --\n\
-which are run when the jshell tool is started or reset.
+which are run when the jshell tool is started or reset.\n\
+If <file> is not specified, the start-up last specified in a /set start or\n\
+/retain start command will be retained.\n
 
 startup.feedback = \
 /set newmode verbose -command    \n\
--- a/langtools/test/jdk/jshell/CommandCompletionTest.java	Fri May 20 09:47:00 2016 -0700
+++ b/langtools/test/jdk/jshell/CommandCompletionTest.java	Fri May 20 11:55:46 2016 -0700
@@ -53,7 +53,7 @@
 
     public void testCommand() {
         assertCompletion("/deb|", false);
-        assertCompletion("/re|", false, "/reload ", "/reset ");
+        assertCompletion("/re|", false, "/reload ", "/reset ", "/retain ");
         assertCompletion("/h|", false, "/help ", "/history ");
     }
 
--- a/langtools/test/jdk/jshell/ToolBasicTest.java	Fri May 20 09:47:00 2016 -0700
+++ b/langtools/test/jdk/jshell/ToolBasicTest.java	Fri May 20 11:55:46 2016 -0700
@@ -569,4 +569,17 @@
             return ex.getMessage();
         }
     }
+
+    public void testHeadlessEditPad() {
+        String prevHeadless = System.getProperty("java.awt.headless");
+        try {
+            System.setProperty("java.awt.headless", "true");
+            test(
+                (a) -> assertCommandOutputStartsWith(a, "/edit printf", "|  Cannot launch editor -- unexpected exception:")
+            );
+        } finally {
+            System.setProperty("java.awt.headless", prevHeadless==null? "false" : prevHeadless);
+        }
+    }
+
 }
--- a/langtools/test/jdk/jshell/ToolFormatTest.java	Fri May 20 09:47:00 2016 -0700
+++ b/langtools/test/jdk/jshell/ToolFormatTest.java	Fri May 20 11:55:46 2016 -0700
@@ -220,7 +220,7 @@
                     (a) -> assertCommandOutputStartsWith(a, "/set feedback te",
                             ""),
                     (a) -> assertCommandOutputStartsWith(a, "/set ",
-                            "ERROR: The '/set' command requires a sub-command and arguments"),
+                            "ERROR: The '/set' command requires a sub-command"),
                     (a) -> assertCommandOutputStartsWith(a, "/set xyz",
                             "ERROR: Invalid '/set' argument: xyz"),
                     (a) -> assertCommandOutputStartsWith(a, "/set f",
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/test/jdk/jshell/ToolRetainTest.java	Fri May 20 11:55:46 2016 -0700
@@ -0,0 +1,168 @@
+/*
+ * 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.
+ *
+ * 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.
+ */
+
+/*
+ * @test
+ * @bug 8157200
+ * @summary Tests of what information is retained across jshell tool runs
+ * @modules jdk.jshell/jdk.internal.jshell.tool
+ * @build ToolRetainTest ReplToolTesting
+ * @run testng ToolRetainTest
+ */
+
+import org.testng.annotations.Test;
+
+@Test
+public class ToolRetainTest extends ReplToolTesting {
+
+    public void testRetainMode() {
+        test(
+                (a) -> assertCommand(a, "/set newmode trm -quiet", "|  Created new feedback mode: trm"),
+                (a) -> assertCommand(a, "/set feedback trm", ""),
+                (a) -> assertCommand(a, "/set format trm display '{name}:{value}'", ""),
+                (a) -> assertCommand(a, "int x = 45", "x:45"),
+                (a) -> assertCommand(a, "/retain mode trm", ""),
+                (a) -> assertCommand(a, "/exit", "")
+        );
+        test(
+                (a) -> assertCommand(a, "/set feedback trm", ""),
+                (a) -> assertCommand(a, "int x = 45", "x:45")
+        );
+    }
+
+    public void testRetain2Mode() {
+        test(
+                (a) -> assertCommand(a, "/set newmode trm1 -quiet", "|  Created new feedback mode: trm1"),
+                (a) -> assertCommand(a, "/retain mode trm1", ""),
+                (a) -> assertCommand(a, "/retain feedback trm1", ""),
+                (a) -> assertCommand(a, "/set format trm1 display '{name}:{value}'", ""),
+                (a) -> assertCommand(a, "int x = 66", "x:66"),
+                (a) -> assertCommand(a, "/retain mode trm1", ""),
+                (a) -> assertCommand(a, "/exit", "")
+        );
+        test(
+                (a) -> assertCommand(a, "/set newmode trm2 -quiet", ""),
+                (a) -> assertCommand(a, "/set format trm2 display '{name}={value}'", ""),
+                (a) -> assertCommand(a, "int x = 45", "x:45"),
+                (a) -> assertCommand(a, "/retain mode trm2", ""),
+                (a) -> assertCommand(a, "/exit", "")
+        );
+        test(
+                (a) -> assertCommand(a, "int x = 99", "x:99"),
+                (a) -> assertCommand(a, "/set feedback trm2", ""),
+                (a) -> assertCommand(a, "int z = 77", "z=77")
+        );
+    }
+
+    public void testRetainFeedback() {
+        test(
+                (a) -> assertCommand(a, "/retain feedback verbose", "|  Feedback mode: verbose"),
+                (a) -> assertCommand(a, "/exit", "")
+        );
+        test(
+                (a) -> assertCommandOutputContains(a, "int h =8", "|  created variable h : int")
+        );
+    }
+
+    public void testRetainFeedbackBlank() {
+        test(
+                (a) -> assertCommand(a, "/set feedback verbose", "|  Feedback mode: verbose"),
+                (a) -> assertCommand(a, "/retain feedback", ""),
+                (a) -> assertCommand(a, "/exit", "")
+        );
+        test(
+                (a) -> assertCommandOutputContains(a, "int qw = 5", "|  created variable qw : int")
+        );
+    }
+
+    public void testRetainEditor() {
+        test(
+                (a) -> assertCommand(a, "/retain editor nonexistent", "|  Editor set to: nonexistent"),
+                (a) -> assertCommand(a, "/exit", "")
+        );
+        test(
+                (a) -> assertCommandOutputContains(a, "int h =8", ""),
+                (a) -> assertCommandOutputContains(a, "/edit h", "Edit Error:")
+        );
+    }
+
+    public void testRetainEditorBlank() {
+        test(
+                (a) -> assertCommand(a, "/set editor nonexistent", "|  Editor set to: nonexistent"),
+                (a) -> assertCommand(a, "/retain editor", ""),
+                (a) -> assertCommand(a, "/exit", "")
+        );
+        test(
+                (a) -> assertCommandOutputContains(a, "int h =8", ""),
+                (a) -> assertCommandOutputContains(a, "/edit h", "Edit Error:")
+        );
+    }
+
+    public void testRetainModeNeg() {
+        test(
+                (a) -> assertCommandOutputStartsWith(a, "/retain mode verbose",
+                        "|  Not valid with predefined mode"),
+                (a) -> assertCommandOutputStartsWith(a, "/retain mode ????",
+                        "|  Expected a feedback mode")
+        );
+    }
+
+    public void testRetainFeedbackNeg() {
+        test(
+                (a) -> assertCommandOutputStartsWith(a, "/retain feedback babble1",
+                        "|  Does not match any current feedback mode"),
+                (a) -> assertCommand(a, "/set newmode trfn",
+                        "|  Created new feedback mode: trfn"),
+                (a) -> assertCommandOutputContains(a, "/retain feedback trfn",
+                        "is predefined or has been retained"),
+                (a) -> assertCommandOutputStartsWith(a, "/retain feedback !!!!",
+                        "|  Expected a feedback mode")
+        );
+    }
+
+    public void testNoRetainMode() {
+        test(
+                (a) -> assertCommand(a, "/set newmode trm -quiet", "|  Created new feedback mode: trm"),
+                (a) -> assertCommand(a, "/set feedback trm", ""),
+                (a) -> assertCommand(a, "/set format trm display '{name}:{value}'", ""),
+                (a) -> assertCommand(a, "int x = 45", "x:45"),
+                (a) -> assertCommand(a, "/exit", "")
+        );
+        test(
+                (a) -> assertCommandOutputStartsWith(a, "/set feedback trm",
+                        "|  Does not match any current feedback mode"),
+                (a) -> assertCommandOutputContains(a, "int x = 45", "==> 45")
+        );
+    }
+
+    public void testNoRetainFeedback() {
+        test(
+                (a) -> assertCommand(a, "/set feedback verbose", "|  Feedback mode: verbose"),
+                (a) -> assertCommand(a, "/exit", "")
+        );
+        test(
+                (a) -> assertCommand(a, "int h =8", "h ==> 8")
+        );
+    }
+
+}
--- a/langtools/test/jdk/jshell/ToolSimpleTest.java	Fri May 20 09:47:00 2016 -0700
+++ b/langtools/test/jdk/jshell/ToolSimpleTest.java	Fri May 20 11:55:46 2016 -0700
@@ -23,7 +23,7 @@
 
 /*
  * @test
- * @bug 8153716 8143955 8151754 8150382 8153920
+ * @bug 8153716 8143955 8151754 8150382 8153920 8156910
  * @summary Simple jshell tool tests
  * @modules jdk.compiler/com.sun.tools.javac.api
  *          jdk.compiler/com.sun.tools.javac.main
@@ -449,18 +449,6 @@
                       assertStartsWith("|  '/save' requires a filename argument.")));
     }
 
-    public void testHeadlessEditPad() {
-        String prevHeadless = System.getProperty("java.awt.headless");
-        try {
-            System.setProperty("java.awt.headless", "true");
-            test(
-                (a) -> assertCommandOutputStartsWith(a, "/edit printf", "|  Cannot launch editor -- unexpected exception:")
-            );
-        } finally {
-            System.setProperty("java.awt.headless", prevHeadless==null? "false" : prevHeadless);
-        }
-    }
-
     public void testOptionQ() {
         test(new String[]{"-q", "-nostartup"},
                 (a) -> assertCommand(a, "1+1", "$1 ==> 2"),
@@ -495,4 +483,11 @@
                         "$1 ==> \"blorp\"")
         );
     }
+
+    public void test8156910() {
+        test(
+                (a) -> assertCommandOutputContains(a, "System.out.println(\"%5d\", 10);", "%5d"),
+                (a) -> assertCommandOutputContains(a, "1234", "==> 1234")
+        );
+    }
 }