8167554: jshell tool: re-execute a range and/or sequence of snippets
authorrfield
Thu, 18 May 2017 14:16:25 -0700
changeset 45215 c9477e22877f
parent 45093 c42dc7b58b4d
child 45217 6f188021f8f8
8167554: jshell tool: re-execute a range and/or sequence of snippets 8180508: jshell tool: support id ranges in all commands with id arguments 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/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/EditorTestBase.java
langtools/test/jdk/jshell/MergedTabShiftTabCommandTest.java
langtools/test/jdk/jshell/ToolBasicTest.java
langtools/test/jdk/jshell/ToolLocaleMessageTest.java
langtools/test/jdk/jshell/ToolSimpleTest.java
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ArgTokenizer.java	Wed Jul 05 23:27:00 2017 +0200
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/ArgTokenizer.java	Thu May 18 14:16:25 2017 -0700
@@ -121,6 +121,17 @@
     }
 
     /**
+     * Is the specified option allowed.
+     *
+     * @param opt the option to check
+     * @return true if the option is allowed
+     */
+    boolean isAllowedOption(String opt) {
+        Boolean has = options.get(opt);
+        return has != null;
+    }
+
+    /**
      * Has the specified option been encountered.
      *
      * @param opt the option to check
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Wed Jul 05 23:27:00 2017 +0200
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/JShellTool.java	Thu May 18 14:16:25 2017 -0700
@@ -90,7 +90,6 @@
 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.ServiceLoader;
 import java.util.Spliterators;
@@ -126,6 +125,7 @@
 public class JShellTool implements MessageHandler {
 
     private static final Pattern LINEBREAK = Pattern.compile("\\R");
+    private static final Pattern ID = Pattern.compile("[se]?\\d+([-\\s].*)?");
             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";
@@ -1189,36 +1189,54 @@
         }
     }
 
-    private void processCommand(String cmd) {
-        if (cmd.startsWith("/-")) {
+    /**
+     * Process a command (as opposed to a snippet) -- things that start with
+     * slash.
+     *
+     * @param input
+     */
+    private void processCommand(String input) {
+        if (input.startsWith("/-")) {
             try {
                 //handle "/-[number]"
-                cmdUseHistoryEntry(Integer.parseInt(cmd.substring(1)));
+                cmdUseHistoryEntry(Integer.parseInt(input.substring(1)));
                 return ;
             } catch (NumberFormatException ex) {
                 //ignore
             }
         }
-        String arg = "";
-        int idx = cmd.indexOf(' ');
+        String cmd;
+        String arg;
+        int idx = input.indexOf(' ');
         if (idx > 0) {
-            arg = cmd.substring(idx + 1).trim();
-            cmd = cmd.substring(0, idx);
+            arg = input.substring(idx + 1).trim();
+            cmd = input.substring(0, idx);
+        } else {
+            cmd = input;
+            arg = "";
         }
+        // find the command as a "real command", not a pseudo-command or doc subject
         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);
+                // not found, it is either a snippet command or an error
+                if (ID.matcher(cmd.substring(1)).matches()) {
+                    // it is in the form of a snipppet id, see if it is a valid history reference
+                    rerunHistoryEntriesById(input);
+                } else {
+                    errormsg("jshell.err.invalid.command", cmd);
                     fluffmsg("jshell.msg.help.for.help");
-                }   break;
+                }
+                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;
+                }
+                break;
             default:
+                // command if too short (ambigous), show the possibly matches
                 errormsg("jshell.err.command.ambiguous", cmd,
                         Arrays.stream(candidates).map(c -> c.command).collect(Collectors.joining(", ")));
                 fluffmsg("jshell.msg.help.for.help");
@@ -1701,6 +1719,9 @@
         registerCommand(new Command("context",
                 "help.context",
                 CommandKind.HELP_SUBJECT));
+        registerCommand(new Command("rerun",
+                "help.rerun",
+                CommandKind.HELP_SUBJECT));
 
         commandCompletions = new ContinuousCompletionProvider(
                 commands.values().stream()
@@ -2247,6 +2268,20 @@
             Predicate<Snippet> defFilter, String rawargs, String cmd) {
         ArgTokenizer at = new ArgTokenizer(cmd, rawargs.trim());
         at.allowedOptions("-all", "-start");
+        return argsOptionsToSnippets(snippetSupplier, defFilter, at);
+    }
+
+    /**
+     * 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 at the ArgTokenizer, with allowed options set
+     * @return
+     */
+    private <T extends Snippet> Stream<T> argsOptionsToSnippets(Supplier<Stream<T>> snippetSupplier,
+            Predicate<Snippet> defFilter, ArgTokenizer at) {
         List<String> args = new ArrayList<>();
         String s;
         while ((s = at.next()) != null) {
@@ -2263,11 +2298,11 @@
             errormsg("jshell.err.conflicting.options", at.whole());
             return null;
         }
-        if (at.hasOption("-all")) {
+        if (at.isAllowedOption("-all") && at.hasOption("-all")) {
             // all snippets including start-up, failed, and overwritten
             return snippetSupplier.get();
         }
-        if (at.hasOption("-start")) {
+        if (at.isAllowedOption("-start") && at.hasOption("-start")) {
             // start-up snippets
             return snippetSupplier.get()
                     .filter(this::inStartUp);
@@ -2277,54 +2312,227 @@
             return snippetSupplier.get()
                     .filter(defFilter);
         }
-        return argsToSnippets(snippetSupplier, args);
+        return new ArgToSnippets<>(snippetSupplier).argsToSnippets(args);
     }
 
     /**
-     * Convert user arguments to a Stream of snippets referenced by those
-     * arguments.
+     * Support for converting arguments that are definition names, snippet ids,
+     * or snippet id ranges into a stream of snippets,
      *
-     * @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
+     * @param <T> the snipper subtype
      */
-    private <T extends Snippet> Stream<T> argsToSnippets(Supplier<Stream<T>> snippetSupplier,
-            List<String> args) {
-        Stream<T> result = null;
-        for (String arg : args) {
+    private class ArgToSnippets<T extends Snippet> {
+
+        // the supplier of snippet streams
+        final Supplier<Stream<T>> snippetSupplier;
+        // these two are parallel, and lazily filled if a range is encountered
+        List<T> allSnippets;
+        String[] allIds = null;
+
+        /**
+         *
+         * @param snippetSupplier the base list of possible snippets
+        */
+        ArgToSnippets(Supplier<Stream<T>> snippetSupplier) {
+            this.snippetSupplier = snippetSupplier;
+        }
+
+        /**
+         * Convert user arguments to a Stream of snippets referenced by those
+         * arguments.
+         *
+         * @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
+         */
+        Stream<T> argsToSnippets(List<String> args) {
+            Stream<T> result = null;
+            for (String arg : args) {
+                // Find the best match
+                Stream<T> st = argToSnippets(arg);
+                if (st == null) {
+                    return null;
+                } else {
+                    result = (result == null)
+                            ? st
+                            : Stream.concat(result, st);
+                }
+            }
+            return result;
+        }
+
+        /**
+         * Convert a user argument to a Stream of snippets referenced by the
+         * argument.
+         *
+         * @param snippetSupplier the base list of possible snippets
+         * @param arg the user's argument to the command
+         * @return a Stream of referenced snippets or null if no matches to
+         * specific arg
+         */
+        Stream<T> argToSnippets(String arg) {
+            if (arg.contains("-")) {
+                return range(arg);
+            }
             // Find the best match
             Stream<T> st = layeredSnippetSearch(snippetSupplier, arg);
             if (st == null) {
-                Stream<Snippet> est = layeredSnippetSearch(state::snippets, arg);
-                if (est == null) {
+                badSnippetErrormsg(arg);
+                return null;
+            } else {
+                return st;
+            }
+        }
+
+        /**
+         * Look for inappropriate snippets to give best error message
+         *
+         * @param arg the bad snippet arg
+         * @param errKey the not found error key
+         */
+        void badSnippetErrormsg(String arg) {
+            Stream<Snippet> est = layeredSnippetSearch(state::snippets, arg);
+            if (est == null) {
+                if (ID.matcher(arg).matches()) {
+                    errormsg("jshell.err.no.snippet.with.id", arg);
+                } else {
                     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);
+                errormsg("jshell.err.the.snippet.cannot.be.used.with.this.command",
+                        arg, est.findFirst().get().source());
             }
         }
-        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)
-        );
+
+        /**
+         * Search through the snippets for the best match to the id/name.
+         *
+         * @param <R> the snippet type
+         * @param aSnippetSupplier the supplier of snippet streams
+         * @param arg the arg to match
+         * @return a Stream of referenced snippets or null if no matches to
+         * specific arg
+         */
+        <R extends Snippet> Stream<R> layeredSnippetSearch(Supplier<Stream<R>> aSnippetSupplier, String arg) {
+            return nonEmptyStream(
+                    // the stream supplier
+                    aSnippetSupplier,
+                    // 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)
+            );
+        }
+
+        /**
+         * Given an id1-id2 range specifier, return a stream of snippets within
+         * our context
+         *
+         * @param arg the range arg
+         * @return a Stream of referenced snippets or null if no matches to
+         * specific arg
+         */
+        Stream<T> range(String arg) {
+            int dash = arg.indexOf('-');
+            String iid = arg.substring(0, dash);
+            String tid = arg.substring(dash + 1);
+            int iidx = snippetIndex(iid);
+            if (iidx < 0) {
+                return null;
+            }
+            int tidx = snippetIndex(tid);
+            if (tidx < 0) {
+                return null;
+            }
+            if (tidx < iidx) {
+                errormsg("jshell.err.end.snippet.range.less.than.start", iid, tid);
+                return null;
+            }
+            return allSnippets.subList(iidx, tidx+1).stream();
+        }
+
+        /**
+         * Lazily initialize the id mapping -- needed only for id ranges.
+         */
+        void initIdMapping() {
+            if (allIds == null) {
+                allSnippets = snippetSupplier.get()
+                        .sorted((a, b) -> order(a) - order(b))
+                        .collect(toList());
+                allIds = allSnippets.stream()
+                        .map(sn -> sn.id())
+                        .toArray(n -> new String[n]);
+            }
+        }
+
+        /**
+         * Return all the snippet ids -- within the context, and in order.
+         *
+         * @return the snippet ids
+         */
+        String[] allIds() {
+            initIdMapping();
+            return allIds;
+        }
+
+        /**
+         * Establish an order on snippet ids.  All startup snippets are first,
+         * all error snippets are last -- within that is by snippet number.
+         *
+         * @param id the id string
+         * @return an ordering int
+         */
+        int order(String id) {
+            try {
+                switch (id.charAt(0)) {
+                    case 's':
+                        return Integer.parseInt(id.substring(1));
+                    case 'e':
+                        return 0x40000000 + Integer.parseInt(id.substring(1));
+                    default:
+                        return 0x20000000 + Integer.parseInt(id);
+                }
+            } catch (Exception ex) {
+                return 0x60000000;
+            }
+        }
+
+        /**
+         * Establish an order on snippets, based on its snippet id. All startup
+         * snippets are first, all error snippets are last -- within that is by
+         * snippet number.
+         *
+         * @param sn the id string
+         * @return an ordering int
+         */
+        int order(Snippet sn) {
+            return order(sn.id());
+        }
+
+        /**
+         * Find the index into the parallel allSnippets and allIds structures.
+         *
+         * @param s the snippet id name
+         * @return the index, or, if not found, report the error and return a
+         * negative number
+         */
+        int snippetIndex(String s) {
+            int idx = Arrays.binarySearch(allIds(), 0, allIds().length, s,
+                    (a, b) -> order(a) - order(b));
+            if (idx < 0) {
+                // the id is not in the snippet domain, find the right error to report
+                if (!ID.matcher(s).matches()) {
+                    errormsg("jshell.err.range.requires.id", s);
+                } else {
+                    badSnippetErrormsg(s);
+                }
+            }
+            return idx;
+        }
+
     }
 
     private boolean cmdDrop(String rawargs) {
@@ -2342,24 +2550,13 @@
             errormsg("jshell.err.drop.arg");
             return false;
         }
-        Stream<Snippet> stream = argsToSnippets(this::dropableSnippets, args);
+        Stream<Snippet> stream = new ArgToSnippets<>(this::dropableSnippets).argsToSnippets(args);
         if (stream == null) {
             // Snippet not found. Error already printed
             fluffmsg("jshell.msg.see.classes.etc");
             return false;
         }
-        List<Snippet> 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));
+        stream.forEach(sn -> state.drop(sn).forEach(this::handleEvent));
         return true;
     }
 
@@ -2690,37 +2887,38 @@
     }
 
     private boolean cmdSave(String rawargs) {
-        ArgTokenizer at = new ArgTokenizer("/save", rawargs.trim());
-        at.allowedOptions("-all", "-start", "-history");
-        String filename = at.next();
-        if (filename == null) {
+        // The filename to save to is the last argument, extract it
+        String[] args = rawargs.split("\\s");
+        String filename = args[args.length - 1];
+        if (filename.isEmpty()) {
             errormsg("jshell.err.file.filename", "/save");
             return false;
         }
-        if (!checkOptionsAndRemainingInput(at)) {
-            return false;
-        }
-        if (at.optionCount() > 1) {
-            errormsg("jshell.err.conflicting.options", at.whole());
+        // All the non-filename arguments are the specifier of what to save
+        String srcSpec = Arrays.stream(args, 0, args.length - 1)
+                .collect(Collectors.joining("\n"));
+        // From the what to save specifier, compute the snippets (as a stream)
+        ArgTokenizer at = new ArgTokenizer("/save", srcSpec);
+        at.allowedOptions("-all", "-start", "-history");
+        Stream<Snippet> snippetStream = argsOptionsToSnippets(state::snippets, this::mainActive, at);
+        if (snippetStream == null) {
+            // error occurred, already reported
             return false;
         }
         try (BufferedWriter writer = Files.newBufferedWriter(toPathResolvingUserHome(filename),
                 Charset.defaultCharset(),
                 CREATE, TRUNCATE_EXISTING, WRITE)) {
             if (at.hasOption("-history")) {
+                // they want history (commands and snippets), ignore the snippet stream
                 for (String s : input.currentSessionHistory()) {
                     writer.write(s);
                     writer.write("\n");
                 }
-            } else if (at.hasOption("-start")) {
-                writer.append(startup.toString());
             } else {
-                String sources = (at.hasOption("-all")
-                        ? state.snippets()
-                        : state.snippets().filter(this::mainActive))
+                // write the snippet stream to the file
+                writer.write(snippetStream
                         .map(Snippet::source)
-                        .collect(Collectors.joining("\n"));
-                writer.write(sources);
+                        .collect(Collectors.joining("\n")));
             }
         } catch (FileNotFoundException e) {
             errormsg("jshell.err.file.not.found", "/save", filename, e.getMessage());
@@ -2837,14 +3035,21 @@
         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);
+    /**
+     * Handle snippet reevaluation commands: {@code /<id>}. These commands are a
+     * sequence of ids and id ranges (names are permitted, though not in the
+     * first position. Support for names is purposely not documented).
+     *
+     * @param rawargs the whole command including arguments
+     */
+    private void rerunHistoryEntriesById(String rawargs) {
+        ArgTokenizer at = new ArgTokenizer("/<id>", rawargs.trim().substring(1));
+        at.allowedOptions();
+        Stream<Snippet> stream = argsOptionsToSnippets(state::snippets, sn -> true, at);
+        if (stream != null) {
+            // successfully parsed, rerun snippets
+            stream.forEach(sn -> rerunSnippet(sn));
+        }
     }
 
     private void rerunSnippet(Snippet snippet) {
--- a/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties	Wed Jul 05 23:27:00 2017 +0200
+++ b/langtools/src/jdk.jshell/share/classes/jdk/internal/jshell/tool/resources/l10n.properties	Thu May 18 14:16:25 2017 -0700
@@ -50,7 +50,7 @@
 jshell.err.startup.unexpected.exception = Unexpected exception reading start-up: {0}
 jshell.err.unexpected.exception = Unexpected exception: {0}
 
-jshell.err.no.such.command.or.snippet.id = No such command or snippet id: {0}
+jshell.err.invalid.command = Invalid command: {0}
 jshell.err.command.ambiguous = Command: ''{0}'' is ambiguous: {1}
 jshell.msg.set.restore = Setting new options and restoring state.
 jshell.msg.set.editor.set = Editor set to: {0}
@@ -105,10 +105,13 @@
 Subjects:\n\
 \n
 
+jshell.err.no.snippet.with.id = No snippet with id: {0}
+jshell.err.end.snippet.range.less.than.start = End of snippet range less than start: {0} - {1}
+jshell.err.range.requires.id = Snippet ranges require snippet ids: {0}
+
 jshell.err.drop.arg =\
 In the /drop argument, please specify an import, variable, method, or class to drop.\n\
 Specify by id or name. Use /list to see ids. Use /reset to reset all state.
-jshell.err.drop.ambiguous = The argument references more than one import, variable, method, or class.
 jshell.err.failed = Failed.
 jshell.msg.native.method = Native Method
 jshell.msg.unknown.source = Unknown Source
@@ -225,7 +228,11 @@
 /list <name>\n\t\
     List snippets with the specified name (preference for active snippets)\n\n\
 /list <id>\n\t\
-    List the snippet with the specified snippet id
+    List the snippet with the specified snippet id\n\n\
+/list <id> <id>...\n\t\
+    List the snippets with the specified snippet ids\n\n\
+/list <id>-<id>\n\t\
+    List the snippets within the range of snippet ids
 
 help.edit.summary = edit a source entry referenced by name or id
 help.edit.args = <name or id>
@@ -238,6 +245,10 @@
     Edit the snippet or snippets with the specified name (preference for active snippets)\n\n\
 /edit <id>\n\t\
     Edit the snippet with the specified snippet id\n\n\
+/edit <id> <id>...\n\t\
+    Edit the snippets with the specified snippet ids\n\n\
+/edit <id>-<id>\n\t\
+    Edit the snippets within the range of snippet ids\n\n\
 /edit\n\t\
     Edit the currently active snippets of code that you typed or read with /open
 
@@ -249,7 +260,11 @@
 /drop <name>\n\t\
     Drop the snippet with the specified name\n\n\
 /drop <id>\n\t\
-    Drop the snippet with the specified snippet id
+    Drop the snippet with the specified snippet id\n\n\
+/drop <id> <id>...\n\t\
+    Drop the snippets with the specified snippet ids\n\n\
+/drop <id>-<id>\n\t\
+    Drop the snippets within the range of snippet ids
 
 help.save.summary = Save snippet source to a file.
 help.save.args = [-all|-history|-start] <file>
@@ -264,7 +279,13 @@
 /save -history <file>\n\t\
     Save the sequential history of all commands and snippets entered since jshell was launched.\n\n\
 /save -start <file>\n\t\
-    Save the current start-up definitions to the file.
+    Save the current start-up definitions to the file.\n\n\
+/save <id> <file>\n\t\
+    Save the snippet with the specified snippet id\n\n\
+/save <id> <id>... <file>\n\t\
+    Save the snippets with the specified snippet ids\n\n\
+/save <id>-<id> <file>\n\t\
+    Save the snippets within the range of snippet ids
 
 help.open.summary = open a file as source input
 help.open.args = <file>
@@ -285,6 +306,10 @@
     List jshell variables with the specified name (preference for active variables)\n\n\
 /vars <id>\n\t\
     List the jshell variable with the specified snippet id\n\n\
+/vars <id> <id>... <file>\n\t\
+    List the jshell variables with the specified snippet ids\n\n\
+/vars <id>-<id> <file>\n\t\
+    List the jshell variables within the range of snippet ids\n\n\
 /vars -start\n\t\
     List the automatically added start-up jshell variables\n\n\
 /vars -all\n\t\
@@ -301,6 +326,10 @@
     List jshell methods with the specified name (preference for active methods)\n\n\
 /methods <id>\n\t\
     List the jshell method with the specified snippet id\n\n\
+/methods <id> <id>... <file>\n\t\
+    List jshell methods with the specified snippet ids\n\n\
+/methods <id>-<id> <file>\n\t\
+    List jshell methods within the range of snippet ids\n\n\
 /methods -start\n\t\
     List the automatically added start-up jshell methods\n\n\
 /methods -all\n\t\
@@ -317,6 +346,10 @@
     List jshell types with the specified name (preference for active types)\n\n\
 /types <id>\n\t\
     List the jshell type with the specified snippet id\n\n\
+/types <id> <id>... <file>\n\t\
+    List jshell types with the specified snippet ids\n\n\
+/types <id>-<id> <file>\n\t\
+    List jshell types within the range of snippet ids\n\n\
 /types -start\n\t\
     List the automatically added start-up jshell types\n\n\
 /types -all\n\t\
@@ -461,17 +494,24 @@
 /? <subject>\n\t\
      Display information about the specified help subject. Example: /? intro
 
-help.bang.summary = re-run last snippet
+help.bang.summary = rerun last snippet -- see /help rerun
 help.bang.args =
 help.bang =\
 Reevaluate the most recently entered snippet.
 
-help.id.summary = re-run snippet by id
+help.id.summary = rerun snippets by id or id range -- see /help rerun
 help.id.args =
 help.id =\
-Reevaluate the snippet specified by the id.
+/<id> <id> <id>\n\
+\n\
+/<id>-<id>\n\
+\n\
+Reevaluate the snippets specified by the id or id range.\n\
+An id range is represented as a two ids separated by a hyphen, e.g.:  3-17\n\
+Start-up and error snippets maybe used, e.g.:  s3-s9    or   e1-e4\n\
+Any number of ids or id ranges may be used, e.g.:  /3-7 s4 14-16 e2
 
-help.previous.summary = re-run n-th previous snippet
+help.previous.summary = rerun n-th previous snippet -- see /help rerun
 help.previous.args =
 help.previous =\
 Reevaluate the n-th most recently entered snippet.
@@ -509,7 +549,7 @@
         then release and press "i", and jshell will propose possible imports\n\t\t\
         which will resolve the identifier based on the content of the specified classpath.
 
-help.context.summary = the evaluation context options for /env /reload and /reset
+help.context.summary = a description of the evaluation context options for /env /reload and /reset
 help.context =\
 These options configure the evaluation context, they can be specified when\n\
 jshell is started: on the command-line, or restarted with the commands /env,\n\
@@ -540,6 +580,38 @@
 On the command-line these options must have two dashes, e.g.: --module-path\n\
 On jshell commands they can have one or two dashes, e.g.: -module-path\n\
 
+help.rerun.summary = a description of ways to re-evaluate previously entered snippets
+help.rerun =\
+There are four ways to re-evaluate previously entered snippets.\n\
+The last snippet can be re-evaluated using: /!\n\
+The n-th previous snippet can be re-evaluated by slash-minus and the digits of n, e.g.:  /-4\n\
+For example:\n\
+\n\
+    \tjshell> 2 + 2\n\
+    \t$1 ==> 4\n\
+\n\
+    \tjshell> /!\n\
+    \t2 + 2\n\
+    \t$2 ==> 4\n\
+\n\
+    \tjshell> int z\n\
+    \tz ==> 0\n\
+\n\
+    \tjshell> /-1\n\
+    \tint z;\n\
+    \tz ==> 0\n\
+\n\
+    \tjshell> /-4\n\
+    \t2 + 2\n\
+    \t$5 ==> 4\n\
+\n\
+The snippets to re-evaluate may be specified by snippet id or id range.\n\
+An id range is represented as a two ids separated by a hyphen, e.g.:  3-17\n\
+Start-up and error snippets maybe used, e.g.:  s3-s9    or   e1-e4\n\
+Any number of ids or id ranges may be used, e.g.:  /3-7 s4 14-16 e2\n\
+\n\
+Finally, you can search backwards through history by entering ctrl-R followed by the string to search for.
+
 help.set._retain = \
 The '-retain' option saves a setting so that it is used in future sessions.\n\
 The -retain option can be used on the following forms of /set:\n\n\t\
--- a/langtools/test/jdk/jshell/CommandCompletionTest.java	Wed Jul 05 23:27:00 2017 +0200
+++ b/langtools/test/jdk/jshell/CommandCompletionTest.java	Thu May 18 14:16:25 2017 -0700
@@ -23,7 +23,7 @@
 
 /*
  * @test
- * @bug 8144095 8164825 8169818 8153402 8165405 8177079 8178013
+ * @bug 8144095 8164825 8169818 8153402 8165405 8177079 8178013 8167554
  * @summary Test Command Completion
  * @modules jdk.compiler/com.sun.tools.javac.api
  *          jdk.compiler/com.sun.tools.javac.main
@@ -162,13 +162,13 @@
                 "/edit ", "/env ", "/exit ",
                 "/help ", "/history ", "/imports ",
                 "/list ", "/methods ", "/open ", "/reload ", "/reset ",
-                "/save ", "/set ", "/types ", "/vars ", "context ", "intro ", "shortcuts "),
+                "/save ", "/set ", "/types ", "/vars ", "context ", "intro ", "rerun ", "shortcuts "),
                 a -> assertCompletion(a, "/? |", false,
                 "/! ", "/-<n> ", "/<id> ", "/? ", "/drop ",
                 "/edit ", "/env ", "/exit ",
                 "/help ", "/history ", "/imports ",
                 "/list ", "/methods ", "/open ", "/reload ", "/reset ",
-                "/save ", "/set ", "/types ", "/vars ", "context ", "intro ", "shortcuts "),
+                "/save ", "/set ", "/types ", "/vars ", "context ", "intro ", "rerun ", "shortcuts "),
                 a -> assertCompletion(a, "/help /s|", false,
                 "/save ", "/set "),
                 a -> assertCompletion(a, "/help /set |", false,
--- a/langtools/test/jdk/jshell/EditorTestBase.java	Wed Jul 05 23:27:00 2017 +0200
+++ b/langtools/test/jdk/jshell/EditorTestBase.java	Thu May 18 14:16:25 2017 -0700
@@ -73,7 +73,7 @@
         for (String edit : new String[] {"/ed", "/edit"}) {
             test(new String[]{"--no-startup"},
                     a -> assertCommandOutputStartsWith(a, edit + " 1",
-                            "|  No such snippet: 1"),
+                            "|  No snippet with id: 1"),
                     a -> assertCommandOutputStartsWith(a, edit + " unknown",
                             "|  No such snippet: unknown")
             );
--- a/langtools/test/jdk/jshell/MergedTabShiftTabCommandTest.java	Wed Jul 05 23:27:00 2017 +0200
+++ b/langtools/test/jdk/jshell/MergedTabShiftTabCommandTest.java	Thu May 18 14:16:25 2017 -0700
@@ -66,17 +66,17 @@
                             Pattern.quote(getResource("jshell.console.see.next.command.doc")) + "\n" +
                             "\r\u0005/");
 
-            inputSink.write("lis\011");
-            waitOutput(out, "list $");
+            inputSink.write("ed\011");
+            waitOutput(out, "edit $");
 
             inputSink.write("\011");
             waitOutput(out, ".*-all.*" +
                             "\n\n" + Pattern.quote(getResource("jshell.console.see.synopsis")) + "\n\r\u0005/");
             inputSink.write("\011");
-            waitOutput(out, Pattern.quote(getResource("help.list.summary")) + "\n\n" +
-                            Pattern.quote(getResource("jshell.console.see.full.documentation")) + "\n\r\u0005/list ");
+            waitOutput(out, Pattern.quote(getResource("help.edit.summary")) + "\n\n" +
+                            Pattern.quote(getResource("jshell.console.see.full.documentation")) + "\n\r\u0005/edit ");
             inputSink.write("\011");
-            waitOutput(out, Pattern.quote(getResource("help.list").replaceAll("\t", "    ")));
+            waitOutput(out, Pattern.quote(getResource("help.edit").replaceAll("\t", "    ")));
 
             inputSink.write("\u0003/env \011");
             waitOutput(out, "\u0005/env -\n" +
--- a/langtools/test/jdk/jshell/ToolBasicTest.java	Wed Jul 05 23:27:00 2017 +0200
+++ b/langtools/test/jdk/jshell/ToolBasicTest.java	Thu May 18 14:16:25 2017 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2017, 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
@@ -23,7 +23,7 @@
 
 /*
  * @test
- * @bug 8143037 8142447 8144095 8140265 8144906 8146138 8147887 8147886 8148316 8148317 8143955 8157953 8080347 8154714 8166649 8167643 8170162 8172102 8165405 8174796 8174797 8175304
+ * @bug 8143037 8142447 8144095 8140265 8144906 8146138 8147887 8147886 8148316 8148317 8143955 8157953 8080347 8154714 8166649 8167643 8170162 8172102 8165405 8174796 8174797 8175304 8167554 8180508
  * @summary Tests for Basic tests for REPL tool
  * @modules jdk.compiler/com.sun.tools.javac.api
  *          jdk.compiler/com.sun.tools.javac.main
@@ -190,8 +190,8 @@
 
     public void testRerun() {
         test(false, new String[] {"--no-startup"},
-                (a) -> assertCommand(a, "/0", "|  No such command or snippet id: /0\n|  Type /help for help."),
-                (a) -> assertCommand(a, "/5", "|  No such command or snippet id: /5\n|  Type /help for help.")
+                (a) -> assertCommand(a, "/0", "|  No snippet with id: 0"),
+                (a) -> assertCommand(a, "/5", "|  No snippet with id: 5")
         );
         String[] codes = new String[] {
                 "int a = 0;", // var
@@ -252,9 +252,9 @@
         );
 
         test(false, new String[] {"--no-startup"},
-                (a) -> assertCommand(a, "/s1", "|  No such command or snippet id: /s1\n|  Type /help for help."),
-                (a) -> assertCommand(a, "/1", "|  No such command or snippet id: /1\n|  Type /help for help."),
-                (a) -> assertCommand(a, "/e1", "|  No such command or snippet id: /e1\n|  Type /help for help.")
+                (a) -> assertCommand(a, "/s1", "|  No snippet with id: s1"),
+                (a) -> assertCommand(a, "/1", "|  No snippet with id: 1"),
+                (a) -> assertCommand(a, "/e1", "|  No snippet with id: e1")
         );
     }
 
@@ -481,17 +481,19 @@
     public void testSave() throws IOException {
         Compiler compiler = new Compiler();
         Path path = compiler.getPath("testSave.repl");
-        List<String> list = Arrays.asList(
-                "int a;",
-                "class A { public String toString() { return \"A\"; } }"
-        );
-        test(
-                (a) -> assertVariable(a, "int", "a"),
-                (a) -> assertCommand(a, "()", null, null, null, "", ""),
-                (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
-                (a) -> assertCommand(a, "/save " + path.toString(), "")
-        );
-        assertEquals(Files.readAllLines(path), list);
+        {
+            List<String> list = Arrays.asList(
+                    "int a;",
+                    "class A { public String toString() { return \"A\"; } }"
+            );
+            test(
+                    (a) -> assertVariable(a, "int", "a"),
+                    (a) -> assertCommand(a, "()", null, null, null, "", ""),
+                    (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
+                    (a) -> assertCommand(a, "/save " + path.toString(), "")
+            );
+            assertEquals(Files.readAllLines(path), list);
+        }
         {
             List<String> output = new ArrayList<>();
             test(
@@ -499,28 +501,47 @@
                     (a) -> assertCommand(a, "()", null, null, null, "", ""),
                     (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
                     (a) -> assertCommandCheckOutput(a, "/list -all", (out) ->
-                            output.addAll(Stream.of(out.split("\n"))
-                                    .filter(str -> !str.isEmpty())
-                                    .map(str -> str.substring(str.indexOf(':') + 2))
-                                    .filter(str -> !str.startsWith("/"))
-                                    .collect(Collectors.toList()))),
+                                    output.addAll(Stream.of(out.split("\n"))
+                            .filter(str -> !str.isEmpty())
+                            .map(str -> str.substring(str.indexOf(':') + 2))
+                            .filter(str -> !str.startsWith("/"))
+                            .collect(Collectors.toList()))),
                     (a) -> assertCommand(a, "/save -all " + path.toString(), "")
             );
             assertEquals(Files.readAllLines(path), output);
         }
-        List<String> output = new ArrayList<>();
-        test(
-                (a) -> assertVariable(a, "int", "a"),
-                (a) -> assertCommand(a, "()", null, null, null, "", ""),
-                (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
-                (a) -> assertCommandCheckOutput(a, "/history", (out) ->
-                        output.addAll(Stream.of(out.split("\n"))
-                                .filter(str -> !str.isEmpty())
-                                .collect(Collectors.toList()))),
-                (a) -> assertCommand(a, "/save -history " + path.toString(), "")
-        );
-        output.add("/save -history " + path.toString());
-        assertEquals(Files.readAllLines(path), output);
+        {
+            List<String> output = new ArrayList<>();
+            test(
+                    (a) -> assertCommand(a, "int a;", null),
+                    (a) -> assertCommand(a, "int b;", null),
+                    (a) -> assertCommand(a, "int c;", null),
+                    (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
+                    (a) -> assertCommandCheckOutput(a, "/list b c a A", (out) ->
+                                    output.addAll(Stream.of(out.split("\n"))
+                            .filter(str -> !str.isEmpty())
+                            .map(str -> str.substring(str.indexOf(':') + 2))
+                            .filter(str -> !str.startsWith("/"))
+                            .collect(Collectors.toList()))),
+                    (a) -> assertCommand(a, "/save 2-3 1 4 " + path.toString(), "")
+            );
+            assertEquals(Files.readAllLines(path), output);
+        }
+        {
+            List<String> output = new ArrayList<>();
+            test(
+                    (a) -> assertVariable(a, "int", "a"),
+                    (a) -> assertCommand(a, "()", null, null, null, "", ""),
+                    (a) -> assertClass(a, "class A { public String toString() { return \"A\"; } }", "class", "A"),
+                    (a) -> assertCommandCheckOutput(a, "/history", (out) ->
+                                output.addAll(Stream.of(out.split("\n"))
+                            .filter(str -> !str.isEmpty())
+                            .collect(Collectors.toList()))),
+                    (a) -> assertCommand(a, "/save -history " + path.toString(), "")
+            );
+            output.add("/save -history " + path.toString());
+            assertEquals(Files.readAllLines(path), output);
+        }
     }
 
     public void testStartRetain() {
@@ -652,6 +673,64 @@
         );
     }
 
+    public void testRerunIdRange() {
+        Compiler compiler = new Compiler();
+        Path startup = compiler.getPath("rangeStartup");
+        String[] startupSources = new String[] {
+            "boolean go = false",
+            "void println(String s) { if (go) System.out.println(s); }",
+            "void println(int i) { if (go) System.out.println(i); }",
+            "println(\"s4\")",
+            "println(\"s5\")",
+            "println(\"s6\")"
+        };
+        String[] sources = new String[] {
+            "frog",
+            "go = true",
+            "println(2)",
+            "println(3)",
+            "println(4)",
+            "querty"
+        };
+        compiler.writeToFile(startup, startupSources);
+        test(false, new String[]{"--startup", startup.toString()},
+                a -> assertCommandOutputStartsWith(a, sources[0], "|  Error:"),
+                a -> assertCommand(a, sources[1], "go ==> true", "", null, "", ""),
+                a -> assertCommand(a, sources[2], "", "", null, "2\n", ""),
+                a -> assertCommand(a, sources[3], "", "", null, "3\n", ""),
+                a -> assertCommand(a, sources[4], "", "", null, "4\n", ""),
+                a -> assertCommandOutputStartsWith(a, sources[5], "|  Error:"),
+                a -> assertCommand(a, "/3", "println(3)", "", null, "3\n", ""),
+                a -> assertCommand(a, "/s4", "println(\"s4\")", "", null, "s4\n", ""),
+                a -> assertCommandOutputStartsWith(a, "/e1", "frog\n|  Error:"),
+                a -> assertCommand(a, "/2-4",
+                        "println(2)\nprintln(3)\nprintln(4)",
+                        "", null, "2\n3\n4\n", ""),
+                a -> assertCommand(a, "/s4-s6",
+                        startupSources[3] + "\n" +startupSources[4] + "\n" +startupSources[5],
+                        "", null, "s4\ns5\ns6\n", ""),
+                a -> assertCommand(a, "/s4-4", null,
+                        "", null, "s4\ns5\ns6\n2\n3\n4\n", ""),
+                a -> assertCommandCheckOutput(a, "/e1-e2",
+                        s -> {
+                            assertTrue(s.trim().startsWith("frog\n|  Error:"),
+                                    "Output: \'" + s + "' does not start with: " + "|  Error:");
+                            assertTrue(s.trim().lastIndexOf("|  Error:") > 10,
+                                    "Output: \'" + s + "' does not have second: " + "|  Error:");
+                        }),
+                a -> assertCommand(a, "/4  s4 2",
+                        "println(4)\nprintln(\"s4\")\nprintln(2)",
+                        "", null, "4\ns4\n2\n", ""),
+                a -> assertCommand(a, "/s5 2-4 3",
+                        "println(\"s5\")\nprintln(2)\nprintln(3)\nprintln(4)\nprintln(3)",
+                        "", null, "s5\n2\n3\n4\n3\n", ""),
+                a -> assertCommand(a, "/2 ff", "|  No such snippet: ff"),
+                a -> assertCommand(a, "/4-2", "|  End of snippet range less than start: 4 - 2"),
+                a -> assertCommand(a, "/s5-s3", "|  End of snippet range less than start: s5 - s3"),
+                a -> assertCommand(a, "/4-s5", "|  End of snippet range less than start: 4 - s5")
+        );
+    }
+
     @Test(enabled = false) // TODO 8158197
     public void testHeadlessEditPad() {
         String prevHeadless = System.getProperty("java.awt.headless");
--- a/langtools/test/jdk/jshell/ToolLocaleMessageTest.java	Wed Jul 05 23:27:00 2017 +0200
+++ b/langtools/test/jdk/jshell/ToolLocaleMessageTest.java	Thu May 18 14:16:25 2017 -0700
@@ -117,7 +117,6 @@
                     (a) -> assertCommandFail(a, "/drop rats"),
                     (a) -> assertCommandOK(a, "void dup() {}"),
                     (a) -> assertCommandOK(a, "int dup"),
-                    (a) -> assertCommandFail(a, "/drop dup"),
                     (a) -> assertCommandFail(a, "/edit zebra", "zebra"),
                     (a) -> assertCommandFail(a, "/list zebra", "zebra", "No such snippet: zebra"),
                     (a) -> assertCommandFail(a, "/open", "/open"),
--- a/langtools/test/jdk/jshell/ToolSimpleTest.java	Wed Jul 05 23:27:00 2017 +0200
+++ b/langtools/test/jdk/jshell/ToolSimpleTest.java	Thu May 18 14:16:25 2017 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2017, 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
@@ -23,7 +23,7 @@
 
 /*
  * @test
- * @bug 8153716 8143955 8151754 8150382 8153920 8156910 8131024 8160089 8153897 8167128 8154513 8170015 8170368 8172102 8172103  8165405 8173073 8173848 8174041 8173916 8174028 8174262 8174797 8177079
+ * @bug 8153716 8143955 8151754 8150382 8153920 8156910 8131024 8160089 8153897 8167128 8154513 8170015 8170368 8172102 8172103  8165405 8173073 8173848 8174041 8173916 8174028 8174262 8174797 8177079 8180508
  * @summary Simple jshell tool tests
  * @modules jdk.compiler/com.sun.tools.javac.api
  *          jdk.compiler/com.sun.tools.javac.main
@@ -37,6 +37,7 @@
 import java.util.List;
 import java.util.Locale;
 import java.util.function.Consumer;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -202,7 +203,7 @@
     @Test
     public void testUnknownCommand() {
         test((a) -> assertCommand(a, "/unknown",
-                "|  No such command or snippet id: /unknown\n" +
+                "|  Invalid command: /unknown\n" +
                 "|  Type /help for help."));
     }
 
@@ -275,9 +276,25 @@
     }
 
     @Test
+    public void testDropRange() {
+        test(false, new String[]{"--no-startup"},
+                a -> assertVariable(a, "int", "a"),
+                a -> assertMethod(a, "int b() { return 0; }", "()int", "b"),
+                a -> assertClass(a, "class A {}", "class", "A"),
+                a -> assertImport(a, "import java.util.stream.*;", "", "java.util.stream.*"),
+                a -> assertCommand(a, "for (int i = 0; i < 10; ++i) {}", ""),
+                a -> assertCommand(a, "/drop 3-5 b 1",
+                        "|  dropped class A\n" +
+                        "|  dropped method b()\n" +
+                        "|  dropped variable a\n"),
+                a -> assertCommand(a, "/list", "")
+        );
+    }
+
+    @Test
     public void testDropNegative() {
         test(false, new String[]{"--no-startup"},
-                a -> assertCommandOutputStartsWith(a, "/drop 0", "|  No such snippet: 0"),
+                a -> assertCommandOutputStartsWith(a, "/drop 0", "|  No snippet with id: 0"),
                 a -> assertCommandOutputStartsWith(a, "/drop a", "|  No such snippet: a"),
                 a -> assertCommandCheckOutput(a, "/drop",
                         assertStartsWith("|  In the /drop argument, please specify an import, variable, method, or class to drop.")),
@@ -292,27 +309,23 @@
 
     @Test
     public void testAmbiguousDrop() {
-        Consumer<String> check = s -> {
-            assertTrue(s.startsWith("|  The argument references more than one import, variable, method, or class"), s);
-            int lines = s.split("\n").length;
-            assertEquals(lines, 5, "Expected 3 ambiguous keys, but found: " + (lines - 2) + "\n" + s);
-        };
         test(
                 a -> assertVariable(a, "int", "a"),
                 a -> assertMethod(a, "int a() { return 0; }", "()int", "a"),
                 a -> assertClass(a, "class a {}", "class", "a"),
-                a -> assertCommandCheckOutput(a, "/drop a", check),
-                a -> assertCommandCheckOutput(a, "/vars", assertVariables()),
-                a -> assertCommandCheckOutput(a, "/methods", assertMethods()),
-                a -> assertCommandCheckOutput(a, "/types", assertClasses()),
-                a -> assertCommandCheckOutput(a, "/imports", assertImports())
+                a -> assertCommand(a, "/drop a",
+                        "|  dropped variable a\n" +
+                        "|  dropped method a()\n" +
+                        "|  dropped class a")
         );
         test(
                 a -> assertMethod(a, "int a() { return 0; }", "()int", "a"),
                 a -> assertMethod(a, "double a(int a) { return 0; }", "(int)double", "a"),
                 a -> assertMethod(a, "double a(double a) { return 0; }", "(double)double", "a"),
-                a -> assertCommandCheckOutput(a, "/drop a", check),
-                a -> assertCommandCheckOutput(a, "/methods", assertMethods())
+                a -> assertCommand(a, "/drop a",
+                        "|  dropped method a()\n" +
+                        "|  dropped method a(int)\n" +
+                        "|  dropped method a(double)\n")
         );
     }
 
@@ -402,12 +415,14 @@
         String arg = "qqqq";
         List<String> startVarList = new ArrayList<>(START_UP);
         startVarList.add("int aardvark");
+        startVarList.add("int weevil");
         test(
                 a -> assertCommandCheckOutput(a, "/list -all",
                         s -> checkLineToList(s, START_UP)),
                 a -> assertCommandOutputStartsWith(a, "/list " + arg,
                         "|  No such snippet: " + arg),
                 a -> assertVariable(a, "int", "aardvark"),
+                a -> assertVariable(a, "int", "weevil"),
                 a -> assertCommandOutputContains(a, "/list aardvark", "aardvark"),
                 a -> assertCommandCheckOutput(a, "/list -start",
                         s -> checkLineToList(s, START_UP)),
@@ -415,6 +430,11 @@
                         s -> checkLineToList(s, startVarList)),
                 a -> assertCommandOutputStartsWith(a, "/list s3",
                         "s3 : import"),
+                a -> assertCommandCheckOutput(a, "/list 1-2 s3",
+                        s -> {
+                            assertTrue(Pattern.matches(".*aardvark.*\\R.*weevil.*\\R.*s3.*import.*", s.trim()),
+                                    "No match: " + s);
+                        }),
                 a -> assertCommandOutputStartsWith(a, "/list " + arg,
                         "|  No such snippet: " + arg)
         );
@@ -439,6 +459,8 @@
                         s -> checkLineToList(s, startVarList)),
                 a -> assertCommandOutputStartsWith(a, "/vars -all",
                         "|    int aardvark = 0\n|    int a = "),
+                a -> assertCommandOutputStartsWith(a, "/vars 1-4",
+                        "|    int aardvark = 0\n|    int a = "),
                 a -> assertCommandOutputStartsWith(a, "/vars f",
                         "|  This command does not accept the snippet 'f'"),
                 a -> assertCommand(a, "/var " + arg,