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