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