]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/commands.jsm
Imported Upstream version 1.1+hg7904
[dactyl.git] / common / modules / commands.jsm
index 7d7093e99a41befb806ccd157b0a3dd2b3cb6c86..0fab7bcbe4d4ecbe8160ecff6d6b949226e0c1f8 100644 (file)
@@ -1,6 +1,6 @@
 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
-// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
+// Copyright (c) 2008-2014 Kris Maglione <maglione.k at Gmail>
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this file.
@@ -8,12 +8,14 @@
 
 try {
 
-Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("commands", {
     exports: ["ArgType", "Command", "Commands", "CommandOption", "Ex", "commands"],
-    require: ["contexts", "messages", "util"],
-    use: ["config", "options", "services", "template"]
-}, this);
+    require: ["contexts", "messages", "util"]
+});
+
+lazyRequire("help", ["help"]);
+lazyRequire("options", ["Option"]);
+lazyRequire("template", ["template"]);
 
 /**
  * A structure representing the options available for a command.
@@ -26,6 +28,7 @@ defineModule("commands", {
  * @property {number} type The option's value type. This is one of:
  *         (@link CommandOption.NOARG),
  *         (@link CommandOption.STRING),
+ *         (@link CommandOption.STRINGMAP),
  *         (@link CommandOption.BOOL),
  *         (@link CommandOption.INT),
  *         (@link CommandOption.FLOAT),
@@ -43,9 +46,9 @@ defineModule("commands", {
  */
 
 var CommandOption = Struct("names", "type", "validator", "completer", "multiple", "description", "default");
-CommandOption.defaultValue("description", function () "");
-CommandOption.defaultValue("type", function () CommandOption.NOARG);
-CommandOption.defaultValue("multiple", function () false);
+CommandOption.defaultValue("description", () => "");
+CommandOption.defaultValue("type", () => CommandOption.NOARG);
+CommandOption.defaultValue("multiple", () => false);
 
 var ArgType = Struct("description", "parse");
 update(CommandOption, {
@@ -61,7 +64,7 @@ update(CommandOption, {
      * @property {object} The option doesn't accept an argument.
      * @final
      */
-    NOARG: ArgType("no arg",  function (arg) !arg || null),
+    NOARG: ArgType("no arg",  arg => !arg || null),
     /**
      * @property {object} The option accepts a boolean argument.
      * @final
@@ -71,7 +74,12 @@ update(CommandOption, {
      * @property {object} The option accepts a string argument.
      * @final
      */
-    STRING: ArgType("string", function (val) val),
+    STRING: ArgType("string", val => val),
+    /**
+     * @property {object} The option accepts a stringmap argument.
+     * @final
+     */
+    STRINGMAP: ArgType("stringmap", (val, quoted) => Option.parse.stringmap(quoted)),
     /**
      * @property {object} The option accepts an integer argument.
      * @final
@@ -101,6 +109,7 @@ update(CommandOption, {
  * @param {function} action The action invoked by this command when executed.
  * @param {Object} extraInfo An optional extra configuration hash. The
  *     following properties are supported.
+ *         always      - see {@link Command#always}
  *         argCount    - see {@link Command#argCount}
  *         bang        - see {@link Command#bang}
  *         completer   - see {@link Command#completer}
@@ -118,22 +127,17 @@ update(CommandOption, {
 var Command = Class("Command", {
     init: function init(specs, description, action, extraInfo) {
         specs = Array.concat(specs); // XXX
-        let parsedSpecs = extraInfo.parsedSpecs || Command.parseSpecs(specs);
 
         this.specs = specs;
-        this.shortNames = array.compact(parsedSpecs.map(function (n) n[1]));
-        this.longNames = parsedSpecs.map(function (n) n[0]);
-        this.name = this.longNames[0];
-        this.names = array.flatten(parsedSpecs);
         this.description = description;
         this.action = action;
 
+        if (extraInfo.options)
+            this._options = extraInfo.options;
+        delete extraInfo.options;
+
         if (extraInfo)
             this.update(extraInfo);
-        if (this.options)
-            this.options = this.options.map(CommandOption.fromArray, CommandOption);
-        for each (let option in this.options)
-            option.localeName = ["command", this.name, option.names[0]];
     },
 
     get toStringParams() [this.name, this.hive.name],
@@ -151,22 +155,23 @@ var Command = Class("Command", {
      * @param {Args} args The Args object passed to {@link #action}.
      * @param {Object} modifiers Any modifiers to be passed to {@link #action}.
      */
-    execute: function execute(args, modifiers) {
+    execute: function execute(args, modifiers={}) {
         const { dactyl } = this.modules;
 
         let context = args.context;
         if (this.deprecated)
             this.warn(context, "deprecated", _("warn.deprecated", ":" + this.name, this.deprecated));
 
-        modifiers = modifiers || {};
-
         if (args.count != null && !this.count)
             throw FailedAssertion(_("command.noCount"));
         if (args.bang && !this.bang)
             throw FailedAssertion(_("command.noBang"));
 
+        args.doc = this.hive.group.lastDocument;
+
         return !dactyl.trapErrors(function exec() {
             let extra = this.hive.argsExtra(args);
+
             for (let k in properties(extra))
                 if (!(k in args))
                     Object.defineProperty(args, k, Object.getOwnPropertyDescriptor(extra, k));
@@ -185,8 +190,7 @@ var Command = Class("Command", {
      * @param {string} name The candidate name.
      * @returns {boolean}
      */
-    hasName: function hasName(name) this.parsedSpecs.some(
-        function ([long, short]) name.indexOf(short) == 0 && long.indexOf(name) == 0),
+    hasName: function hasName(name) Command.hasName(this.parsedSpecs, name),
 
     /**
      * A helper function to parse an argument string.
@@ -206,23 +210,27 @@ var Command = Class("Command", {
         extra: extra
     }),
 
-    complained: Class.memoize(function () ({})),
+    complained: Class.Memoize(function () RealSet()),
 
     /**
      * @property {[string]} All of this command's name specs. e.g., "com[mand]"
      */
     specs: null,
+    parsedSpecs: Class.Memoize(function () Command.parseSpecs(this.specs)),
+
     /** @property {[string]} All of this command's short names, e.g., "com" */
-    shortNames: null,
+    shortNames: Class.Memoize(function () array.compact(this.parsedSpecs.map(n => n[1]))),
+
     /**
      * @property {[string]} All of this command's long names, e.g., "command"
      */
-    longNames: null,
+    longNames: Class.Memoize(function () this.parsedSpecs.map(n => n[0])),
 
     /** @property {string} The command's canonical name. */
-    name: null,
+    name: Class.Memoize(function () this.longNames[0]),
+
     /** @property {[string]} All of this command's long and short names. */
-    names: null,
+    names: Class.Memoize(function () this.names = array.flatten(this.parsedSpecs)),
 
     /** @property {string} This command's description, as shown in :listcommands */
     description: Messages.Localized(""),
@@ -234,28 +242,41 @@ var Command = Class("Command", {
      * @property {function (Args)} The function called to execute this command.
      */
     action: null,
+
+    /**
+     * @property {function (Args)} A function which is called when this
+     * command is encountered, even if we are ignoring commands. Used to
+     * implement control structures.
+     */
+    always: null,
+
     /**
      * @property {string} This command's argument count spec.
      * @see Commands#parseArguments
      */
     argCount: 0,
+
     /**
      * @property {function (CompletionContext, Args)} This command's completer.
      * @see CompletionContext
      */
     completer: null,
+
     /** @property {boolean} Whether this command accepts a here document. */
     hereDoc: false,
+
     /**
      * @property {boolean} Whether this command may be called with a bang,
      *     e.g., :com!
      */
     bang: false,
+
     /**
      * @property {boolean} Whether this command may be called with a count,
      *     e.g., :12bdel
      */
     count: false,
+
     /**
      * @property {function(args)} A function which should return a list
      *     of domains referenced in the given args. Used in determining
@@ -263,6 +284,7 @@ var Command = Class("Command", {
      *     private data.
      */
     domains: function (args) [],
+
     /**
      * @property {boolean} At what index this command's literal arguments
      *     begin. For instance, with a value of 2, all arguments starting with
@@ -271,14 +293,21 @@ var Command = Class("Command", {
      *     key mappings or Ex command lines as arguments.
      */
     literal: null,
+
     /**
      * @property {Array} The options this command takes.
      * @see Commands@parseArguments
      */
-    options: [],
+    options: Class.Memoize(function ()
+        this._options.map(function (opt) {
+            let option = CommandOption.fromArray(opt);
+            option.localeName = ["command", this.name, option.names[0]];
+            return option;
+        }, this)),
+    _options: [],
 
-    optionMap: Class.memoize(function () array(this.options)
-                .map(function (opt) opt.names.map(function (name) [name, opt]))
+    optionMap: Class.Memoize(function () array(this.options)
+                .map(opt => opt.names.map(name => [name, opt]))
                 .flatten().toObject()),
 
     newArgs: function newArgs(base) {
@@ -288,19 +317,20 @@ var Command = Class("Command", {
         return res;
     },
 
-    argsPrototype: Class.memoize(function argsPrototype() {
+    argsPrototype: Class.Memoize(function argsPrototype() {
         let res = update([], {
                 __iterator__: function AP__iterator__() array.iterItems(this),
 
                 command: this,
 
-                explicitOpts: Class.memoize(function () ({})),
+                explicitOpts: Class.Memoize(function () ({})),
 
-                has: function AP_has(opt) Set.has(this.explicitOpts, opt) || typeof opt === "number" && Set.has(this, opt),
+                has: function AP_has(opt) hasOwnProperty(this.explicitOpts, opt)
+                                       || typeof opt === "number" && hasOwnProperty(this, opt),
 
                 get literalArg() this.command.literal != null && this[this.command.literal] || "",
 
-                // TODO: string: Class.memoize(function () { ... }),
+                // TODO: string: Class.Memoize(function () { ... }),
 
                 verify: function verify() {
                     if (this.command.argCount) {
@@ -316,10 +346,14 @@ var Command = Class("Command", {
         });
 
         this.options.forEach(function (opt) {
-            if (opt.default !== undefined)
-                Object.defineProperty(res, opt.names[0],
-                                      Object.getOwnPropertyDescriptor(opt, "default") ||
-                                          { configurable: true, enumerable: true, get: function () opt.default });
+            if (opt.default !== undefined) {
+                let prop = Object.getOwnPropertyDescriptor(opt, "default") ||
+                    { configurable: true, enumerable: true, get: function () opt.default };
+
+                if (prop.get && !prop.set)
+                    prop.set = function (val) { Class.replaceProperty(this, opt.names[0], val); };
+                Object.defineProperty(res, opt.names[0], prop);
+            }
         });
 
         return res;
@@ -369,10 +403,16 @@ var Command = Class("Command", {
     warn: function warn(context, type, message) {
         let loc = !context ? "" : [context.file, context.line, " "].join(":");
 
-        if (!Set.add(this.complained, type + ":" + (context ? context.file : "[Command Line]")))
+        let key = type + ":" + (context ? context.file : "[Command Line]");
+
+        if (!this.complained.add(key))
             this.modules.dactyl.warn(loc + message);
     }
 }, {
+    hasName: function hasName(specs, name)
+        specs.some(([long, short]) =>
+            name.indexOf(short) == 0 && long.indexOf(name) == 0),
+
     // TODO: do we really need more than longNames as a convenience anyway?
     /**
      *  Converts command name abbreviation specs of the form
@@ -450,34 +490,78 @@ var Ex = Module("Ex", {
 var CommandHive = Class("CommandHive", Contexts.Hive, {
     init: function init(group) {
         init.supercall(this, group);
+
         this._map = {};
         this._list = [];
+        this._specs = [];
     },
 
+    /**
+     * Caches this command hive.
+     */
+
+    cache: function cache() {
+        let { cache } = this.modules;
+        this.cached = true;
+
+        let cached = cache.get(this.cacheKey, () => {
+            this.cached = false;
+            this.modules.moduleManager.initDependencies("commands");
+
+            let map = {};
+            for (let [name, cmd] in Iterator(this._map))
+                if (cmd.sourceModule)
+                    map[name] = { sourceModule: cmd.sourceModule, isPlaceholder: true };
+
+            let specs = [];
+            for (let cmd of this._list)
+                for (let spec of cmd.parsedSpecs)
+                    specs.push(spec.concat(cmd.name));
+
+            return { map: map, specs: specs };
+        });
+
+        let cached = cache.get(this.cacheKey);
+        if (this.cached) {
+            this._specs = cached.specs;
+            for (let [k, v] in Iterator(cached.map))
+                this._map[k] = v;
+        }
+    },
+
+    get cacheKey() "commands/hives/" + this.name + ".json",
+
     /** @property {Iterator(Command)} @private */
-    __iterator__: function __iterator__() array.iterValues(this._list.sort(function (a, b) a.name > b.name)),
+    __iterator__: function __iterator__() {
+        if (this.cached)
+            this.modules.initDependencies("commands");
+        this.cached = false;
+        return array.iterValues(this._list.sort((a, b) => a.name > b.name));
+    },
 
     /** @property {string} The last executed Ex command line. */
     repeat: null,
 
     /**
-     * Adds a new command to the builtin hive. Accessible only to core
-     * dactyl code. Plugins should use group.commands.add instead.
+     * Adds a new command to the builtin hive. Accessible only to core dactyl
+     * code. Plugins should use group.commands.add instead.
      *
-     * @param {[string]} specs The names by which this command can be
-     *     invoked. The first name specified is the command's canonical
-     *     name.
+     * @param {[string]} specs The names by which this command can be invoked.
+     *     The first name specified is the command's canonical name.
      * @param {string} description A description of the command.
      * @param {function} action The action invoked by this command.
      * @param {Object} extra An optional extra configuration hash.
-     * @optional
+     *     @optional
+     * @param {boolean} replace Replace an existing command of the same name.
+     *     @optional
      */
-    add: function add(specs, description, action, extra, replace) {
+    add: function add(specs, description, action, extra={}, replace=false) {
         const { commands, contexts } = this.modules;
 
-        extra = extra || {};
         if (!extra.definedAt)
             extra.definedAt = contexts.getCaller(Components.stack.caller);
+        if (!extra.sourceModule)
+            extra.sourceModule = commands.currentDependency;
 
         extra.hive = this;
         extra.parsedSpecs = Command.parseSpecs(specs);
@@ -485,33 +569,33 @@ var CommandHive = Class("CommandHive", Contexts.Hive, {
         let names = array.flatten(extra.parsedSpecs);
         let name = names[0];
 
-        util.assert(!names.some(function (name) name in commands.builtin._map),
-                    _("command.cantReplace", name));
+        if (this.name != "builtin") {
+            util.assert(!names.some(name => name in commands.builtin._map),
+                        _("command.cantReplace", name));
 
-        util.assert(replace || names.every(function (name) !(name in this._map), this),
-                    _("command.wontReplace", name));
+            util.assert(replace || names.every(name => !(name in this._map)),
+                        _("command.wontReplace", name));
+        }
 
         for (let name in values(names)) {
             ex.__defineGetter__(name, function () this._run(name));
-            if (name in this._map)
+            if (name in this._map && !this._map[name].isPlaceholder)
                 this.remove(name);
         }
 
-        let self = this;
-        let closure = function () self._map[name];
+        let closure = () => this._map[name];
 
-        memoize(this._map, name, function () commands.Command(specs, description, action, extra));
-        memoize(this._list, this._list.length, closure);
+        memoize(this._map, name, () => commands.Command(specs, description, action, extra));
+        if (!extra.hidden)
+            memoize(this._list, this._list.length, closure);
         for (let alias in values(names.slice(1)))
             memoize(this._map, alias, closure);
 
         return name;
     },
 
-    _add: function _add(names, description, action, extra, replace) {
+    _add: function _add(names, description, action, extra={}, replace=false) {
         const { contexts } = this.modules;
-
-        extra = extra || {};
         extra.definedAt = contexts.getCaller(Components.stack.caller.caller);
         return this.add.apply(this, arguments);
     },
@@ -535,9 +619,23 @@ var CommandHive = Class("CommandHive", Contexts.Hive, {
      *     its names matches *name* exactly.
      * @returns {Command}
      */
-    get: function get(name, full) this._map[name]
-            || !full && array.nth(this._list, function (cmd) cmd.hasName(name), 0)
-            || null,
+    get: function get(name, full) {
+        let cmd = this._map[name]
+               || !full && this._list.find(cmd => cmd.hasName(name))
+               || null;
+
+        if (!cmd && full) {
+            // Hrm. This is wrong. -Kris
+            let name = this._specs.find(spec => Command.hasName(spec, name));
+            return name && this.get(name);
+        }
+
+        if (cmd && cmd.isPlaceholder) {
+            this.modules.moduleManager.initDependencies("commands", [cmd.sourceModule]);
+            cmd = this._map[name];
+        }
+        return cmd;
+    },
 
     /**
      * Remove the user-defined command with matching *name*.
@@ -549,7 +647,7 @@ var CommandHive = Class("CommandHive", Contexts.Hive, {
         util.assert(this.group.modifiable, _("command.cantDelete"));
 
         let cmd = this.get(name);
-        this._list = this._list.filter(function (c) c !== cmd);
+        this._list = this._list.filter(c => c !== cmd);
         for (let name in values(cmd.names))
             delete this._map[name];
     }
@@ -560,6 +658,7 @@ var CommandHive = Class("CommandHive", Contexts.Hive, {
  */
 var Commands = Module("commands", {
     lazyInit: true,
+    lazyDepends: true,
 
     Local: function Local(dactyl, modules, window) let ({ Group, contexts } = modules) ({
         init: function init() {
@@ -571,13 +670,20 @@ var Commands = Module("commands", {
             });
         },
 
+        reallyInit: function reallyInit() {
+            if (false)
+                this.builtin.cache();
+            else
+                this.modules.moduleManager.initDependencies("commands");
+        },
+
         get context() contexts.context,
 
         get readHeredoc() modules.io.readHeredoc,
 
         get allHives() contexts.allGroups.commands,
 
-        get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
+        get userHives() this.allHives.filter(h => h !== this.builtin),
 
         /**
          * Executes an Ex command script.
@@ -653,46 +759,43 @@ var Commands = Module("commands", {
             const { commandline, completion } = this.modules;
             function completerToString(completer) {
                 if (completer)
-                    return [k for ([k, v] in Iterator(config.completers)) if (completer == completion.closure[v])][0] || "custom";
+                    return [k for ([k, v] in Iterator(config.completers)) if (completer == completion.bound[v])][0] || "custom";
                 return "";
             }
             // TODO: allow matching of aliases?
-            function cmds(hive) hive._list.filter(function (cmd) cmd.name.indexOf(filter || "") == 0)
-
-            let hives = (hives || this.userHives).map(function (h) [h, cmds(h)]).filter(function ([h, c]) c.length);
-
-            let list = <table>
-                <tr highlight="Title">
-                    <td/>
-                    <td style="padding-right: 1em;"></td>
-                    <td style="padding-right: 1ex;">{_("title.Name")}</td>
-                    <td style="padding-right: 1ex;">{_("title.Args")}</td>
-                    <td style="padding-right: 1ex;">{_("title.Range")}</td>
-                    <td style="padding-right: 1ex;">{_("title.Complete")}</td>
-                    <td style="padding-right: 1ex;">{_("title.Definition")}</td>
-                </tr>
-                <col style="min-width: 6em; padding-right: 1em;"/>
-                {
-                    template.map(hives, function ([hive, cmds]) let (i = 0)
-                        <tr style="height: .5ex;"/> +
-                        template.map(cmds, function (cmd)
-                            <tr>
-                                <td highlight="Title">{!i++ ? hive.name : ""}</td>
-                                <td>{cmd.bang ? "!" : " "}</td>
-                                <td>{cmd.name}</td>
-                                <td>{cmd.argCount}</td>
-                                <td>{cmd.count ? "0c" : ""}</td>
-                                <td>{completerToString(cmd.completer)}</td>
-                                <td>{cmd.replacementText || "function () { ... }"}</td>
-                            </tr>) +
-                        <tr style="height: .5ex;"/>)
-                }
-            </table>;
-
-            if (list.*.length() === list.text().length() + 2)
-                dactyl.echomsg(_("command.none"));
-            else
-                commandline.commandOutput(list);
+            function cmds(hive) hive._list.filter(cmd => cmd.name.startsWith(filter || ""))
+
+            let hives = (hives || this.userHives).map(h => [h, cmds(h)])
+                                                 .filter(([h, c]) => c.length);
+
+            let list = ["table", {},
+                ["tr", { highlight: "Title" },
+                    ["td"],
+                    ["td", { style: "padding-right: 1em;" }],
+                    ["td", { style: "padding-right: 1ex;" }, _("title.Name")],
+                    ["td", { style: "padding-right: 1ex;" }, _("title.Args")],
+                    ["td", { style: "padding-right: 1ex;" }, _("title.Range")],
+                    ["td", { style: "padding-right: 1ex;" }, _("title.Complete")],
+                    ["td", { style: "padding-right: 1ex;" }, _("title.Definition")]],
+                ["col", { style: "min-width: 6em; padding-right: 1em;" }],
+                hives.map(([hive, cmds]) => let (i = 0) [
+                    ["tr", { style: "height: .5ex;" }],
+                    cmds.map(cmd =>
+                        ["tr", {},
+                            ["td", { highlight: "Title" }, !i++ ? hive.name : ""],
+                            ["td", {}, cmd.bang ? "!" : " "],
+                            ["td", {}, cmd.name],
+                            ["td", {}, cmd.argCount],
+                            ["td", {}, cmd.count ? "0c" : ""],
+                            ["td", {}, completerToString(cmd.completer)],
+                            ["td", {}, cmd.replacementText || "function () { ... }"]]),
+                    ["tr", { style: "height: .5ex;" }]])];
+
+            // E4X-FIXME
+            // if (list.*.length() === list.text().length() + 2)
+            //     dactyl.echomsg(_("command.none"));
+            // else
+            commandline.commandOutput(list);
         }
     }),
 
@@ -712,7 +815,8 @@ var Commands = Module("commands", {
 
     /** @property {Iterator(Command)} @private */
     iterator: function iterator() iter.apply(null, this.hives.array)
-                              .sort(function (a, b) a.serialGroup - b.serialGroup || a.name > b.name)
+                              .sort((a, b) => (a.serialGroup - b.serialGroup ||
+                                               a.name > b.name))
                               .iterValues(),
 
     /** @property {string} The last executed Ex command line. */
@@ -727,9 +831,9 @@ var Commands = Module("commands", {
 
         return group._add.apply(group, arguments);
     },
-    addUserCommand: deprecated("group.commands.add", { get: function addUserCommand() this.user.closure._add }),
+    addUserCommand: deprecated("group.commands.add", { get: function addUserCommand() this.user.bound._add }),
     getUserCommands: deprecated("iter(group.commands)", function getUserCommands() iter(this.user).toArray()),
-    removeUserCommand: deprecated("group.commands.remove", { get: function removeUserCommand() this.user.closure.remove }),
+    removeUserCommand: deprecated("group.commands.remove", { get: function removeUserCommand() this.user.bound.remove }),
 
     /**
      * Returns the specified command invocation object serialized to
@@ -743,12 +847,15 @@ var Commands = Module("commands", {
 
         let defaults = {};
         if (args.ignoreDefaults)
-            defaults = array(this.options).map(function (opt) [opt.names[0], opt.default])
+            defaults = array(this.options).map(opt => [opt.names[0], opt.default])
                                           .toObject();
 
         for (let [opt, val] in Iterator(args.options || {})) {
+            if (val === undefined)
+                continue;
             if (val != null && defaults[opt] === val)
                 continue;
+
             let chr = /^-.$/.test(opt) ? " " : "=";
             if (isArray(val))
                 opt += chr + Option.stringify.stringlist(val);
@@ -756,13 +863,14 @@ var Commands = Module("commands", {
                 opt += chr + Commands.quote(val);
             res.push(opt);
         }
+
         for (let [, arg] in Iterator(args.arguments || []))
             res.push(Commands.quote(arg));
 
         let str = args.literalArg;
         if (str)
             res.push(!/\n/.test(str) ? str :
-                     this.hereDoc && false ? "<<EOF\n" + String.replace(str, /\n$/, "") + "\nEOF"
+                     this.serializeHereDoc ? "<<EOF\n" + String.replace(str, /\n$/, "") + "\nEOF"
                                            : String.replace(str, /\n/g, "\n" + res[0].replace(/./g, " ").replace(/.$/, "\\")));
         return res.join(" ");
     },
@@ -774,8 +882,8 @@ var Commands = Module("commands", {
      *     any of the command's names.
      * @returns {Command}
      */
-    get: function get(name, full) iter(this.hives).map(function ([i, hive]) hive.get(name, full))
-                                                  .nth(util.identity, 0),
+    get: function get(name, full) iter(this.hives).map(([i, hive]) => hive.get(name, full))
+                                                  .find(util.identity),
 
     /**
      * Returns true if a command invocation contains a URL referring to the
@@ -788,7 +896,7 @@ var Commands = Module("commands", {
     hasDomain: function hasDomain(command, host) {
         try {
             for (let [cmd, args] in this.subCommands(command))
-                if (Array.concat(cmd.domains(args)).some(function (domain) util.isSubdomain(domain, host)))
+                if (Array.concat(cmd.domains(args)).some(domain => util.isSubdomain(domain, host)))
                     return true;
         }
         catch (e) {
@@ -807,7 +915,8 @@ var Commands = Module("commands", {
     hasPrivateData: function hasPrivateData(command) {
         for (let [cmd, args] in this.subCommands(command))
             if (cmd.privateData)
-                return !callable(cmd.privateData) || cmd.privateData(args);
+                return !callable(cmd.privateData) ? cmd.privateData
+                                                  : cmd.privateData(args);
         return false;
     },
 
@@ -858,13 +967,10 @@ var Commands = Module("commands", {
      *     Args object.
      * @returns {Args}
      */
-    parseArgs: function parseArgs(str, params) {
+    parseArgs: function parseArgs(str, params={}) {
         const self = this;
 
-        function getNextArg(str, _keepQuotes) {
-            if (arguments.length < 2)
-                _keepQuotes = keepQuotes;
-
+        function getNextArg(str, _keepQuotes=keepQuotes) {
             if (str.substr(0, 2) === "<<" && hereDoc) {
                 let arg = /^<<(\S*)/.exec(str)[1];
                 let count = arg.length + 2;
@@ -883,7 +989,7 @@ var Commands = Module("commands", {
 
         try {
 
-            var { allowUnknownOptions, argCount, complete, extra, hereDoc, literal, options, keepQuotes } = params || {};
+            var { allowUnknownOptions, argCount, complete, extra, hereDoc, literal, options, keepQuotes } = params;
 
             if (!options)
                 options = [];
@@ -908,7 +1014,7 @@ var Commands = Module("commands", {
             let matchOpts = function matchOpts(arg) {
                 // Push possible option matches into completions
                 if (complete && !onlyArgumentsRemaining)
-                    completeOpts = options.filter(function (opt) opt.multiple || !Set.has(args, opt.names[0]));
+                    completeOpts = options.filter(opt => (opt.multiple || !hasOwnProperty(args, opt.names[0])));
             };
             let resetCompletions = function resetCompletions() {
                 completeOpts = null;
@@ -960,7 +1066,7 @@ var Commands = Module("commands", {
                 if (!onlyArgumentsRemaining) {
                     for (let [, opt] in Iterator(options)) {
                         for (let [, optname] in Iterator(opt.names)) {
-                            if (sub.indexOf(optname) == 0) {
+                            if (sub.startsWith(optname)) {
                                 let count = 0;
                                 let invalid = false;
                                 let arg, quote, quoted;
@@ -1007,7 +1113,7 @@ var Commands = Module("commands", {
 
                                             if (arg == null || (typeof arg == "number" && isNaN(arg))) {
                                                 if (!complete || orig != "" || args.completeStart != str.length)
-                                                    fail(_("command.invalidOptTypeArg", opt.type.description, optname, argString));
+                                                    fail(_("command.invalidOptTypeArg", opt.type.description, optname, quoted));
                                                 if (complete)
                                                     complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
                                             }
@@ -1016,7 +1122,7 @@ var Commands = Module("commands", {
                                         // we have a validator function
                                         if (typeof opt.validator == "function") {
                                             if (opt.validator(arg, quoted) == false && (arg || !complete)) {
-                                                fail(_("command.invalidOptArg", optname, argString));
+                                                fail(_("command.invalidOptArg", optname, quoted));
                                                 if (complete) // Always true.
                                                     complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
                                             }
@@ -1062,7 +1168,7 @@ var Commands = Module("commands", {
                     if (sub.substr(0, 2) === "<<" && hereDoc)
                         let ([count, arg] = getNextArg(sub)) {
                             sub = arg + sub.substr(count);
-                        }
+                        };
 
                     args.push(sub);
                     args.quote = null;
@@ -1100,7 +1206,7 @@ var Commands = Module("commands", {
                     context.filter = args.completeFilter;
 
                     if (isArray(arg))
-                        context.filters.push(function (item) arg.indexOf(item.text) === -1);
+                        context.filters.push(item => arg.indexOf(item.text) === -1);
 
                     if (typeof opt.completer == "function")
                         var compl = opt.completer(context, args);
@@ -1133,14 +1239,14 @@ var Commands = Module("commands", {
         }
     },
 
-    nameRegexp: util.regexp(<![CDATA[
+    nameRegexp: util.regexp(literal(/*
             [^
                 0-9
                 <forbid>
             ]
             [^ <forbid> ]*
-        ]]>, "gx", {
-        forbid: util.regexp(String.replace(<![CDATA[
+        */), "gx", {
+        forbid: util.regexp(String.replace(literal(/*
             U0000-U002c // U002d -
             U002e-U002f
             U003a-U0040 // U0041-U005a a-z
@@ -1163,19 +1269,19 @@ var Commands = Module("commands", {
             Ufe70-Ufeff // Arabic Presentation Forms-B
             Uff00-Uffef // Halfwidth and Fullwidth Forms
             Ufff0-Uffff // Specials
-        ]]>, /U/g, "\\u"), "x")
+        */), /U/g, "\\u"), "x")
     }),
 
-    validName: Class.memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
+    validName: Class.Memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
 
-    commandRegexp: Class.memoize(function commandRegexp() util.regexp(<![CDATA[
+    commandRegexp: Class.Memoize(function commandRegexp() util.regexp(literal(/*
             ^
             (?P<spec>
                 (?P<prespace> [:\s]*)
                 (?P<count>    (?:\d+ | %)? )
                 (?P<fullCmd>
-                    (?: (?P<group>   <name>) : )?
-                    (?P<cmd>      (?:<name> | !)? ))
+                    (?: (?P<group> <name>) : )?
+                    (?P<cmd>      (?:-? [()] | <name> | !)? ))
                 (?P<bang>     !?)
                 (?P<space>    \s*)
             )
@@ -1183,7 +1289,7 @@ var Commands = Module("commands", {
                 (?:. | \n)*?
             )?
             $
-        ]]>, "x", {
+        */), "x", {
             name: this.nameRegexp
         })),
 
@@ -1240,10 +1346,8 @@ var Commands = Module("commands", {
                 return;
             }
 
-            if (complete) {
-                complete.fork(command.name);
-                var context = complete.fork("args", len);
-            }
+            if (complete)
+                var context = complete.fork(command.name).fork("opts", len);;
 
             if (!complete || /(\w|^)[!\s]/.test(str))
                 args = command.parseArgs(args, context, { count: count, bang: bang });
@@ -1288,7 +1392,8 @@ var Commands = Module("commands", {
         let quote = null;
         let len = str.length;
 
-        function fixEscapes(str) str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}|(.))/g, function (m, n1) n1 || m);
+        function fixEscapes(str) str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}|(.))/g,
+                                             (m, n1) => n1 || m);
 
         // Fix me.
         if (isString(sep))
@@ -1330,9 +1435,9 @@ var Commands = Module("commands", {
             context.title = ["Command"];
             context.keys = { text: "longNames", description: "description" };
             if (group)
-                context.generate = function () group._list;
+                context.generate = () => group._list;
             else
-                context.generate = function () modules.commands.hives.map(function (h) h._list).flatten();
+                context.generate = () => modules.commands.hives.map(h => h._list).flatten();
         };
 
         // provides completions for ex commands, including their arguments
@@ -1368,7 +1473,7 @@ var Commands = Module("commands", {
                 return;
             }
 
-            let cmdContext = context.fork(command.name, match.fullCmd.length + match.bang.length + match.space.length);
+            let cmdContext = context.fork(command.name + "/args", match.fullCmd.length + match.bang.length + match.space.length);
             try {
                 if (!cmdContext.waitingForTab) {
                     if (!args.completeOpt && command.completer && args.completeStart != null) {
@@ -1385,6 +1490,40 @@ var Commands = Module("commands", {
             }
         };
 
+        completion.exMacro = function exMacro(context, args, cmd) {
+            if (!cmd.action.macro)
+                return;
+            let { macro } = cmd.action;
+
+            let start = "«%-d-]'", end = "'[-d-%»";
+
+            let n = /^\d+$/.test(cmd.argCount) ? parseInt(cmd.argCount) : 12;
+            for (let i = args.completeArg; i < n; i++)
+                args[i] = start + i + end;
+
+            let params = {
+                args: { __proto__: args, toString: function () this.join(" ") },
+                bang:  args.bang ? "!" : "",
+                count: args.count
+            };
+
+            if (!macro.valid(params))
+                return;
+
+            let str = macro(params);
+            let idx = str.indexOf(start);
+            if (!~idx || !/^(')?(\d+)'/.test(str.substr(idx + start.length))
+                    || RegExp.$2 != args.completeArg)
+                return;
+
+            let quote = RegExp.$2;
+            context.quote = null;
+            context.offset -= idx;
+            context.filter = str.substr(0, idx) + (quote ? Option.quote : util.identity)(context.filter);
+
+            context.fork("ex", 0, completion, "ex");
+        };
+
         completion.userCommand = function userCommand(context, group) {
             context.title = ["User Command", "Definition"];
             context.keys = { text: "name", description: "replacementText" };
@@ -1395,12 +1534,19 @@ var Commands = Module("commands", {
     commands: function initCommands(dactyl, modules, window) {
         const { commands, contexts } = modules;
 
+        commands.add(["(", "-("], "",
+            function (args) { dactyl.echoerr(_("dactyl.cheerUp")); },
+            { hidden: true });
+        commands.add([")", "-)"], "",
+            function (args) { dactyl.echoerr(_("dactyl.somberDown")); },
+            { hidden: true });
+
         commands.add(["com[mand]"],
             "List or define commands",
             function (args) {
                 let cmd = args[0];
 
-                util.assert(!cmd || cmd.split(",").every(commands.validName.closure.test),
+                util.assert(!cmd || cmd.split(",").every(commands.validName.bound.test),
                             _("command.invalidName", cmd));
 
                 if (args.length <= 1)
@@ -1410,7 +1556,7 @@ var Commands = Module("commands", {
                                 _("group.cantChangeBuiltin", _("command.commands")));
 
                     let completer = args["-complete"];
-                    let completerFunc = null; // default to no completion for user commands
+                    let completerFunc = function (context, args) modules.completion.exMacro(context, args, this);
 
                     if (completer) {
                         if (/^custom,/.test(completer)) {
@@ -1424,13 +1570,13 @@ var Commands = Module("commands", {
                                     return dactyl.userEval(completer);
                                 });
                                 if (callable(result))
-                                    return result.apply(this, Array.slice(arguments));
+                                    return result.apply(this, arguments);
                                 else
                                     return context.completions = result;
                             };
                         }
                         else
-                            completerFunc = function (context) modules.completion.closure[config.completers[completer]](context);
+                            completerFunc = context => modules.completion.bound[config.completers[completer]](context);
                     }
 
                     let added = args["-group"].add(cmd.split(","),
@@ -1439,7 +1585,7 @@ var Commands = Module("commands", {
                                         function makeParams(args, modifiers) ({
                                             args: {
                                                 __proto__: args,
-                                                toString: function () this.string,
+                                                toString: function () this.string
                                             },
                                             bang:  this.bang && args.bang ? "!" : "",
                                             count: this.count && args.count
@@ -1505,7 +1651,7 @@ var Commands = Module("commands", {
                                     ["+", "One or more arguments are allowed"]],
                         default: "0",
                         type: CommandOption.STRING,
-                        validator: function (arg) /^[01*?+]$/.test(arg)
+                        validator: bind("test", /^[01*?+]$/)
                     },
                     {
                         names: ["-nopersist", "-n"],
@@ -1515,8 +1661,8 @@ var Commands = Module("commands", {
                 literal: 1,
 
                 serialize: function () array(commands.userHives)
-                    .filter(function (h) h.persist)
-                    .map(function (hive) [
+                    .filter(h => h.persist)
+                    .map(hive => [
                         {
                             command: this.name,
                             bang: true,
@@ -1534,7 +1680,7 @@ var Commands = Module("commands", {
                             ignoreDefaults: true
                         }
                         for (cmd in hive) if (cmd.persist)
-                    ], this)
+                    ])
                     .flatten().array
             });
 
@@ -1573,11 +1719,12 @@ var Commands = Module("commands", {
             iterate: function (args) commands.iterator().map(function (cmd) ({
                 __proto__: cmd,
                 columns: [
-                    cmd.hive == commands.builtin ? "" : <span highlight="Object" style="padding-right: 1em;">{cmd.hive.name}</span>
+                    cmd.hive == commands.builtin ? "" : ["span", { highlight: "Object", style: "padding-right: 1em;" },
+                                                            cmd.hive.name]
                 ]
             })),
-            iterateIndex: function (args) let (tags = services["dactyl:"].HELP_TAGS)
-                this.iterate(args).filter(function (cmd) cmd.hive === commands.builtin || Set.has(tags, cmd.helpTag)),
+            iterateIndex: function (args) let (tags = help.tags)
+                this.iterate(args).filter(cmd => (cmd.hive === commands.builtin || hasOwnProperty(tags, cmd.helpTag))),
             format: {
                 headings: ["Command", "Group", "Description"],
                 description: function (cmd) template.linkifyHelp(cmd.description + (cmd.replacementText ? ": " + cmd.action : "")),
@@ -1606,9 +1753,9 @@ var Commands = Module("commands", {
     javascript: function initJavascript(dactyl, modules, window) {
         const { JavaScript, commands } = modules;
 
-        JavaScript.setCompleter([commands.user.get, commands.user.remove],
+        JavaScript.setCompleter([CommandHive.prototype.get, CommandHive.prototype.remove],
                                 [function () [[c.names, c.description] for (c in this)]]);
-        JavaScript.setCompleter([commands.get],
+        JavaScript.setCompleter([Commands.prototype.get],
                                 [function () [[c.names, c.description] for (c in this.iterator())]]);
     },
     mappings: function initMappings(dactyl, modules, window) {
@@ -1616,9 +1763,9 @@ var Commands = Module("commands", {
 
         mappings.add([modes.COMMAND],
             ["@:"], "Repeat the last Ex command",
-            function (args) {
+            function ({ count }) {
                 if (commands.repeat) {
-                    for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100))
+                    for (let i in util.interruptibleRange(0, Math.max(count, 1), 100))
                         dactyl.execute(commands.repeat);
                 }
                 else
@@ -1628,42 +1775,40 @@ var Commands = Module("commands", {
     }
 });
 
-(function () {
-
-    Commands.quoteMap = {
-        "\n": "\\n",
-        "\t": "\\t",
-    };
-    function quote(q, list, map) {
-        map = map || Commands.quoteMap;
-        let re = RegExp("[" + list + "]", "g");
-        function quote(str) q + String.replace(str, re, function ($0) $0 in map ? map[$0] : ("\\" + $0)) + q;
-        quote.list = list;
-        return quote;
-    };
-
-    Commands.quoteArg = {
-        '"': quote('"', '\n\t"\\\\'),
-        "'": quote("'", "'", { "'": "''" }),
-        "":  quote("",  "|\\\\\\s'\"")
-    };
-    Commands.complQuote = {
-        '"': ['"', quote("", Commands.quoteArg['"'].list), '"'],
-        "'": ["'", quote("", Commands.quoteArg["'"].list), "'"],
-        "":  ["", Commands.quoteArg[""], ""]
-    };
-
-    Commands.parseBool = function (arg) {
-        if (/^(true|1|on)$/i.test(arg))
-            return true;
-        if (/^(false|0|off)$/i.test(arg))
-            return false;
-        return NaN;
-    };
-})();
+let quote = function quote(q, list, map=Commands.quoteMap) {
+    let re = RegExp("[" + list + "]", "g");
+    function quote(str) (q + String.replace(str, re, $0 => ($0 in map ? map[$0] : ("\\" + $0)))
+                           + q);
+    quote.list = list;
+    return quote;
+};
+
+Commands.quoteMap = {
+    "\n": "\\n",
+    "\t": "\\t"
+};
+
+Commands.quoteArg = {
+    '"': quote('"', '\n\t"\\\\'),
+    "'": quote("'", "'", { "'": "''" }),
+    "":  quote("",  "|\\\\\\s'\"")
+};
+Commands.complQuote = {
+    '"': ['"', quote("", Commands.quoteArg['"'].list), '"'],
+    "'": ["'", quote("", Commands.quoteArg["'"].list), "'"],
+    "":  ["", Commands.quoteArg[""], ""]
+};
+
+Commands.parseBool = function (arg) {
+    if (/^(true|1|on)$/i.test(arg))
+        return true;
+    if (/^(false|0|off)$/i.test(arg))
+        return false;
+    return NaN;
+};
 
 endModule();
 
 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
 
-// vim: set fdm=marker sw=4 ts=4 et ft=javascript:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: