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