]> git.donarmstrong.com Git - dactyl.git/blob - common/content/mappings.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / mappings.js
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 /** @scope modules */
10
11 /**
12  * A class representing key mappings. Instances are created by the
13  * {@link Mappings} class.
14  *
15  * @param {[Modes.Mode]} modes The modes in which this mapping is active.
16  * @param {[string]} keys The key sequences which are bound to
17  *     *action*.
18  * @param {string} description A short one line description of the key mapping.
19  * @param {function} action The action invoked by each key sequence.
20  * @param {Object} extraInfo An optional extra configuration hash. The
21  *     following properties are supported.
22  *         arg     - see {@link Map#arg}
23  *         count   - see {@link Map#count}
24  *         motion  - see {@link Map#motion}
25  *         noremap - see {@link Map#noremap}
26  *         rhs     - see {@link Map#rhs}
27  *         silent  - see {@link Map#silent}
28  * @optional
29  * @private
30  */
31 var Map = Class("Map", {
32     init: function (modes, keys, description, action, extraInfo) {
33         this.id = ++Map.id;
34         this.modes = modes;
35         this._keys = keys;
36         this.action = action;
37         this.description = description;
38
39         Object.freeze(this.modes);
40
41         if (extraInfo)
42             this.update(extraInfo);
43     },
44
45     name: Class.memoize(function () this.names[0]),
46
47     /** @property {[string]} All of this mapping's names (key sequences). */
48     names: Class.memoize(function () this._keys.map(function (k) events.canonicalKeys(k))),
49
50     get toStringParams() [this.modes.map(function (m) m.name), this.names.map(String.quote)],
51
52     get identifier() [this.modes[0].name, this.hive.prefix + this.names[0]].join("."),
53
54     /** @property {number} A unique ID for this mapping. */
55     id: null,
56     /** @property {[Modes.Mode]} All of the modes for which this mapping applies. */
57     modes: null,
58     /** @property {function (number)} The function called to execute this mapping. */
59     action: null,
60     /** @property {string} This mapping's description, as shown in :listkeys. */
61     description: Messages.Localized(""),
62
63     /** @property {boolean} Whether this mapping accepts an argument. */
64     arg: false,
65     /** @property {boolean} Whether this mapping accepts a count. */
66     count: false,
67     /**
68      * @property {boolean} Whether the mapping accepts a motion mapping
69      *     as an argument.
70      */
71     motion: false,
72     /** @property {boolean} Whether the RHS of the mapping should expand mappings recursively. */
73     noremap: false,
74     /** @property {boolean} Whether any output from the mapping should be echoed on the command line. */
75     silent: false,
76     /** @property {string} The literal RHS expansion of this mapping. */
77     rhs: null,
78     /**
79      * @property {boolean} Specifies whether this is a user mapping. User
80      *     mappings may be created by plugins, or directly by users. Users and
81      *     plugin authors should create only user mappings.
82      */
83     user: false,
84
85     /**
86      * Returns whether this mapping can be invoked by a key sequence matching
87      * *name*.
88      *
89      * @param {string} name The name to query.
90      * @returns {boolean}
91      */
92     hasName: function (name) this.keys.indexOf(name) >= 0,
93
94     get keys() array.flatten(this.names.map(mappings.closure.expand)),
95
96     /**
97      * Execute the action for this mapping.
98      *
99      * @param {object} args The arguments object for the given mapping.
100      */
101     execute: function (args) {
102         if (!isObject(args)) // Backwards compatibility :(
103             args = iter(["motion", "count", "arg", "command"])
104                 .map(function ([i, prop]) [prop, this[i]], arguments)
105                 .toObject();
106
107         args = update({ context: contexts.context },
108                       this.hive.argsExtra(args),
109                       args);
110
111         let self = this;
112         function repeat() self.action(args)
113         if (this.names[0] != ".") // FIXME: Kludge.
114             mappings.repeat = repeat;
115
116         if (this.executing)
117             util.dumpStack(_("map.recursive", args.command));
118         dactyl.assert(!this.executing, _("map.recursive", args.command));
119
120         try {
121             this.executing = true;
122             var res = repeat();
123         }
124         catch (e) {
125             events.feedingKeys = false;
126             dactyl.reportError(e, true);
127         }
128         finally {
129             this.executing = false;
130         }
131         return res;
132     }
133
134 }, {
135     id: 0
136 });
137
138 var MapHive = Class("MapHive", Contexts.Hive, {
139     init: function init(group) {
140         init.supercall(this, group);
141         this.stacks = {};
142     },
143
144     /**
145      * Iterates over all mappings present in all of the given *modes*.
146      *
147      * @param {[Modes.Mode]} modes The modes for which to return mappings.
148      */
149     iterate: function (modes) {
150         let stacks = Array.concat(modes).map(this.closure.getStack);
151         return values(stacks.shift().sort(function (m1, m2) String.localeCompare(m1.name, m2.name))
152             .filter(function (map) map.rhs &&
153                 stacks.every(function (stack) stack.some(function (m) m.rhs && m.rhs === map.rhs && m.name === map.name))));
154     },
155
156     /**
157      * Adds a new key mapping.
158      *
159      * @param {[Modes.Mode]} modes The modes that this mapping applies to.
160      * @param {[string]} keys The key sequences which are bound to *action*.
161      * @param {string} description A description of the key mapping.
162      * @param {function} action The action invoked by each key sequence.
163      * @param {Object} extra An optional extra configuration hash.
164      * @optional
165      */
166     add: function (modes, keys, description, action, extra) {
167         extra = extra || {};
168
169         modes = Array.concat(modes);
170         if (!modes.every(util.identity))
171             throw TypeError(/*L*/"Invalid modes: " + modes);
172
173         let map = Map(modes, keys, description, action, extra);
174         map.definedAt = contexts.getCaller(Components.stack.caller);
175         map.hive = this;
176
177         if (this.name !== "builtin")
178             for (let [, name] in Iterator(map.names))
179                 for (let [, mode] in Iterator(map.modes))
180                     this.remove(mode, name);
181
182         for (let mode in values(map.modes))
183             this.getStack(mode).add(map);
184         return map;
185     },
186
187     /**
188      * Returns the mapping stack for the given mode.
189      *
190      * @param {Modes.Mode} mode The mode to search.
191      * @returns {[Map]}
192      */
193     getStack: function getStack(mode) {
194         if (!(mode in this.stacks))
195             return this.stacks[mode] = MapHive.Stack();
196         return this.stacks[mode];
197     },
198
199     /**
200      * Returns the map from *mode* named *cmd*.
201      *
202      * @param {Modes.Mode} mode The mode to search.
203      * @param {string} cmd The map name to match.
204      * @returns {Map|null}
205      */
206     get: function (mode, cmd) this.getStack(mode).mappings[cmd],
207
208     /**
209      * Returns a count of maps with names starting with but not equal to
210      * *prefix*.
211      *
212      * @param {Modes.Mode} mode The mode to search.
213      * @param {string} prefix The map prefix string to match.
214      * @returns {number)
215      */
216     getCandidates: function (mode, prefix) this.getStack(mode).candidates[prefix] || 0,
217
218     /**
219      * Returns whether there is a user-defined mapping *cmd* for the specified
220      * *mode*.
221      *
222      * @param {Modes.Mode} mode The mode to search.
223      * @param {string} cmd The candidate key mapping.
224      * @returns {boolean}
225      */
226     has: function (mode, cmd) this.getStack(mode).mappings[cmd] != null,
227
228     /**
229      * Remove the mapping named *cmd* for *mode*.
230      *
231      * @param {Modes.Mode} mode The mode to search.
232      * @param {string} cmd The map name to match.
233      */
234     remove: function (mode, cmd) {
235         let stack = this.getStack(mode);
236         for (let [i, map] in array.iterItems(stack)) {
237             let j = map.names.indexOf(cmd);
238             if (j >= 0) {
239                 delete stack.states;
240                 map.names.splice(j, 1);
241                 if (map.names.length == 0) // FIX ME.
242                     for (let [mode, stack] in Iterator(this.stacks))
243                         this.stacks[mode] = MapHive.Stack(stack.filter(function (m) m != map));
244                 return;
245             }
246         }
247     },
248
249     /**
250      * Remove all user-defined mappings for *mode*.
251      *
252      * @param {Modes.Mode} mode The mode to remove all mappings from.
253      */
254     clear: function (mode) {
255         this.stacks[mode] = MapHive.Stack([]);
256     }
257 }, {
258     Stack: Class("Stack", Array, {
259         init: function (ary) {
260             let self = ary || [];
261             self.__proto__ = this.__proto__;
262             return self;
263         },
264
265         __iterator__: function () array.iterValues(this),
266
267         get candidates() this.states.candidates,
268         get mappings() this.states.mappings,
269
270         add: function (map) {
271             this.push(map);
272             delete this.states;
273         },
274
275         states: Class.memoize(function () {
276             var states = {
277                 candidates: {},
278                 mappings: {}
279             };
280
281             for (let map in this)
282                 for (let name in values(map.keys)) {
283                     states.mappings[name] = map;
284                     let state = "";
285                     for (let key in events.iterKeys(name)) {
286                         state += key;
287                         if (state !== name)
288                             states.candidates[state] = (states.candidates[state] || 0) + 1;
289                     }
290                 }
291             return states;
292         })
293     })
294 });
295
296 /**
297  * @instance mappings
298  */
299 var Mappings = Module("mappings", {
300     init: function () {
301     },
302
303     repeat: Modes.boundProperty(),
304
305     get allHives() contexts.allGroups.mappings,
306
307     get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
308
309     expandLeader: function expandLeader(keyString) keyString.replace(/<Leader>/i, function () options["mapleader"]),
310
311     prefixes: Class.memoize(function () {
312         let list = Array.map("CASM", function (s) s + "-");
313
314         return iter(util.range(0, 1 << list.length)).map(function (mask)
315             list.filter(function (p, i) mask & (1 << i)).join("")).toArray().concat("*-");
316     }),
317
318     expand: function expand(keys) {
319         keys = keys.replace(/<leader>/i, options["mapleader"]);
320         if (!/<\*-/.test(keys))
321             return keys;
322
323         return util.debrace(events.iterKeys(keys).map(function (key) {
324             if (/^<\*-/.test(key))
325                 return ["<", this.prefixes, key.slice(3)];
326             return key;
327         }, this).flatten().array).map(function (k) events.canonicalKeys(k));
328     },
329
330     iterate: function (mode) {
331         let seen = {};
332         for (let hive in this.hives.iterValues())
333             for (let map in array(hive.getStack(mode)).iterValues())
334                 if (!Set.add(seen, map.name))
335                     yield map;
336     },
337
338     // NOTE: just normal mode for now
339     /** @property {Iterator(Map)} */
340     __iterator__: function () this.iterate(modes.NORMAL),
341
342     getDefault: deprecated("mappings.builtin.get", function getDefault(mode, cmd) this.builtin.get(mode, cmd)),
343     getUserIterator: deprecated("mappings.user.iterator", function getUserIterator(modes) this.user.iterator(modes)),
344     hasMap: deprecated("group.mappings.has", function hasMap(mode, cmd) this.user.has(mode, cmd)),
345     remove: deprecated("group.mappings.remove", function remove(mode, cmd) this.user.remove(mode, cmd)),
346     removeAll: deprecated("group.mappings.clear", function removeAll(mode) this.user.clear(mode)),
347
348     /**
349      * Adds a new default key mapping.
350      *
351      * @param {[Modes.Mode]} modes The modes that this mapping applies to.
352      * @param {[string]} keys The key sequences which are bound to *action*.
353      * @param {string} description A description of the key mapping.
354      * @param {function} action The action invoked by each key sequence.
355      * @param {Object} extra An optional extra configuration hash.
356      * @optional
357      */
358     add: function add() {
359         let group = this.builtin;
360         if (!util.isDactyl(Components.stack.caller)) {
361             deprecated.warn(add, "mappings.add", "group.mappings.add");
362             group = this.user;
363         }
364
365         let map = group.add.apply(group, arguments);
366         map.definedAt = contexts.getCaller(Components.stack.caller);
367         return map;
368     },
369
370     /**
371      * Adds a new user-defined key mapping.
372      *
373      * @param {[Modes.Mode]} modes The modes that this mapping applies to.
374      * @param {[string]} keys The key sequences which are bound to *action*.
375      * @param {string} description A description of the key mapping.
376      * @param {function} action The action invoked by each key sequence.
377      * @param {Object} extra An optional extra configuration hash (see
378      *     {@link Map#extraInfo}).
379      * @optional
380      */
381     addUserMap: deprecated("group.mappings.add", function addUserMap() {
382         let map = this.user.add.apply(this.user, arguments);
383         map.definedAt = contexts.getCaller(Components.stack.caller);
384         return map;
385     }),
386
387     /**
388      * Returns the map from *mode* named *cmd*.
389      *
390      * @param {Modes.Mode} mode The mode to search.
391      * @param {string} cmd The map name to match.
392      * @returns {Map}
393      */
394     get: function get(mode, cmd) this.hives.map(function (h) h.get(mode, cmd)).compact()[0] || null,
395
396     /**
397      * Returns an array of maps with names starting with but not equal to
398      * *prefix*.
399      *
400      * @param {Modes.Mode} mode The mode to search.
401      * @param {string} prefix The map prefix string to match.
402      * @returns {[Map]}
403      */
404     getCandidates: function (mode, prefix)
405         this.hives.map(function (h) h.getCandidates(mode, prefix))
406                   .flatten(),
407
408     /**
409      * Lists all user-defined mappings matching *filter* for the specified
410      * *modes* in the specified *hives*.
411      *
412      * @param {[Modes.Mode]} modes An array of modes to search.
413      * @param {string} filter The filter string to match. @optional
414      * @param {[MapHive]} hives The map hives to list. @optional
415      */
416     list: function (modes, filter, hives) {
417         let modeSign = modes.map(function (m) m.char || "").join("")
418                      + modes.map(function (m) !m.char ? " " + m.name : "").join("");
419         modeSign = modeSign.replace(/^ /, "");
420
421         hives = (hives || mappings.userHives).map(function (h) [h, maps(h)])
422                                              .filter(function ([h, m]) m.length);
423
424         function maps(hive) {
425             let maps = iter.toArray(hive.iterate(modes));
426             if (filter)
427                 maps = maps.filter(function (m) m.names[0] === filter);
428             return maps;
429         }
430
431         let list = <table>
432                 <tr highlight="Title">
433                     <td/>
434                     <td style="padding-right: 1em;">{_("title.Mode")}</td>
435                     <td style="padding-right: 1em;">{_("title.Command")}</td>
436                     <td style="padding-right: 1em;">{_("title.Action")}</td>
437                 </tr>
438                 <col style="min-width: 6em; padding-right: 1em;"/>
439                 {
440                     template.map(hives, function ([hive, maps]) let (i = 0)
441                         <tr style="height: .5ex;"/> +
442                         template.map(maps, function (map)
443                             template.map(map.names, function (name)
444                             <tr>
445                                 <td highlight="Title">{!i++ ? hive.name : ""}</td>
446                                 <td>{modeSign}</td>
447                                 <td>{name}</td>
448                                 <td>{map.rhs || map.action.toSource()}</td>
449                             </tr>)) +
450                         <tr style="height: .5ex;"/>)
451                 }
452                 </table>;
453
454         // TODO: Move this to an ItemList to show this automatically
455         if (list.*.length() === list.text().length() + 2)
456             dactyl.echomsg(_("map.none"));
457         else
458             commandline.commandOutput(list);
459     }
460 }, {
461 }, {
462     contexts: function initContexts(dactyl, modules, window) {
463         update(Mappings.prototype, {
464             hives: contexts.Hives("mappings", MapHive),
465             user: contexts.hives.mappings.user,
466             builtin: contexts.hives.mappings.builtin
467         });
468     },
469     commands: function initCommands(dactyl, modules, window) {
470         function addMapCommands(ch, mapmodes, modeDescription) {
471             function map(args, noremap) {
472                 let mapmodes = array.uniq(args["-modes"].map(findMode));
473                 let hives = args.explicitOpts["-group"] ? [args["-group"]] : null;
474
475                 if (!args.length) {
476                     mappings.list(mapmodes, null, hives);
477                     return;
478                 }
479
480                 if (args[1] && !/^<nop>$/i.test(args[1])
481                     && !args["-count"] && !args["-ex"] && !args["-javascript"]
482                     && mapmodes.every(function (m) m.count))
483                     args[1] = "<count>" + args[1];
484
485                 let [lhs, rhs] = args;
486                 if (noremap)
487                     args["-builtin"] = true;
488
489                 if (!rhs) // list the mapping
490                     mappings.list(mapmodes, mappings.expandLeader(lhs), hives);
491                 else {
492                     util.assert(args["-group"].modifiable,
493                                 _("map.builtinImmutable"));
494
495                     args["-group"].add(mapmodes, [lhs],
496                         args["-description"],
497                         contexts.bindMacro(args, "-keys", function (params) params),
498                         {
499                             arg: args["-arg"],
500                             count: args["-count"] || !(args["-ex"] || args["-javascript"]),
501                             noremap: args["-builtin"],
502                             persist: !args["-nopersist"],
503                             get rhs() String(this.action),
504                             silent: args["-silent"]
505                         });
506                 }
507             }
508
509             const opts = {
510                 identifier: "map",
511                 completer: function (context, args) {
512                     let mapmodes = array.uniq(args["-modes"].map(findMode));
513                     if (args.length == 1)
514                         return completion.userMapping(context, mapmodes, args["-group"]);
515                     if (args.length == 2) {
516                         if (args["-javascript"])
517                             return completion.javascript(context);
518                         if (args["-ex"])
519                             return completion.ex(context);
520                     }
521                 },
522                 hereDoc: true,
523                 literal: 1,
524                 options: [
525                     {
526                         names: ["-arg", "-a"],
527                         description: "Accept an argument after the requisite key press",
528                     },
529                     {
530                         names: ["-builtin", "-b"],
531                         description: "Execute this mapping as if there were no user-defined mappings"
532                     },
533                     {
534                         names: ["-count", "-c"],
535                         description: "Accept a count before the requisite key press"
536                     },
537                     {
538                         names: ["-description", "-desc", "-d"],
539                         description: "A description of this mapping",
540                         default: /*L*/"User-defined mapping",
541                         type: CommandOption.STRING
542                     },
543                     {
544                         names: ["-ex", "-e"],
545                         description: "Execute this mapping as an Ex command rather than keys"
546                     },
547                     contexts.GroupFlag("mappings"),
548                     {
549                         names: ["-javascript", "-js", "-j"],
550                         description: "Execute this mapping as JavaScript rather than keys"
551                     },
552                     update({}, modeFlag, {
553                         names: ["-modes", "-mode", "-m"],
554                         type: CommandOption.LIST,
555                         description: "Create this mapping in the given modes",
556                         default: mapmodes || ["n", "v"]
557                     }),
558                     {
559                         names: ["-nopersist", "-n"],
560                         description: "Do not save this mapping to an auto-generated RC file"
561                     },
562                     {
563                         names: ["-silent", "-s", "<silent>", "<Silent>"],
564                         description: "Do not echo any generated keys to the command line"
565                     }
566                 ],
567                 serialize: function () {
568                     return this.name != "map" ? [] :
569                         array(mappings.userHives)
570                             .filter(function (h) h.persist)
571                             .map(function (hive) [
572                                 {
573                                     command: "map",
574                                     options: array([
575                                         hive.name !== "user" && ["-group", hive.name],
576                                         ["-modes", uniqueModes(map.modes)],
577                                         ["-description", map.description],
578                                         map.silent && ["-silent"]])
579
580                                         .filter(util.identity)
581                                         .toObject(),
582                                     arguments: [map.names[0]],
583                                     literalArg: map.rhs,
584                                     ignoreDefaults: true
585                                 }
586                                 for (map in userMappings(hive))
587                                 if (map.persist)
588                             ])
589                             .flatten().array;
590                 }
591             };
592             function userMappings(hive) {
593                 let seen = {};
594                 for (let stack in values(hive.stacks))
595                     for (let map in array.iterValues(stack))
596                         if (!Set.add(seen, map.id))
597                             yield map;
598             }
599
600             modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
601             commands.add([ch ? ch + "m[ap]" : "map"],
602                 "Map a key sequence" + modeDescription,
603                 function (args) { map(args, false); },
604                 update({}, opts));
605
606             commands.add([ch + "no[remap]"],
607                 "Map a key sequence without remapping keys" + modeDescription,
608                 function (args) { map(args, true); },
609                 update({}, opts));
610
611             commands.add([ch + "unm[ap]"],
612                 "Remove a mapping" + modeDescription,
613                 function (args) {
614                     util.assert(args["-group"].modifiable, _("map.builtinImmutable"));
615
616                     util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
617
618                     let mapmodes = array.uniq(args["-modes"].map(findMode));
619
620                     let found = 0;
621                     for (let mode in values(mapmodes))
622                         if (args.bang)
623                             args["-group"].clear(mode);
624                         else if (args["-group"].has(mode, args[0])) {
625                             args["-group"].remove(mode, args[0]);
626                             found++;
627                         }
628
629                     if (!found && !args.bang)
630                         dactyl.echoerr(_("map.noSuch", args[0]));
631                 },
632                 {
633                     identifier: "unmap",
634                     argCount: "?",
635                     bang: true,
636                     completer: opts.completer,
637                     options: [
638                         contexts.GroupFlag("mappings"),
639                         update({}, modeFlag, {
640                             names: ["-modes", "-mode", "-m"],
641                             type: CommandOption.LIST,
642                             description: "Remove mapping from the given modes",
643                             default: mapmodes || ["n", "v"]
644                         })
645                     ]
646                 });
647         }
648
649         let modeFlag = {
650             names: ["-mode", "-m"],
651             type: CommandOption.STRING,
652             validator: function (value) Array.concat(value).every(findMode),
653             completer: function () [[array.compact([mode.name.toLowerCase().replace(/_/g, "-"), mode.char]), mode.description]
654                                     for (mode in values(modes.all))
655                                     if (!mode.hidden)]
656         };
657
658         function findMode(name) {
659             if (name)
660                 for (let mode in values(modes.all))
661                     if (name == mode || name == mode.char
662                         || String.toLowerCase(name).replace(/-/g, "_") == mode.name.toLowerCase())
663                         return mode;
664             return null;
665         }
666         function uniqueModes(modes) {
667             let chars = [k for ([k, v] in Iterator(modules.modes.modeChars))
668                          if (v.every(function (mode) modes.indexOf(mode) >= 0))];
669             return array.uniq(modes.filter(function (m) chars.indexOf(m.char) < 0)
670                                    .map(function (m) m.name.toLowerCase())
671                                    .concat(chars));
672         }
673
674         commands.add(["feedkeys", "fk"],
675             "Fake key events",
676             function (args) { events.feedkeys(args[0] || "", args.bang, false, findMode(args["-mode"])); },
677             {
678                 argCount: "1",
679                 bang: true,
680                 literal: 0,
681                 options: [
682                     update({}, modeFlag, {
683                         description: "The mode in which to feed the keys"
684                     })
685                 ]
686             });
687
688         addMapCommands("", [modes.NORMAL, modes.VISUAL], "");
689
690         for (let mode in modes.mainModes)
691             if (mode.char && !commands.get(mode.char + "map", true))
692                 addMapCommands(mode.char,
693                                [m.mask for (m in modes.mainModes) if (m.char == mode.char)],
694                                mode.displayName);
695
696         let args = {
697             getMode: function (args) findMode(args["-mode"]),
698             iterate: function (args, mainOnly) {
699                 let modes = [this.getMode(args)];
700                 if (!mainOnly)
701                     modes = modes[0].allBases;
702
703                 let seen = {};
704                 // Bloody hell. --Kris
705                 for (let [i, mode] in Iterator(modes))
706                     for (let hive in mappings.hives.iterValues())
707                         for (let map in array.iterValues(hive.getStack(mode)))
708                             for (let name in values(map.names))
709                                 if (!Set.add(seen, name)) {
710                                     yield {
711                                         name: name,
712                                         columns: [
713                                             i === 0 ? "" : <span highlight="Object" style="padding-right: 1em;">{mode.name}</span>,
714                                             hive == mappings.builtin ? "" : <span highlight="Object" style="padding-right: 1em;">{hive.name}</span>
715                                         ],
716                                         __proto__: map
717                                     };
718                                 }
719             },
720             format: {
721                 description: function (map) (XML.ignoreWhitespace = false, XML.prettyPrinting = false, <>
722                         {options.get("passkeys").has(map.name)
723                             ? <span highlight="URLExtra">(passed by {template.helpLink("'passkeys'")})</span>
724                             : <></>}
725                         {template.linkifyHelp(map.description + (map.rhs ? ": " + map.rhs : ""))}
726                 </>),
727                 help: function (map) let (char = array.compact(map.modes.map(function (m) m.char))[0])
728                     char === "n" ? map.name : char ? char + "_" + map.name : "",
729                 headings: ["Command", "Mode", "Group", "Description"]
730             }
731         }
732
733         dactyl.addUsageCommand({
734             __proto__: args,
735             name: ["listk[eys]", "lk"],
736             description: "List all mappings along with their short descriptions",
737             options: [
738                 update({}, modeFlag, {
739                     default: "n",
740                     description: "The mode for which to list mappings"
741                 })
742             ]
743         });
744
745         iter.forEach(modes.mainModes, function (mode) {
746             if (mode.char && !commands.get(mode.char + "listkeys", true))
747                 dactyl.addUsageCommand({
748                     __proto__: args,
749                     name: [mode.char + "listk[eys]", mode.char + "lk"],
750                     iterateIndex: function (args)
751                             let (self = this, prefix = /^[bCmn]$/.test(mode.char) ? "" : mode.char + "_",
752                                  tags = services["dactyl:"].HELP_TAGS)
753                                     ({ helpTag: prefix + map.name, __proto__: map }
754                                      for (map in self.iterate(args, true))
755                                      if (map.hive === mappings.builtin || Set.has(tags, prefix + map.name))),
756                     description: "List all " + mode.displayName + " mode mappings along with their short descriptions",
757                     index: mode.char + "-map",
758                     getMode: function (args) mode,
759                     options: []
760                 });
761         });
762     },
763     completion: function initCompletion(dactyl, modules, window) {
764         completion.userMapping = function userMapping(context, modes_, hive) {
765             hive = hive || mappings.user;
766             modes_ = modes_ || [modes.NORMAL];
767             context.keys = { text: function (m) m.names[0], description: function (m) m.description + ": " + m.action };
768             context.completions = hive.iterate(modes_);
769         };
770     },
771     javascript: function initJavascript(dactyl, modules, window) {
772         JavaScript.setCompleter([mappings.get, mappings.builtin.get],
773             [
774                 null,
775                 function (context, obj, args) [[m.names, m.description] for (m in this.iterate(args[0]))]
776             ]);
777     },
778     options: function initOptions(dactyl, modules, window) {
779         options.add(["mapleader", "ml"],
780             "Define the replacement keys for the <Leader> pseudo-key",
781             "string", "\\", {
782                 setter: function (value) {
783                     if (this.hasChanged)
784                         for (let hive in values(mappings.allHives))
785                             for (let stack in values(hive.stacks))
786                                 delete stack.states;
787                     return value;
788                 }
789             });
790     }
791 });
792
793 // vim: set fdm=marker sw=4 ts=4 et: