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>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
12 * A class representing key mappings. Instances are created by the
13 * {@link Mappings} class.
15 * @param {number[]} modes The modes in which this mapping is active.
16 * @param {string[]} keys The key sequences which are bound to
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}
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);
41 this.description = description;
44 Object.freeze(this.modes);
47 update(this, extraInfo);
50 name: Class.memoize(function () this.names[0]),
52 /** @property {string[]} All of this mapping's names (key sequences). */
53 names: Class.memoize(function () this._keys.map(function (k) events.canonicalKeys(k))),
55 get toStringParams() [this.modes.map(function (m) m.name), this.names.map(String.quote)],
57 get identifier() [this.modes[0].name, this.hive.prefix + this.names[0]].join("."),
59 /** @property {number} A unique ID for this mapping. */
61 /** @property {number[]} All of the modes for which this mapping applies. */
63 /** @property {function (number)} The function called to execute this mapping. */
65 /** @property {string} This mapping's description, as shown in :listkeys. */
66 description: Messages.Localized(""),
68 /** @property {boolean} Whether this mapping accepts an argument. */
70 /** @property {boolean} Whether this mapping accepts a count. */
73 * @property {boolean} Whether the mapping accepts a motion mapping
77 /** @property {boolean} Whether the RHS of the mapping should expand mappings recursively. */
79 /** @property {boolean} Whether any output from the mapping should be echoed on the command line. */
81 /** @property {string} The literal RHS expansion of this mapping. */
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.
91 * Returns whether this mapping can be invoked by a key sequence matching
94 * @param {string} name The name to query.
97 hasName: function (name) this.keys.indexOf(name) >= 0,
99 get keys() this.names.map(mappings.expandLeader),
102 * Execute the action for this mapping.
104 * @param {object} args The arguments object for the given mapping.
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)
112 args = update({ context: contexts.context },
113 this.hive.argsExtra(args),
117 function repeat() self.action(args)
118 if (this.names[0] != ".") // FIXME: Kludge.
119 mappings.repeat = repeat;
122 util.dumpStack("Attempt to execute mapping recursively: " + args.command);
123 dactyl.assert(!this.executing, _("map.recursive", args.command));
126 this.executing = true;
130 events.feedingKeys = false;
131 dactyl.reportError(e, true);
134 this.executing = false;
143 var MapHive = Class("MapHive", Contexts.Hive, {
144 init: function init(group) {
145 init.supercall(this, group);
150 * Iterates over all mappings present in all of the given *modes*.
152 * @param {[Modes.Mode]} modes The modes for which to return mappings.
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))));
162 * Adds a new default key mapping.
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.
171 add: function (modes, keys, description, action, extra) {
174 let map = Map(modes, keys, description, action, extra);
175 map.definedAt = contexts.getCaller(Components.stack.caller);
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);
183 for (let mode in values(map.modes))
184 this.getStack(mode).add(map);
189 * Returns the mapping stack for the given mode.
191 * @param {Modes.Mode} mode The mode to search.
194 getStack: function getStack(mode) {
195 if (!(mode in this.stacks))
196 return this.stacks[mode] = MapHive.Stack();
197 return this.stacks[mode];
201 * Returns the map from *mode* named *cmd*.
203 * @param {Modes.Mode} mode The mode to search.
204 * @param {string} cmd The map name to match.
205 * @returns {Map|null}
207 get: function (mode, cmd) this.getStack(mode).mappings[cmd],
210 * Returns a count of maps with names starting with but not equal to
213 * @param {Modes.Mode} mode The mode to search.
214 * @param {string} prefix The map prefix string to match.
217 getCandidates: function (mode, prefix) this.getStack(mode).candidates[prefix] || 0,
220 * Returns whether there is a user-defined mapping *cmd* for the specified
223 * @param {Modes.Mode} mode The mode to search.
224 * @param {string} cmd The candidate key mapping.
227 has: function (mode, cmd) this.getStack(mode).mappings[cmd] != null,
230 * Remove the mapping named *cmd* for *mode*.
232 * @param {Modes.Mode} mode The mode to search.
233 * @param {string} cmd The map name to match.
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);
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));
251 * Remove all user-defined mappings for *mode*.
253 * @param {Modes.Mode} mode The mode to remove all mappings from.
255 clear: function (mode) {
256 this.stacks[mode] = MapHive.Stack([]);
259 Stack: Class("Stack", Array, {
260 init: function (ary) {
261 let self = ary || [];
262 self.__proto__ = this.__proto__;
266 __iterator__: function () array.iterValues(this),
268 get candidates() this.states.candidates,
269 get mappings() this.states.mappings,
271 add: function (map) {
276 states: Class.memoize(function () {
282 for (let map in this)
283 for (let name in values(map.keys)) {
284 states.mappings[name] = map;
286 for (let key in events.iterKeys(name)) {
289 states.candidates[state] = (states.candidates[state] || 0) + 1;
300 var Mappings = Module("mappings", {
304 repeat: Modes.boundProperty(),
306 get allHives() contexts.allGroups.mappings,
308 get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
310 expandLeader: function (keyString) keyString.replace(/<Leader>/i, options["mapleader"]),
312 iterate: function (mode) {
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))
320 // NOTE: just normal mode for now
321 /** @property {Iterator(Map)} */
322 __iterator__: function () this.iterate(modes.NORMAL),
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)),
331 * Adds a new default key mapping.
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.
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");
347 let map = group.add.apply(group, arguments);
348 map.definedAt = contexts.getCaller(Components.stack.caller);
353 * Adds a new user-defined key mapping.
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}).
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);
370 * Returns the map from *mode* named *cmd*.
372 * @param {number} mode The mode to search.
373 * @param {string} cmd The map name to match.
376 get: function get(mode, cmd) this.hives.map(function (h) h.get(mode, cmd)).compact()[0] || null,
379 * Returns an array of maps with names starting with but not equal to
382 * @param {number} mode The mode to search.
383 * @param {string} prefix The map prefix string to match.
386 getCandidates: function (mode, prefix)
387 this.hives.map(function (h) h.getCandidates(mode, prefix))
391 * Lists all user-defined mappings matching *filter* for the specified
394 * @param {number[]} modes An array of modes to search.
395 * @param {string} filter The filter string to match.
397 list: function (modes, filter, hives) {
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(/^ /, "");
403 hives = (hives || mappings.userHives).map(function (h) [h, maps(h)])
404 .filter(function ([h, m]) m.length);
406 function maps(hive) {
407 let maps = iter.toArray(hive.iterate(modes));
409 maps = maps.filter(function (m) m.names[0] === filter);
414 <tr highlight="Title">
416 <td style="padding-right: 1em;">Mode</td>
417 <td style="padding-right: 1em;">Command</td>
418 <td style="padding-right: 1em;">Action</td>
420 <col style="min-width: 6em; padding-right: 1em;"/>
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)
427 <td highlight="Title">{!i++ ? hive.name : ""}</td>
430 <td>{map.rhs || map.action.toSource()}</td>
432 <tr style="height: .5ex;"/>)
436 // TODO: Move this to an ItemList to show this automatically
437 if (list.*.length() === list.text().length() + 2)
438 dactyl.echomsg(_("map.none"));
440 commandline.commandOutput(list);
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
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;
458 mappings.list(mapmodes, null, hives);
462 let [lhs, rhs] = args;
464 args["-builtin"] = true;
466 if (!rhs) // list the mapping
467 mappings.list(mapmodes, mappings.expandLeader(lhs), hives);
469 util.assert(args["-group"].modifiable,
470 _("map.builtinImmutable"));
472 args["-group"].add(mapmodes, [lhs],
473 args["-description"],
474 contexts.bindMacro(args, "-keys", function (params) params),
477 count: args["-count"],
478 noremap: args["-builtin"],
479 persist: !args["-nopersist"],
480 get rhs() String(this.action),
481 silent: args["-silent"]
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);
495 return completion.ex(context);
502 names: ["-arg", "-a"],
503 description: "Accept an argument after the requisite key press",
506 names: ["-builtin", "-b"],
507 description: "Execute this mapping as if there were no user-defined mappings"
510 names: ["-count", "-c"],
511 description: "Accept a count before the requisite key press"
514 names: ["-description", "-desc", "-d"],
515 description: "A description of this mapping",
516 default: "User-defined mapping",
517 type: CommandOption.STRING
520 names: ["-ex", "-e"],
521 description: "Execute this mapping as an Ex command rather than keys"
523 contexts.GroupFlag("mappings"),
525 names: ["-javascript", "-js", "-j"],
526 description: "Execute this mapping as JavaScript rather than keys"
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"]
535 names: ["-nopersist", "-n"],
536 description: "Do not save this mapping to an auto-generated RC file"
539 names: ["-silent", "-s", "<silent>", "<Silent>"],
540 description: "Do not echo any generated keys to the command line"
543 serialize: function () {
544 return this.name != "map" ? [] :
545 array(mappings.userHives)
546 .filter(function (h) h.persist)
547 .map(function (hive) [
551 hive.name !== "user" && ["-group", hive.name],
552 ["-modes", uniqueModes(map.modes)],
553 ["-description", map.description],
554 map.silent && ["-silent"]])
556 .filter(util.identity)
558 arguments: [map.names[0]],
562 for (map in userMappings(hive))
568 function userMappings(hive) {
570 for (let stack in values(hive.stacks))
571 for (let map in array.iterValues(stack))
572 if (!set.add(seen, map.id))
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); },
582 commands.add([ch + "no[remap]"],
583 "Map a key sequence without remapping keys" + modeDescription,
584 function (args) { map(args, true); },
587 commands.add([ch + "unm[ap]"],
588 "Remove a mapping" + modeDescription,
590 util.assert(args["-group"].modifiable, _("map.builtinImmutable"));
592 util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
594 let mapmodes = array.uniq(args["-modes"].map(findMode));
597 for (let mode in values(mapmodes))
599 args["-group"].clear(mode);
600 else if (args["-group"].has(mode, args[0])) {
601 args["-group"].remove(mode, args[0]);
605 if (!found && !args.bang)
606 dactyl.echoerr(_("map.noSuch", args[0]));
611 completer: opts.completer,
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"]
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))
633 function findMode(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())
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));
647 commands.add(["feedkeys", "fk"],
649 function (args) { events.feedkeys(args[0] || "", args.bang, false, findMode(args["-mode"])); },
655 update({}, modeFlag, {
656 description: "The mode in which to feed the keys"
661 addMapCommands("", [modes.NORMAL, modes.VISUAL], "");
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()]);
670 getMode: function (args) findMode(args["-mode"]),
671 iterate: function (args, mainOnly) {
672 let modes = [this.getMode(args)];
674 modes = modes.concat(modes[0].bases);
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)) {
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>
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>
698 {template.linkifyHelp(map.description + (map.rhs ? ": " + map.rhs : ""))}
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"]
706 dactyl.addUsageCommand({
708 name: ["listk[eys]", "lk"],
709 description: "List all mappings along with their short descriptions",
711 update({}, modeFlag, {
713 description: "The mode for which to list mappings"
718 iter.forEach(modes.mainModes, function (mode) {
719 if (mode.char && !commands.get(mode.char + "listkeys", true))
720 dactyl.addUsageCommand({
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,
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_);
744 javascript: function initJavascript(dactyl, modules, window) {
745 JavaScript.setCompleter([mappings.get, mappings.builtin.get],
748 function (context, obj, args) [[m.names, m.description] for (m in this.iterate(args[0]))]
751 options: function initOptions(dactyl, modules, window) {
752 options.add(["mapleader", "ml"],
753 "Define the replacement keys for the <Leader> pseudo-key",
755 setter: function (value) {
757 for (let hive in values(mappings.allHives))
758 for (let stack in values(hive.stacks))
766 // vim: set fdm=marker sw=4 ts=4 et: