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