]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/commands.jsm
199cfff13eca5e0b999b0a74c6d1a77504d36661
[dactyl.git] / common / modules / commands.jsm
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2012 Kris Maglione <maglione.k at Gmail>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 try {
10
11 defineModule("commands", {
12     exports: ["ArgType", "Command", "Commands", "CommandOption", "Ex", "commands"],
13     require: ["contexts", "messages", "util"]
14 });
15
16 lazyRequire("help", ["help"]);
17 lazyRequire("options", ["Option"]);
18 lazyRequire("template", ["template"]);
19
20 /**
21  * A structure representing the options available for a command.
22  *
23  * Do NOT create instances of this class yourself, use the helper method
24  * {@see Commands#add} instead
25  *
26  * @property {[string]} names An array of option names. The first name
27  *     is the canonical option name.
28  * @property {number} type The option's value type. This is one of:
29  *         (@link CommandOption.NOARG),
30  *         (@link CommandOption.STRING),
31  *         (@link CommandOption.STRINGMAP),
32  *         (@link CommandOption.BOOL),
33  *         (@link CommandOption.INT),
34  *         (@link CommandOption.FLOAT),
35  *         (@link CommandOption.LIST),
36  *         (@link CommandOption.ANY)
37  * @property {function} validator A validator function
38  * @property {function (CompletionContext, object)} completer A list of
39  *    completions, or a completion function which will be passed a
40  *    {@link CompletionContext} and an object like that returned by
41  *    {@link commands.parseArgs} with the following additional keys:
42  *      completeOpt - The name of the option currently being completed.
43  * @property {boolean} multiple Whether this option can be specified multiple times
44  * @property {string} description A description of the option
45  * @property {object} default The option's default value
46  */
47
48 var CommandOption = Struct("names", "type", "validator", "completer", "multiple", "description", "default");
49 CommandOption.defaultValue("description", function () "");
50 CommandOption.defaultValue("type", function () CommandOption.NOARG);
51 CommandOption.defaultValue("multiple", function () false);
52
53 var ArgType = Struct("description", "parse");
54 update(CommandOption, {
55     /**
56      * @property {object} The option argument is unspecified. Any argument
57      *     is accepted and caller is responsible for parsing the return
58      *     value.
59      * @final
60      */
61     ANY: 0,
62
63     /**
64      * @property {object} The option doesn't accept an argument.
65      * @final
66      */
67     NOARG: ArgType("no arg",  function (arg) !arg || null),
68     /**
69      * @property {object} The option accepts a boolean argument.
70      * @final
71      */
72     BOOL: ArgType("boolean", function parseBoolArg(val) Commands.parseBool(val)),
73     /**
74      * @property {object} The option accepts a string argument.
75      * @final
76      */
77     STRING: ArgType("string", function (val) val),
78     /**
79      * @property {object} The option accepts a stringmap argument.
80      * @final
81      */
82     STRINGMAP: ArgType("stringmap", function (val, quoted) Option.parse.stringmap(quoted)),
83     /**
84      * @property {object} The option accepts an integer argument.
85      * @final
86      */
87     INT: ArgType("int", function parseIntArg(val) parseInt(val)),
88     /**
89      * @property {object} The option accepts a float argument.
90      * @final
91      */
92     FLOAT: ArgType("float", function parseFloatArg(val) parseFloat(val)),
93     /**
94      * @property {object} The option accepts a string list argument.
95      *     E.g. "foo,bar"
96      * @final
97      */
98     LIST: ArgType("list", function parseListArg(arg, quoted) Option.splitList(quoted))
99 });
100
101 /**
102  * A class representing Ex commands. Instances are created by
103  * the {@link Commands} class.
104  *
105  * @param {[string]} specs The names by which this command can be invoked.
106  *     These are specified in the form "com[mand]" where "com" is a unique
107  *     command name prefix.
108  * @param {string} description A short one line description of the command.
109  * @param {function} action The action invoked by this command when executed.
110  * @param {Object} extraInfo An optional extra configuration hash. The
111  *     following properties are supported.
112  *         always      - see {@link Command#always}
113  *         argCount    - see {@link Command#argCount}
114  *         bang        - see {@link Command#bang}
115  *         completer   - see {@link Command#completer}
116  *         count       - see {@link Command#count}
117  *         domains     - see {@link Command#domains}
118  *         heredoc     - see {@link Command#heredoc}
119  *         literal     - see {@link Command#literal}
120  *         options     - see {@link Command#options}
121  *         privateData - see {@link Command#privateData}
122  *         serialize   - see {@link Command#serialize}
123  *         subCommand  - see {@link Command#subCommand}
124  * @optional
125  * @private
126  */
127 var Command = Class("Command", {
128     init: function init(specs, description, action, extraInfo) {
129         specs = Array.concat(specs); // XXX
130
131         this.specs = specs;
132         this.description = description;
133         this.action = action;
134
135         if (extraInfo.options)
136             this._options = extraInfo.options;
137         delete extraInfo.options;
138
139         if (extraInfo)
140             this.update(extraInfo);
141     },
142
143     get toStringParams() [this.name, this.hive.name],
144
145     get identifier() this.hive.prefix + this.name,
146
147     get helpTag() ":" + this.name,
148
149     get lastCommand() this._lastCommand || this.modules.commandline.command,
150     set lastCommand(val) { this._lastCommand = val; },
151
152     /**
153      * Execute this command.
154      *
155      * @param {Args} args The Args object passed to {@link #action}.
156      * @param {Object} modifiers Any modifiers to be passed to {@link #action}.
157      */
158     execute: function execute(args, modifiers) {
159         const { dactyl } = this.modules;
160
161         let context = args.context;
162         if (this.deprecated)
163             this.warn(context, "deprecated", _("warn.deprecated", ":" + this.name, this.deprecated));
164
165         modifiers = modifiers || {};
166
167         if (args.count != null && !this.count)
168             throw FailedAssertion(_("command.noCount"));
169         if (args.bang && !this.bang)
170             throw FailedAssertion(_("command.noBang"));
171
172         args.doc = this.hive.group.lastDocument;
173
174         return !dactyl.trapErrors(function exec() {
175             let extra = this.hive.argsExtra(args);
176
177             for (let k in properties(extra))
178                 if (!(k in args))
179                     Object.defineProperty(args, k, Object.getOwnPropertyDescriptor(extra, k));
180
181             if (this.always)
182                 this.always(args, modifiers);
183
184             if (!context || !context.noExecute)
185                 this.action(args, modifiers);
186         }, this);
187     },
188
189     /**
190      * Returns whether this command may be invoked via *name*.
191      *
192      * @param {string} name The candidate name.
193      * @returns {boolean}
194      */
195     hasName: function hasName(name) Command.hasName(this.parsedSpecs, name),
196
197     /**
198      * A helper function to parse an argument string.
199      *
200      * @param {string} args The argument string to parse.
201      * @param {CompletionContext} complete A completion context.
202      *     Non-null when the arguments are being parsed for completion
203      *     purposes.
204      * @param {Object} extra Extra keys to be spliced into the
205      *     returned Args object.
206      * @returns {Args}
207      * @see Commands#parseArgs
208      */
209     parseArgs: function parseArgs(args, complete, extra) this.modules.commands.parseArgs(args, {
210         __proto__: this,
211         complete: complete,
212         extra: extra
213     }),
214
215     complained: Class.Memoize(function () ({})),
216
217     /**
218      * @property {[string]} All of this command's name specs. e.g., "com[mand]"
219      */
220     specs: null,
221     parsedSpecs: Class.Memoize(function () Command.parseSpecs(this.specs)),
222
223     /** @property {[string]} All of this command's short names, e.g., "com" */
224     shortNames: Class.Memoize(function () array.compact(this.parsedSpecs.map(function (n) n[1]))),
225
226     /**
227      * @property {[string]} All of this command's long names, e.g., "command"
228      */
229     longNames: Class.Memoize(function () this.parsedSpecs.map(function (n) n[0])),
230
231     /** @property {string} The command's canonical name. */
232     name: Class.Memoize(function () this.longNames[0]),
233
234     /** @property {[string]} All of this command's long and short names. */
235     names: Class.Memoize(function () this.names = array.flatten(this.parsedSpecs)),
236
237     /** @property {string} This command's description, as shown in :listcommands */
238     description: Messages.Localized(""),
239
240     /** @property {string|null} If set, the deprecation message for this command. */
241     deprecated: Messages.Localized(null),
242
243     /**
244      * @property {function (Args)} The function called to execute this command.
245      */
246     action: null,
247
248     /**
249      * @property {function (Args)} A function which is called when this
250      * command is encountered, even if we are ignoring commands. Used to
251      * implement control structures.
252      */
253     always: null,
254
255     /**
256      * @property {string} This command's argument count spec.
257      * @see Commands#parseArguments
258      */
259     argCount: 0,
260
261     /**
262      * @property {function (CompletionContext, Args)} This command's completer.
263      * @see CompletionContext
264      */
265     completer: null,
266
267     /** @property {boolean} Whether this command accepts a here document. */
268     hereDoc: false,
269
270     /**
271      * @property {boolean} Whether this command may be called with a bang,
272      *     e.g., :com!
273      */
274     bang: false,
275
276     /**
277      * @property {boolean} Whether this command may be called with a count,
278      *     e.g., :12bdel
279      */
280     count: false,
281
282     /**
283      * @property {function(args)} A function which should return a list
284      *     of domains referenced in the given args. Used in determining
285      *     whether to purge the command from history when clearing
286      *     private data.
287      */
288     domains: function (args) [],
289
290     /**
291      * @property {boolean} At what index this command's literal arguments
292      *     begin. For instance, with a value of 2, all arguments starting with
293      *     the third are parsed as a single string, with all quoting characters
294      *     passed literally. This is especially useful for commands which take
295      *     key mappings or Ex command lines as arguments.
296      */
297     literal: null,
298
299     /**
300      * @property {Array} The options this command takes.
301      * @see Commands@parseArguments
302      */
303     options: Class.Memoize(function ()
304         this._options.map(function (opt) {
305             let option = CommandOption.fromArray(opt);
306             option.localeName = ["command", this.name, option.names[0]];
307             return option;
308         }, this)),
309     _options: [],
310
311     optionMap: Class.Memoize(function () array(this.options)
312                 .map(function (opt) opt.names.map(function (name) [name, opt]))
313                 .flatten().toObject()),
314
315     newArgs: function newArgs(base) {
316         let res = [];
317         update(res, base);
318         res.__proto__ = this.argsPrototype;
319         return res;
320     },
321
322     argsPrototype: Class.Memoize(function argsPrototype() {
323         let res = update([], {
324                 __iterator__: function AP__iterator__() array.iterItems(this),
325
326                 command: this,
327
328                 explicitOpts: Class.Memoize(function () ({})),
329
330                 has: function AP_has(opt) Set.has(this.explicitOpts, opt) || typeof opt === "number" && Set.has(this, opt),
331
332                 get literalArg() this.command.literal != null && this[this.command.literal] || "",
333
334                 // TODO: string: Class.Memoize(function () { ... }),
335
336                 verify: function verify() {
337                     if (this.command.argCount) {
338                         util.assert((this.length > 0 || !/^[1+]$/.test(this.command.argCount)) &&
339                                     (this.literal == null || !/[1+]/.test(this.command.argCount) || /\S/.test(this.literalArg || "")),
340                                      _("error.argumentRequired"));
341
342                         util.assert((this.length == 0 || this.command.argCount !== "0") &&
343                                     (this.length <= 1 || !/^[01?]$/.test(this.command.argCount)),
344                                     _("error.trailingCharacters"));
345                     }
346                 }
347         });
348
349         this.options.forEach(function (opt) {
350             if (opt.default !== undefined) {
351                 let prop = Object.getOwnPropertyDescriptor(opt, "default") ||
352                     { configurable: true, enumerable: true, get: function () opt.default };
353
354                 if (prop.get && !prop.set)
355                     prop.set = function (val) { Class.replaceProperty(this, opt.names[0], val) };
356                 Object.defineProperty(res, opt.names[0], prop);
357             }
358         });
359
360         return res;
361     }),
362
363     /**
364      * @property {boolean|function(args)} When true, invocations of this
365      *     command may contain private data which should be purged from
366      *     saved histories when clearing private data. If a function, it
367      *     should return true if an invocation with the given args
368      *     contains private data
369      */
370     privateData: true,
371     /**
372      * @property {function} Should return an array of *Object*s suitable to be
373      *     passed to {@link Commands#commandToString}, one for each past
374      *     invocation which should be restored on subsequent @dactyl startups.
375      */
376     serialize: null,
377     serialGroup: 50,
378     /**
379      * @property {number} If this command takes another ex command as an
380      *     argument, the index of that argument. Used in determining whether to
381      *     purge the command from history when clearing private data.
382      */
383     subCommand: null,
384     /**
385      * @property {boolean} Specifies whether this is a user command. User
386      *     commands may be created by plugins, or directly by users, and,
387      *     unlike basic commands, may be overwritten. Users and plugin authors
388      *     should create only user commands.
389      */
390     user: false,
391     /**
392      * @property {string} For commands defined via :command, contains the Ex
393      *     command line to be executed upon invocation.
394      */
395     replacementText: null,
396
397     /**
398      * Warns of a misuse of this command once per warning type per file.
399      *
400      * @param {object} context The calling context.
401      * @param {string} type The type of warning.
402      * @param {string} warning The warning message.
403      */
404     warn: function warn(context, type, message) {
405         let loc = !context ? "" : [context.file, context.line, " "].join(":");
406
407         if (!Set.add(this.complained, type + ":" + (context ? context.file : "[Command Line]")))
408             this.modules.dactyl.warn(loc + message);
409     }
410 }, {
411     hasName: function hasName(specs, name)
412         specs.some(function ([long, short])
413             name.indexOf(short) == 0 && long.indexOf(name) == 0),
414
415     // TODO: do we really need more than longNames as a convenience anyway?
416     /**
417      *  Converts command name abbreviation specs of the form
418      *  'shortname[optional-tail]' to short and long versions:
419      *      ["abc[def]", "ghijkl"] ->  [["abcdef", "abc"], ["ghijlk"]]
420      *
421      *  @param {Array} specs An array of command name specs to parse.
422      *  @returns {Array}
423      */
424     parseSpecs: function parseSpecs(specs) {
425         return specs.map(function (spec) {
426             let [, head, tail] = /([^[]+)(?:\[(.*)])?/.exec(spec);
427             return tail ? [head + tail, head] : [head];
428         });
429     }
430 });
431
432 // Prototype.
433 var Ex = Module("Ex", {
434     Local: function Local(dactyl, modules, window) ({
435         get commands() modules.commands,
436         get context() modules.contexts.context
437     }),
438
439     _args: function E_args(cmd, args) {
440         args = Array.slice(args);
441
442         let res = cmd.newArgs({ context: this.context });
443         if (isObject(args[0]))
444             for (let [k, v] in Iterator(args.shift()))
445                 if (k == "!")
446                     res.bang = v;
447                 else if (k == "#")
448                     res.count = v;
449                 else {
450                     let opt = cmd.optionMap["-" + k];
451                     let val = opt.type && opt.type.parse(v);
452
453                     util.assert(val != null && (typeof val !== "number" || !isNaN(val)),
454                                 _("option.noSuch", k));
455
456                     Class.replaceProperty(res, opt.names[0], val);
457                     res.explicitOpts[opt.names[0]] = val;
458                 }
459         for (let [i, val] in array.iterItems(args))
460             res[i] = String(val);
461         return res;
462     },
463
464     _complete: function E_complete(cmd) let (self = this)
465         function _complete(context, func, obj, args) {
466             args = self._args(cmd, args);
467             args.completeArg = args.length - 1;
468             if (cmd.completer && args.length)
469                 return cmd.completer(context, args);
470         },
471
472     _run: function E_run(name) {
473         const self = this;
474         let cmd = this.commands.get(name);
475         util.assert(cmd, _("command.noSuch"));
476
477         return update(function exCommand(options) {
478             let args = self._args(cmd, arguments);
479             args.verify();
480             return cmd.execute(args);
481         }, {
482             dactylCompleter: self._complete(cmd)
483         });
484     },
485
486     __noSuchMethod__: function __noSuchMethod__(meth, args) this._run(meth).apply(this, args)
487 });
488
489 var CommandHive = Class("CommandHive", Contexts.Hive, {
490     init: function init(group) {
491         init.supercall(this, group);
492
493         this._map = {};
494         this._list = [];
495         this._specs = [];
496     },
497
498     /**
499      * Caches this command hive.
500      */
501
502     cache: function cache() {
503         let self = this;
504         let { cache } = this.modules;
505         this.cached = true;
506
507         let cached = cache.get(this.cacheKey, function () {
508             self.cached = false;
509             this.modules.moduleManager.initDependencies("commands");
510
511             let map = {};
512             for (let [name, cmd] in Iterator(self._map))
513                 if (cmd.sourceModule)
514                     map[name] = { sourceModule: cmd.sourceModule, isPlaceholder: true };
515
516             let specs = [];
517             for (let cmd in values(self._list))
518                 for each (let spec in cmd.parsedSpecs)
519                     specs.push(spec.concat(cmd.name));
520
521             return { map: map, specs: specs };
522         });
523
524         let cached = cache.get(this.cacheKey);
525         if (this.cached) {
526             this._specs = cached.specs;
527             for (let [k, v] in Iterator(cached.map))
528                 this._map[k] = v;
529         }
530     },
531
532     get cacheKey() "commands/hives/" + this.name + ".json",
533
534     /** @property {Iterator(Command)} @private */
535     __iterator__: function __iterator__() {
536         if (this.cached)
537             this.modules.initDependencies("commands");
538         this.cached = false;
539         return array.iterValues(this._list.sort(function (a, b) a.name > b.name))
540     },
541
542     /** @property {string} The last executed Ex command line. */
543     repeat: null,
544
545     /**
546      * Adds a new command to the builtin hive. Accessible only to core
547      * dactyl code. Plugins should use group.commands.add instead.
548      *
549      * @param {[string]} specs The names by which this command can be
550      *     invoked. The first name specified is the command's canonical
551      *     name.
552      * @param {string} description A description of the command.
553      * @param {function} action The action invoked by this command.
554      * @param {Object} extra An optional extra configuration hash.
555      * @optional
556      */
557     add: function add(specs, description, action, extra, replace) {
558         const { commands, contexts } = this.modules;
559
560         extra = extra || {};
561         if (!extra.definedAt)
562             extra.definedAt = contexts.getCaller(Components.stack.caller);
563         if (!extra.sourceModule)
564             extra.sourceModule = commands.currentDependency;
565
566         extra.hive = this;
567         extra.parsedSpecs = Command.parseSpecs(specs);
568
569         let names = array.flatten(extra.parsedSpecs);
570         let name = names[0];
571
572         if (this.name != "builtin") {
573             util.assert(!names.some(function (name) name in commands.builtin._map),
574                         _("command.cantReplace", name));
575
576             util.assert(replace || names.every(function (name) !(name in this._map), this),
577                         _("command.wontReplace", name));
578         }
579
580         for (let name in values(names)) {
581             ex.__defineGetter__(name, function () this._run(name));
582             if (name in this._map && !this._map[name].isPlaceholder)
583                 this.remove(name);
584         }
585
586         let self = this;
587         let closure = function () self._map[name];
588
589         memoize(this._map, name, function () commands.Command(specs, description, action, extra));
590         if (!extra.hidden)
591             memoize(this._list, this._list.length, closure);
592         for (let alias in values(names.slice(1)))
593             memoize(this._map, alias, closure);
594
595         return name;
596     },
597
598     _add: function _add(names, description, action, extra, replace) {
599         const { contexts } = this.modules;
600
601         extra = extra || {};
602         extra.definedAt = contexts.getCaller(Components.stack.caller.caller);
603         return this.add.apply(this, arguments);
604     },
605
606     /**
607      * Clear all commands.
608      * @returns {Command}
609      */
610     clear: function clear() {
611         util.assert(this.group.modifiable, _("command.cantDelete"));
612         this._map = {};
613         this._list = [];
614     },
615
616     /**
617      * Returns the command with matching *name*.
618      *
619      * @param {string} name The name of the command to return. This can be
620      *     any of the command's names.
621      * @param {boolean} full If true, only return a command if one of
622      *     its names matches *name* exactly.
623      * @returns {Command}
624      */
625     get: function get(name, full) {
626         let cmd = this._map[name]
627                || !full && array.nth(this._list, function (cmd) cmd.hasName(name), 0)
628                || null;
629
630         if (!cmd && full) {
631             let name = array.nth(this.specs, function (spec) Command.hasName(spec, name), 0);
632             return name && this.get(name);
633         }
634
635         if (cmd && cmd.isPlaceholder) {
636             this.modules.moduleManager.initDependencies("commands", [cmd.sourceModule]);
637             cmd = this._map[name];
638         }
639         return cmd;
640     },
641
642     /**
643      * Remove the user-defined command with matching *name*.
644      *
645      * @param {string} name The name of the command to remove. This can be
646      *     any of the command's names.
647      */
648     remove: function remove(name) {
649         util.assert(this.group.modifiable, _("command.cantDelete"));
650
651         let cmd = this.get(name);
652         this._list = this._list.filter(function (c) c !== cmd);
653         for (let name in values(cmd.names))
654             delete this._map[name];
655     }
656 });
657
658 /**
659  * @instance commands
660  */
661 var Commands = Module("commands", {
662     lazyInit: true,
663     lazyDepends: true,
664
665     Local: function Local(dactyl, modules, window) let ({ Group, contexts } = modules) ({
666         init: function init() {
667             this.Command = Class("Command", Command, { modules: modules });
668             update(this, {
669                 hives: contexts.Hives("commands", Class("CommandHive", CommandHive, { modules: modules })),
670                 user: contexts.hives.commands.user,
671                 builtin: contexts.hives.commands.builtin
672             });
673         },
674
675         reallyInit: function reallyInit() {
676             if (false)
677                 this.builtin.cache();
678             else
679                 this.modules.moduleManager.initDependencies("commands");
680         },
681
682         get context() contexts.context,
683
684         get readHeredoc() modules.io.readHeredoc,
685
686         get allHives() contexts.allGroups.commands,
687
688         get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
689
690         /**
691          * Executes an Ex command script.
692          *
693          * @param {string} string A string containing the commands to execute.
694          * @param {object} tokens An optional object containing tokens to be
695          *      interpolated into the command string.
696          * @param {object} args Optional arguments object to be passed to
697          *      command actions.
698          * @param {object} context An object containing information about
699          *      the file that is being or has been sourced to obtain the
700          *      command string.
701          */
702         execute: function execute(string, tokens, silent, args, context) {
703             contexts.withContext(context || this.context || { file: "[Command Line]", line: 1 },
704                                  function (context) {
705                 modules.io.withSavedValues(["readHeredoc"], function () {
706                     this.readHeredoc = function readHeredoc(end) {
707                         let res = [];
708                         contexts.context.line++;
709                         while (++i < lines.length) {
710                             if (lines[i] === end)
711                                 return res.join("\n");
712                             res.push(lines[i]);
713                         }
714                         util.assert(false, _("command.eof", end));
715                     };
716
717                     args = update({}, args || {});
718
719                     if (tokens && !callable(string))
720                         string = util.compileMacro(string, true);
721                     if (callable(string))
722                         string = string(tokens || {});
723
724                     let lines = string.split(/\r\n|[\r\n]/);
725                     let startLine = context.line;
726
727                     for (var i = 0; i < lines.length && !context.finished; i++) {
728                         // Deal with editors from Silly OSs.
729                         let line = lines[i].replace(/\r$/, "");
730
731                         context.line = startLine + i;
732
733                         // Process escaped new lines
734                         while (i < lines.length && /^\s*\\/.test(lines[i + 1]))
735                             line += "\n" + lines[++i].replace(/^\s*\\/, "");
736
737                         try {
738                             dactyl.execute(line, args);
739                         }
740                         catch (e) {
741                             if (!silent) {
742                                 e.message = context.file + ":" + context.line + ": " + e.message;
743                                 dactyl.reportError(e, true);
744                             }
745                         }
746                     }
747                 });
748             });
749         },
750
751         /**
752          * Lists all user-defined commands matching *filter* and optionally
753          * *hives*.
754          *
755          * @param {string} filter Limits the list to those commands with a name
756          *     matching this anchored substring.
757          * @param {[Hive]} hives List of hives.
758          * @optional
759          */
760         list: function list(filter, hives) {
761             const { commandline, completion } = this.modules;
762             function completerToString(completer) {
763                 if (completer)
764                     return [k for ([k, v] in Iterator(config.completers)) if (completer == completion.closure[v])][0] || "custom";
765                 return "";
766             }
767             // TODO: allow matching of aliases?
768             function cmds(hive) hive._list.filter(function (cmd) cmd.name.indexOf(filter || "") == 0)
769
770             let hives = (hives || this.userHives).map(function (h) [h, cmds(h)]).filter(function ([h, c]) c.length);
771
772             let list = ["table", {},
773                 ["tr", { highlight: "Title" },
774                     ["td"],
775                     ["td", { style: "padding-right: 1em;" }],
776                     ["td", { style: "padding-right: 1ex;" }, _("title.Name")],
777                     ["td", { style: "padding-right: 1ex;" }, _("title.Args")],
778                     ["td", { style: "padding-right: 1ex;" }, _("title.Range")],
779                     ["td", { style: "padding-right: 1ex;" }, _("title.Complete")],
780                     ["td", { style: "padding-right: 1ex;" }, _("title.Definition")]],
781                 ["col", { style: "min-width: 6em; padding-right: 1em;" }],
782                 hives.map(function ([hive, cmds]) let (i = 0) [
783                     ["tr", { style: "height: .5ex;" }],
784                     cmds.map(function (cmd)
785                         ["tr", {},
786                             ["td", { highlight: "Title" }, !i++ ? hive.name : ""],
787                             ["td", {}, cmd.bang ? "!" : " "],
788                             ["td", {}, cmd.name],
789                             ["td", {}, cmd.argCount],
790                             ["td", {}, cmd.count ? "0c" : ""],
791                             ["td", {}, completerToString(cmd.completer)],
792                             ["td", {}, cmd.replacementText || "function () { ... }"]]),
793                     ["tr", { style: "height: .5ex;" }]])];
794
795             // E4X-FIXME
796             // if (list.*.length() === list.text().length() + 2)
797             //     dactyl.echomsg(_("command.none"));
798             // else
799             commandline.commandOutput(list);
800         }
801     }),
802
803     /**
804      * @property Indicates that no count was specified for this
805      *     command invocation.
806      * @final
807      */
808     COUNT_NONE: null,
809     /**
810      * @property {number} Indicates that the full buffer range (1,$) was
811      *     specified for this command invocation.
812      * @final
813      */
814     // FIXME: this isn't a count at all
815     COUNT_ALL: -2, // :%...
816
817     /** @property {Iterator(Command)} @private */
818     iterator: function iterator() iter.apply(null, this.hives.array)
819                               .sort(function (a, b) a.serialGroup - b.serialGroup || a.name > b.name)
820                               .iterValues(),
821
822     /** @property {string} The last executed Ex command line. */
823     repeat: null,
824
825     add: function add() {
826         let group = this.builtin;
827         if (!util.isDactyl(Components.stack.caller)) {
828             deprecated.warn(add, "commands.add", "group.commands.add");
829             group = this.user;
830         }
831
832         return group._add.apply(group, arguments);
833     },
834     addUserCommand: deprecated("group.commands.add", { get: function addUserCommand() this.user.closure._add }),
835     getUserCommands: deprecated("iter(group.commands)", function getUserCommands() iter(this.user).toArray()),
836     removeUserCommand: deprecated("group.commands.remove", { get: function removeUserCommand() this.user.closure.remove }),
837
838     /**
839      * Returns the specified command invocation object serialized to
840      * an executable Ex command string.
841      *
842      * @param {Object} args The command invocation object.
843      * @returns {string}
844      */
845     commandToString: function commandToString(args) {
846         let res = [args.command + (args.bang ? "!" : "")];
847
848         let defaults = {};
849         if (args.ignoreDefaults)
850             defaults = array(this.options).map(function (opt) [opt.names[0], opt.default])
851                                           .toObject();
852
853         for (let [opt, val] in Iterator(args.options || {})) {
854             if (val === undefined)
855                 continue;
856             if (val != null && defaults[opt] === val)
857                 continue;
858
859             let chr = /^-.$/.test(opt) ? " " : "=";
860             if (isArray(val))
861                 opt += chr + Option.stringify.stringlist(val);
862             else if (val != null)
863                 opt += chr + Commands.quote(val);
864             res.push(opt);
865         }
866
867         for (let [, arg] in Iterator(args.arguments || []))
868             res.push(Commands.quote(arg));
869
870         let str = args.literalArg;
871         if (str)
872             res.push(!/\n/.test(str) ? str :
873                      this.serializeHereDoc ? "<<EOF\n" + String.replace(str, /\n$/, "") + "\nEOF"
874                                            : String.replace(str, /\n/g, "\n" + res[0].replace(/./g, " ").replace(/.$/, "\\")));
875         return res.join(" ");
876     },
877
878     /**
879      * Returns the command with matching *name*.
880      *
881      * @param {string} name The name of the command to return. This can be
882      *     any of the command's names.
883      * @returns {Command}
884      */
885     get: function get(name, full) iter(this.hives).map(function ([i, hive]) hive.get(name, full))
886                                                   .nth(util.identity, 0),
887
888     /**
889      * Returns true if a command invocation contains a URL referring to the
890      * domain *host*.
891      *
892      * @param {string} command
893      * @param {string} host
894      * @returns {boolean}
895      */
896     hasDomain: function hasDomain(command, host) {
897         try {
898             for (let [cmd, args] in this.subCommands(command))
899                 if (Array.concat(cmd.domains(args)).some(function (domain) util.isSubdomain(domain, host)))
900                     return true;
901         }
902         catch (e) {
903             util.reportError(e);
904         }
905         return false;
906     },
907
908     /**
909      * Returns true if a command invocation contains private data which should
910      * be cleared when purging private data.
911      *
912      * @param {string} command
913      * @returns {boolean}
914      */
915     hasPrivateData: function hasPrivateData(command) {
916         for (let [cmd, args] in this.subCommands(command))
917             if (cmd.privateData)
918                 return !callable(cmd.privateData) ? cmd.privateData
919                                                   : cmd.privateData(args);
920         return false;
921     },
922
923     // TODO: should it handle comments?
924     //     : it might be nice to be able to specify that certain quoting
925     //     should be disabled E.g. backslash without having to resort to
926     //     using literal etc.
927     //     : error messages should be configurable or else we can ditch
928     //     Vim compatibility but it actually gives useful messages
929     //     sometimes rather than just "Invalid arg"
930     //     : I'm not sure documenting the returned object here, and
931     //     elsewhere, as type Args rather than simply Object makes sense,
932     //     especially since it is further augmented for use in
933     //     Command#action etc.
934     /**
935      * Parses *str* for options and plain arguments.
936      *
937      * The returned *Args* object is an augmented array of arguments.
938      * Any key/value pairs of *extra* will be available and the
939      * following additional properties:
940      *     -opt       - the value of the option -opt if specified
941      *     string     - the original argument string *str*
942      *     literalArg - any trailing literal argument
943      *
944      * Quoting rules:
945      *     '-quoted strings   - only ' and \ itself are escaped
946      *     "-quoted strings   - also ", \n and \t are translated
947      *     non-quoted strings - everything is taken literally apart from "\
948      *                          " and "\\"
949      *
950      * @param {string} str The Ex command-line string to parse. E.g.
951      *     "-x=foo -opt=bar arg1 arg2"
952      * @param {[CommandOption]} options The options accepted. These are specified
953      *      as an array of {@link CommandOption} structures.
954      * @param {string} argCount The number of arguments accepted.
955      *            "0": no arguments
956      *            "1": exactly one argument
957      *            "+": one or more arguments
958      *            "*": zero or more arguments (default if unspecified)
959      *            "?": zero or one arguments
960      * @param {boolean} allowUnknownOptions Whether unspecified options
961      *     should cause an error.
962      * @param {number} literal The index at which any literal arg begins.
963      *     See {@link Command#literal}.
964      * @param {CompletionContext} complete The relevant completion context
965      *     when the args are being parsed for completion.
966      * @param {Object} extra Extra keys to be spliced into the returned
967      *     Args object.
968      * @returns {Args}
969      */
970     parseArgs: function parseArgs(str, params) {
971         const self = this;
972
973         function getNextArg(str, _keepQuotes) {
974             if (arguments.length < 2)
975                 _keepQuotes = keepQuotes;
976
977             if (str.substr(0, 2) === "<<" && hereDoc) {
978                 let arg = /^<<(\S*)/.exec(str)[1];
979                 let count = arg.length + 2;
980                 if (complete)
981                     return [count, "", ""];
982                 return [count, self.readHeredoc(arg), ""];
983             }
984
985             let [count, arg, quote] = Commands.parseArg(str, null, _keepQuotes);
986             if (quote == "\\" && !complete)
987                 return [, , , _("error.trailingCharacters", "\\")];
988             if (quote && !complete)
989                 return [, , , _("error.missingQuote", quote)];
990             return [count, arg, quote];
991         }
992
993         try {
994
995             var { allowUnknownOptions, argCount, complete, extra, hereDoc, literal, options, keepQuotes } = params || {};
996
997             if (!options)
998                 options = [];
999
1000             if (!argCount)
1001                 argCount = "*";
1002
1003             var args = params.newArgs ? params.newArgs() : [];
1004             args.string = str; // for access to the unparsed string
1005
1006             // FIXME!
1007             for (let [k, v] in Iterator(extra || []))
1008                 args[k] = v;
1009
1010             // FIXME: best way to specify these requirements?
1011             var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0; // after a -- has been found
1012             var arg = null;
1013             var i = 0;
1014             var completeOpts;
1015
1016             // XXX
1017             let matchOpts = function matchOpts(arg) {
1018                 // Push possible option matches into completions
1019                 if (complete && !onlyArgumentsRemaining)
1020                     completeOpts = options.filter(function (opt) opt.multiple || !Set.has(args, opt.names[0]));
1021             };
1022             let resetCompletions = function resetCompletions() {
1023                 completeOpts = null;
1024                 args.completeArg = null;
1025                 args.completeOpt = null;
1026                 args.completeFilter = null;
1027                 args.completeStart = i;
1028                 args.quote = Commands.complQuote[""];
1029             };
1030             if (complete) {
1031                 resetCompletions();
1032                 matchOpts("");
1033                 args.completeArg = 0;
1034             }
1035
1036             let fail = function fail(error) {
1037                 if (complete)
1038                     complete.message = error;
1039                 else
1040                     util.assert(false, error);
1041             };
1042
1043             outer:
1044             while (i < str.length || complete) {
1045                 var argStart = i;
1046                 let re = /\s*/gy;
1047                 re.lastIndex = i;
1048                 i += re.exec(str)[0].length;
1049
1050                 if (str[i] == "|") {
1051                     args.string = str.slice(0, i);
1052                     args.trailing = str.slice(i + 1);
1053                     break;
1054                 }
1055                 if (i == str.length && !complete)
1056                     break;
1057
1058                 if (complete)
1059                     resetCompletions();
1060
1061                 var sub = str.substr(i);
1062                 if ((!onlyArgumentsRemaining) && /^--(\s|$)/.test(sub)) {
1063                     onlyArgumentsRemaining = true;
1064                     i += 2;
1065                     continue;
1066                 }
1067
1068                 var optname = "";
1069                 if (!onlyArgumentsRemaining) {
1070                     for (let [, opt] in Iterator(options)) {
1071                         for (let [, optname] in Iterator(opt.names)) {
1072                             if (sub.indexOf(optname) == 0) {
1073                                 let count = 0;
1074                                 let invalid = false;
1075                                 let arg, quote, quoted;
1076
1077                                 let sep = sub[optname.length];
1078                                 let argString = sub.substr(optname.length + 1);
1079                                 if (sep == "=" || /\s/.test(sep) && opt.type != CommandOption.NOARG) {
1080                                     [count, quoted, quote, error] = getNextArg(argString, true);
1081                                     arg = Option.dequote(quoted);
1082                                     util.assert(!error, error);
1083
1084                                     // if we add the argument to an option after a space, it MUST not be empty
1085                                     if (sep != "=" && !quote && arg.length == 0 && !complete)
1086                                         arg = null;
1087
1088                                     count++; // to compensate the "=" character
1089                                 }
1090                                 else if (!/\s/.test(sep) && sep != undefined) // this isn't really an option as it has trailing characters, parse it as an argument
1091                                     invalid = true;
1092
1093                                 let context = null;
1094                                 if (!complete && quote)
1095                                     fail(_("command.invalidOptArg", optname, argString));
1096
1097                                 if (!invalid) {
1098                                     if (complete && !/[\s=]/.test(sep))
1099                                         matchOpts(sub);
1100
1101                                     if (complete && count > 0) {
1102                                         args.completeStart += optname.length + 1;
1103                                         args.completeOpt = opt;
1104                                         args.completeFilter = arg;
1105                                         args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
1106                                     }
1107                                     if (!complete || arg != null) {
1108                                         if (opt.type) {
1109                                             let orig = arg;
1110                                             arg = opt.type.parse(arg, quoted);
1111
1112                                             if (complete && isArray(arg)) {
1113                                                 args.completeFilter = arg[arg.length - 1] || "";
1114                                                 args.completeStart += orig.length - args.completeFilter.length;
1115                                             }
1116
1117                                             if (arg == null || (typeof arg == "number" && isNaN(arg))) {
1118                                                 if (!complete || orig != "" || args.completeStart != str.length)
1119                                                     fail(_("command.invalidOptTypeArg", opt.type.description, optname, argString));
1120                                                 if (complete)
1121                                                     complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
1122                                             }
1123                                         }
1124
1125                                         // we have a validator function
1126                                         if (typeof opt.validator == "function") {
1127                                             if (opt.validator(arg, quoted) == false && (arg || !complete)) {
1128                                                 fail(_("command.invalidOptArg", optname, argString));
1129                                                 if (complete) // Always true.
1130                                                     complete.highlight(args.completeStart, count - 1, "SPELLCHECK");
1131                                             }
1132                                         }
1133                                     }
1134
1135                                     if (arg != null || opt.type == CommandOption.NOARG) {
1136                                         // option allowed multiple times
1137                                         if (opt.multiple)
1138                                             args[opt.names[0]] = (args[opt.names[0]] || []).concat(arg);
1139                                         else
1140                                             Class.replaceProperty(args, opt.names[0], opt.type == CommandOption.NOARG || arg);
1141
1142                                         args.explicitOpts[opt.names[0]] = args[opt.names[0]];
1143                                     }
1144
1145                                     i += optname.length + count;
1146                                     if (i == str.length)
1147                                         break outer;
1148                                     continue outer;
1149                                 }
1150                                 // if it is invalid, just fall through and try the next argument
1151                             }
1152                         }
1153                     }
1154                 }
1155
1156                 matchOpts(sub);
1157
1158                 if (complete)
1159                     if (argCount == "0" || args.length > 0 && (/[1?]/.test(argCount)))
1160                         complete.highlight(i, sub.length, "SPELLCHECK");
1161
1162                 if (args.length === literal) {
1163                     if (complete)
1164                         args.completeArg = args.length;
1165
1166                     let re = /(?:\s*(?=\n)|\s*)([^]*)/gy;
1167                     re.lastIndex = argStart || 0;
1168                     sub = re.exec(str)[1];
1169
1170                     // Hack.
1171                     if (sub.substr(0, 2) === "<<" && hereDoc)
1172                         let ([count, arg] = getNextArg(sub)) {
1173                             sub = arg + sub.substr(count);
1174                         }
1175
1176                     args.push(sub);
1177                     args.quote = null;
1178                     break;
1179                 }
1180
1181                 // if not an option, treat this token as an argument
1182                 let [count, arg, quote, error] = getNextArg(sub);
1183                 util.assert(!error, error);
1184
1185                 if (complete) {
1186                     args.quote = Commands.complQuote[quote] || Commands.complQuote[""];
1187                     args.completeFilter = arg || "";
1188                 }
1189                 else if (count == -1)
1190                     fail(_("command.parsing", arg));
1191                 else if (!onlyArgumentsRemaining && sub[0] === "-")
1192                     fail(_("command.invalidOpt", arg));
1193
1194                 if (arg != null)
1195                     args.push(arg);
1196                 if (complete)
1197                     args.completeArg = args.length - 1;
1198
1199                 i += count;
1200                 if (count <= 0 || i == str.length)
1201                     break;
1202             }
1203
1204             if (complete && args.trailing == null) {
1205                 if (args.completeOpt) {
1206                     let opt = args.completeOpt;
1207                     let context = complete.fork(opt.names[0], args.completeStart);
1208                     let arg = args.explicitOpts[opt.names[0]];
1209                     context.filter = args.completeFilter;
1210
1211                     if (isArray(arg))
1212                         context.filters.push(function (item) arg.indexOf(item.text) === -1);
1213
1214                     if (typeof opt.completer == "function")
1215                         var compl = opt.completer(context, args);
1216                     else
1217                         compl = opt.completer || [];
1218
1219                     context.title = [opt.names[0]];
1220                     context.quote = args.quote;
1221                     if (compl)
1222                         context.completions = compl;
1223                 }
1224                 complete.advance(args.completeStart);
1225                 complete.keys = {
1226                     text: "names",
1227                     description: function (opt) messages.get(["command", params.name, "options", opt.names[0], "description"].join("."), opt.description)
1228                 };
1229                 complete.title = ["Options"];
1230                 if (completeOpts)
1231                     complete.completions = completeOpts;
1232             }
1233
1234             if (args.verify)
1235                 args.verify();
1236
1237             return args;
1238         }
1239         catch (e if complete && e instanceof FailedAssertion) {
1240             complete.message = e;
1241             return args;
1242         }
1243     },
1244
1245     nameRegexp: util.regexp(literal(/*
1246             [^
1247                 0-9
1248                 <forbid>
1249             ]
1250             [^ <forbid> ]*
1251         */), "gx", {
1252         forbid: util.regexp(String.replace(literal(/*
1253             U0000-U002c // U002d -
1254             U002e-U002f
1255             U003a-U0040 // U0041-U005a a-z
1256             U005b-U0060 // U0061-U007a A-Z
1257             U007b-U00bf
1258             U02b0-U02ff // Spacing Modifier Letters
1259             U0300-U036f // Combining Diacritical Marks
1260             U1dc0-U1dff // Combining Diacritical Marks Supplement
1261             U2000-U206f // General Punctuation
1262             U20a0-U20cf // Currency Symbols
1263             U20d0-U20ff // Combining Diacritical Marks for Symbols
1264             U2400-U243f // Control Pictures
1265             U2440-U245f // Optical Character Recognition
1266             U2500-U257f // Box Drawing
1267             U2580-U259f // Block Elements
1268             U2700-U27bf // Dingbats
1269             Ufe20-Ufe2f // Combining Half Marks
1270             Ufe30-Ufe4f // CJK Compatibility Forms
1271             Ufe50-Ufe6f // Small Form Variants
1272             Ufe70-Ufeff // Arabic Presentation Forms-B
1273             Uff00-Uffef // Halfwidth and Fullwidth Forms
1274             Ufff0-Uffff // Specials
1275         */), /U/g, "\\u"), "x")
1276     }),
1277
1278     validName: Class.Memoize(function validName() util.regexp("^" + this.nameRegexp.source + "$")),
1279
1280     commandRegexp: Class.Memoize(function commandRegexp() util.regexp(literal(/*
1281             ^
1282             (?P<spec>
1283                 (?P<prespace> [:\s]*)
1284                 (?P<count>    (?:\d+ | %)? )
1285                 (?P<fullCmd>
1286                     (?: (?P<group> <name>) : )?
1287                     (?P<cmd>      (?:-? [()] | <name> | !)? ))
1288                 (?P<bang>     !?)
1289                 (?P<space>    \s*)
1290             )
1291             (?P<args>
1292                 (?:. | \n)*?
1293             )?
1294             $
1295         */), "x", {
1296             name: this.nameRegexp
1297         })),
1298
1299     /**
1300      * Parses a complete Ex command.
1301      *
1302      * The parsed string is returned as an Array like
1303      * [count, command, bang, args]:
1304      *     count   - any count specified
1305      *     command - the Ex command name
1306      *     bang    - whether the special "bang" version was called
1307      *     args    - the commands full argument string
1308      * E.g. ":2foo! bar" -> [2, "foo", true, "bar"]
1309      *
1310      * @param {string} str The Ex command line string.
1311      * @returns {Array}
1312      */
1313     // FIXME: why does this return an Array rather than Object?
1314     parseCommand: function parseCommand(str) {
1315         // remove comments
1316         str.replace(/\s*".*$/, "");
1317
1318         let matches = this.commandRegexp.exec(str);
1319         if (!matches)
1320             return [];
1321
1322         let { spec, count, group, cmd, bang, space, args } = matches;
1323         if (!cmd && bang)
1324             [cmd, bang] = [bang, cmd];
1325
1326         if (!cmd || args && args[0] != "|" && !(space || cmd == "!"))
1327             return [];
1328
1329         // parse count
1330         if (count)
1331             count = count == "%" ? this.COUNT_ALL : parseInt(count, 10);
1332         else
1333             count = this.COUNT_NONE;
1334
1335         return [count, cmd, !!bang, args || "", spec.length, group];
1336     },
1337
1338     parseCommands: function parseCommands(str, complete) {
1339         const { contexts } = this.modules;
1340         do {
1341             let [count, cmd, bang, args, len, group] = commands.parseCommand(str);
1342             if (!group)
1343                 var command = this.get(cmd || "");
1344             else if (group = contexts.getGroup(group, "commands"))
1345                 command = group.get(cmd || "");
1346
1347             if (command == null) {
1348                 yield [null, { commandString: str }];
1349                 return;
1350             }
1351
1352             if (complete)
1353                 var context = complete.fork(command.name).fork("opts", len);;
1354
1355             if (!complete || /(\w|^)[!\s]/.test(str))
1356                 args = command.parseArgs(args, context, { count: count, bang: bang });
1357             else
1358                 args = this.parseArgs(args, { extra: { count: count, bang: bang } });
1359             args.context = this.context;
1360             args.commandName = cmd;
1361             args.commandString = str.substr(0, len) + args.string;
1362             str = args.trailing;
1363             yield [command, args];
1364             if (args.break)
1365                 break;
1366         }
1367         while (str);
1368     },
1369
1370     subCommands: function subCommands(command) {
1371         let commands = [command];
1372         while (command = commands.shift())
1373             try {
1374                 for (let [command, args] in this.parseCommands(command)) {
1375                     if (command) {
1376                         yield [command, args];
1377                         if (command.subCommand && args[command.subCommand])
1378                             commands.push(args[command.subCommand]);
1379                     }
1380                 }
1381             }
1382             catch (e) {}
1383     },
1384
1385     /** @property */
1386     get complQuote() Commands.complQuote,
1387
1388     /** @property */
1389     get quoteArg() Commands.quoteArg // XXX: better somewhere else?
1390
1391 }, {
1392     // returns [count, parsed_argument]
1393     parseArg: function parseArg(str, sep, keepQuotes) {
1394         let arg = "";
1395         let quote = null;
1396         let len = str.length;
1397
1398         function fixEscapes(str) str.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4}|(.))/g, function (m, n1) n1 || m);
1399
1400         // Fix me.
1401         if (isString(sep))
1402             sep = RegExp(sep);
1403         sep = sep != null ? sep : /\s/;
1404         let re1 = RegExp("^" + (sep.source === "" ? "(?!)" : sep.source));
1405         let re2 = RegExp(/^()((?:[^\\S"']|\\.)+)((?:\\$)?)/.source.replace("S", sep.source));
1406
1407         while (str.length && !re1.test(str)) {
1408             let res;
1409             if ((res = re2.exec(str)))
1410                 arg += keepQuotes ? res[0] : res[2].replace(/\\(.)/g, "$1");
1411             else if ((res = /^(")((?:[^\\"]|\\.)*)("?)/.exec(str)))
1412                 arg += keepQuotes ? res[0] : JSON.parse(fixEscapes(res[0]) + (res[3] ? "" : '"'));
1413             else if ((res = /^(')((?:[^']|'')*)('?)/.exec(str)))
1414                 arg += keepQuotes ? res[0] : res[2].replace("''", "'", "g");
1415             else
1416                 break;
1417
1418             if (!res[3])
1419                 quote = res[1];
1420             if (!res[1])
1421                 quote = res[3];
1422             str = str.substr(res[0].length);
1423         }
1424
1425         return [len - str.length, arg, quote];
1426     },
1427
1428     quote: function quote(str) Commands.quoteArg[
1429         /[\b\f\n\r\t]/.test(str)   ? '"' :
1430         /[\s"'\\]|^$|^-/.test(str) ? "'"
1431                                    : ""](str)
1432 }, {
1433     completion: function initCompletion(dactyl, modules, window) {
1434         const { completion, contexts } = modules;
1435
1436         completion.command = function command(context, group) {
1437             context.title = ["Command"];
1438             context.keys = { text: "longNames", description: "description" };
1439             if (group)
1440                 context.generate = function () group._list;
1441             else
1442                 context.generate = function () modules.commands.hives.map(function (h) h._list).flatten();
1443         };
1444
1445         // provides completions for ex commands, including their arguments
1446         completion.ex = function ex(context) {
1447             const { commands } = modules;
1448
1449             // if there is no space between the command name and the cursor
1450             // then get completions of the command name
1451             for (var [command, args] in commands.parseCommands(context.filter, context))
1452                 if (args.trailing)
1453                     context.advance(args.commandString.length + 1);
1454             if (!args)
1455                 args = { commandString: context.filter };
1456
1457             let match = commands.commandRegexp.exec(args.commandString);
1458             if (!match)
1459                 return;
1460
1461             if (match.group)
1462                 context.advance(match.group.length + 1);
1463
1464             context.advance(match.prespace.length + match.count.length);
1465             if (!(match.bang || match.space)) {
1466                 context.fork("", 0, this, "command", match.group && contexts.getGroup(match.group, "commands"));
1467                 return;
1468             }
1469
1470             // dynamically get completions as specified with the command's completer function
1471             context.highlight();
1472             if (!command) {
1473                 context.message = _("command.noSuch", match.cmd);
1474                 context.highlight(0, match.cmd.length, "SPELLCHECK");
1475                 return;
1476             }
1477
1478             let cmdContext = context.fork(command.name + "/args", match.fullCmd.length + match.bang.length + match.space.length);
1479             try {
1480                 if (!cmdContext.waitingForTab) {
1481                     if (!args.completeOpt && command.completer && args.completeStart != null) {
1482                         cmdContext.advance(args.completeStart);
1483                         cmdContext.quote = args.quote;
1484                         cmdContext.filter = args.completeFilter;
1485                         command.completer.call(command, cmdContext, args);
1486                     }
1487                 }
1488             }
1489             catch (e) {
1490                 util.reportError(e);
1491                 cmdContext.message = _("error.error", e);
1492             }
1493         };
1494
1495         completion.exMacro = function exMacro(context, args, cmd) {
1496             if (!cmd.action.macro)
1497                 return;
1498             let { macro } = cmd.action;
1499
1500             let start = "«%-d-]'", end = "'[-d-%»";
1501
1502             let n = /^\d+$/.test(cmd.argCount) ? parseInt(cmd.argCount) : 12;
1503             for (let i = args.completeArg; i < n; i++)
1504                 args[i] = start + i + end;
1505
1506             let params = {
1507                 args: { __proto__: args, toString: function () this.join(" ") },
1508                 bang:  args.bang ? "!" : "",
1509                 count: args.count
1510             };
1511
1512             if (!macro.valid(params))
1513                 return;
1514
1515             let str = macro(params);
1516             let idx = str.indexOf(start);
1517             if (!~idx || !/^(')?(\d+)'/.test(str.substr(idx + start.length))
1518                     || RegExp.$2 != args.completeArg)
1519                 return;
1520
1521             let quote = RegExp.$2;
1522             context.quote = null;
1523             context.offset -= idx;
1524             context.filter = str.substr(0, idx) + (quote ? Option.quote : util.identity)(context.filter);
1525
1526             context.fork("ex", 0, completion, "ex");
1527         };
1528
1529         completion.userCommand = function userCommand(context, group) {
1530             context.title = ["User Command", "Definition"];
1531             context.keys = { text: "name", description: "replacementText" };
1532             context.completions = group || modules.commands.user;
1533         };
1534     },
1535
1536     commands: function initCommands(dactyl, modules, window) {
1537         const { commands, contexts } = modules;
1538
1539         commands.add(["(", "-("], "",
1540             function (args) { dactyl.echoerr(_("dactyl.cheerUp")); },
1541             { hidden: true });
1542         commands.add([")", "-)"], "",
1543             function (args) { dactyl.echoerr(_("dactyl.somberDown")); },
1544             { hidden: true });
1545
1546         commands.add(["com[mand]"],
1547             "List or define commands",
1548             function (args) {
1549                 let cmd = args[0];
1550
1551                 util.assert(!cmd || cmd.split(",").every(commands.validName.closure.test),
1552                             _("command.invalidName", cmd));
1553
1554                 if (args.length <= 1)
1555                     commands.list(cmd, args.explicitOpts["-group"] ? [args["-group"]] : null);
1556                 else {
1557                     util.assert(args["-group"].modifiable,
1558                                 _("group.cantChangeBuiltin", _("command.commands")));
1559
1560                     let completer = args["-complete"];
1561                     let completerFunc = function (context, args) modules.completion.exMacro(context, args, this);
1562
1563                     if (completer) {
1564                         if (/^custom,/.test(completer)) {
1565                             completer = completer.substr(7);
1566
1567                             if (contexts.context)
1568                                 var ctxt = update({}, contexts.context || {});
1569                             completerFunc = function (context) {
1570                                 var result = contexts.withSavedValues(["context"], function () {
1571                                     contexts.context = ctxt;
1572                                     return dactyl.userEval(completer);
1573                                 });
1574                                 if (callable(result))
1575                                     return result.apply(this, Array.slice(arguments));
1576                                 else
1577                                     return context.completions = result;
1578                             };
1579                         }
1580                         else
1581                             completerFunc = function (context) modules.completion.closure[config.completers[completer]](context);
1582                     }
1583
1584                     let added = args["-group"].add(cmd.split(","),
1585                                     args["-description"],
1586                                     contexts.bindMacro(args, "-ex",
1587                                         function makeParams(args, modifiers) ({
1588                                             args: {
1589                                                 __proto__: args,
1590                                                 toString: function () this.string
1591                                             },
1592                                             bang:  this.bang && args.bang ? "!" : "",
1593                                             count: this.count && args.count
1594                                         })),
1595                                     {
1596                                         argCount: args["-nargs"],
1597                                         bang: args["-bang"],
1598                                         count: args["-count"],
1599                                         completer: completerFunc,
1600                                         literal: args["-literal"],
1601                                         persist: !args["-nopersist"],
1602                                         replacementText: args.literalArg,
1603                                         context: contexts.context && update({}, contexts.context)
1604                                     }, args.bang);
1605
1606                     if (!added)
1607                         dactyl.echoerr(_("command.exists"));
1608                 }
1609             }, {
1610                 bang: true,
1611                 completer: function (context, args) {
1612                     const { completion } = modules;
1613                     if (args.completeArg == 0)
1614                         completion.userCommand(context, args["-group"]);
1615                     else
1616                         args["-javascript"] ? completion.javascript(context) : completion.ex(context);
1617                 },
1618                 hereDoc: true,
1619                 options: [
1620                     { names: ["-bang", "-b"],  description: "Command may be followed by a !" },
1621                     { names: ["-count", "-c"], description: "Command may be preceded by a count" },
1622                     {
1623                         // TODO: "E180: invalid complete value: " + arg
1624                         names: ["-complete", "-C"],
1625                         description: "The argument completion function",
1626                         completer: function (context) [[k, ""] for ([k, v] in Iterator(config.completers))],
1627                         type: CommandOption.STRING,
1628                         validator: function (arg) arg in config.completers || /^custom,/.test(arg),
1629                     },
1630                     {
1631                         names: ["-description", "-desc", "-d"],
1632                         description: "A user-visible description of the command",
1633                         default: "User-defined command",
1634                         type: CommandOption.STRING
1635                     },
1636                     contexts.GroupFlag("commands"),
1637                     {
1638                         names: ["-javascript", "-js", "-j"],
1639                         description: "Execute the definition as JavaScript rather than Ex commands"
1640                     },
1641                     {
1642                         names: ["-literal", "-l"],
1643                         description: "Process the specified argument ignoring any quoting or meta characters",
1644                         type: CommandOption.INT
1645                     },
1646                     {
1647                         names: ["-nargs", "-a"],
1648                         description: "The allowed number of arguments",
1649                         completer: [["0", "No arguments are allowed (default)"],
1650                                     ["1", "One argument is allowed"],
1651                                     ["*", "Zero or more arguments are allowed"],
1652                                     ["?", "Zero or one argument is allowed"],
1653                                     ["+", "One or more arguments are allowed"]],
1654                         default: "0",
1655                         type: CommandOption.STRING,
1656                         validator: bind("test", /^[01*?+]$/)
1657                     },
1658                     {
1659                         names: ["-nopersist", "-n"],
1660                         description: "Do not save this command to an auto-generated RC file"
1661                     }
1662                 ],
1663                 literal: 1,
1664
1665                 serialize: function () array(commands.userHives)
1666                     .filter(function (h) h.persist)
1667                     .map(function (hive) [
1668                         {
1669                             command: this.name,
1670                             bang: true,
1671                             options: iter([v, typeof cmd[k] == "boolean" ? null : cmd[k]]
1672                                           // FIXME: this map is expressed multiple times
1673                                           for ([k, v] in Iterator({
1674                                               argCount: "-nargs",
1675                                               bang: "-bang",
1676                                               count: "-count",
1677                                               description: "-description"
1678                                           }))
1679                                           if (cmd[k])).toObject(),
1680                             arguments: [cmd.name],
1681                             literalArg: cmd.action,
1682                             ignoreDefaults: true
1683                         }
1684                         for (cmd in hive) if (cmd.persist)
1685                     ], this)
1686                     .flatten().array
1687             });
1688
1689         commands.add(["delc[ommand]"],
1690             "Delete the specified user-defined command",
1691             function (args) {
1692                 util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
1693                 let name = args[0];
1694
1695                 if (args.bang)
1696                     args["-group"].clear();
1697                 else if (args["-group"].get(name))
1698                     args["-group"].remove(name);
1699                 else
1700                     dactyl.echoerr(_("command.noSuchUser", name));
1701             }, {
1702                 argCount: "?",
1703                 bang: true,
1704                 completer: function (context, args) modules.completion.userCommand(context, args["-group"]),
1705                 options: [contexts.GroupFlag("commands")]
1706             });
1707
1708         commands.add(["comp[letions]"],
1709             "List the completion results for a given command substring",
1710             function (args) { modules.completion.listCompleter("ex", args[0]); },
1711             {
1712                 argCount: "1",
1713                 completer: function (context, args) modules.completion.ex(context),
1714                 literal: 0
1715             });
1716
1717         dactyl.addUsageCommand({
1718             name: ["listc[ommands]", "lc"],
1719             description: "List all Ex commands along with their short descriptions",
1720             index: "ex-cmd",
1721             iterate: function (args) commands.iterator().map(function (cmd) ({
1722                 __proto__: cmd,
1723                 columns: [
1724                     cmd.hive == commands.builtin ? "" : ["span", { highlight: "Object", style: "padding-right: 1em;" },
1725                                                             cmd.hive.name]
1726                 ]
1727             })),
1728             iterateIndex: function (args) let (tags = help.tags)
1729                 this.iterate(args).filter(function (cmd) cmd.hive === commands.builtin || Set.has(tags, cmd.helpTag)),
1730             format: {
1731                 headings: ["Command", "Group", "Description"],
1732                 description: function (cmd) template.linkifyHelp(cmd.description + (cmd.replacementText ? ": " + cmd.action : "")),
1733                 help: function (cmd) ":" + cmd.name
1734             }
1735         });
1736
1737         commands.add(["y[ank]"],
1738             "Yank the output of the given command to the clipboard",
1739             function (args) {
1740                 let cmd = /^:/.test(args[0]) ? args[0] : ":echo " + args[0];
1741
1742                 let res = modules.commandline.withOutputToString(commands.execute, commands, cmd);
1743
1744                 dactyl.clipboardWrite(res);
1745
1746                 let lines = res.split("\n").length;
1747                 dactyl.echomsg(_("command.yank.yankedLine" + (lines == 1 ? "" : "s"), lines));
1748             },
1749             {
1750                 argCount: "1",
1751                 completer: function (context) modules.completion[/^:/.test(context.filter) ? "ex" : "javascript"](context),
1752                 literal: 0
1753             });
1754     },
1755     javascript: function initJavascript(dactyl, modules, window) {
1756         const { JavaScript, commands } = modules;
1757
1758         JavaScript.setCompleter([CommandHive.prototype.get, CommandHive.prototype.remove],
1759                                 [function () [[c.names, c.description] for (c in this)]]);
1760         JavaScript.setCompleter([Commands.prototype.get],
1761                                 [function () [[c.names, c.description] for (c in this.iterator())]]);
1762     },
1763     mappings: function initMappings(dactyl, modules, window) {
1764         const { commands, mappings, modes } = modules;
1765
1766         mappings.add([modes.COMMAND],
1767             ["@:"], "Repeat the last Ex command",
1768             function (args) {
1769                 if (commands.repeat) {
1770                     for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100))
1771                         dactyl.execute(commands.repeat);
1772                 }
1773                 else
1774                     dactyl.echoerr(_("command.noPrevious"));
1775             },
1776             { count: true });
1777     }
1778 });
1779
1780 let quote = function quote(q, list, map) {
1781     map = map || Commands.quoteMap;
1782     let re = RegExp("[" + list + "]", "g");
1783     function quote(str) q + String.replace(str, re, function ($0) $0 in map ? map[$0] : ("\\" + $0)) + q;
1784     quote.list = list;
1785     return quote;
1786 };
1787
1788 Commands.quoteMap = {
1789     "\n": "\\n",
1790     "\t": "\\t",
1791 };
1792
1793 Commands.quoteArg = {
1794     '"': quote('"', '\n\t"\\\\'),
1795     "'": quote("'", "'", { "'": "''" }),
1796     "":  quote("",  "|\\\\\\s'\"")
1797 };
1798 Commands.complQuote = {
1799     '"': ['"', quote("", Commands.quoteArg['"'].list), '"'],
1800     "'": ["'", quote("", Commands.quoteArg["'"].list), "'"],
1801     "":  ["", Commands.quoteArg[""], ""]
1802 };
1803
1804 Commands.parseBool = function (arg) {
1805     if (/^(true|1|on)$/i.test(arg))
1806         return true;
1807     if (/^(false|0|off)$/i.test(arg))
1808         return false;
1809     return NaN;
1810 };
1811
1812 endModule();
1813
1814 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
1815
1816 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: