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