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-2012 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 {[Modes.Mode]} 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} 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}
31 var Map = Class("Map", {
32 init: function (modes, keys, description, action, info) {
37 this.description = description;
39 Object.freeze(this.modes);
42 if (Set.has(Map.types, info.type))
43 this.update(Map.types[info.type]);
48 name: Class.Memoize(function () this.names[0]),
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))),
53 get toStringParams() [this.modes.map(function (m) m.name), this.names.map(String.quote)],
55 get identifier() [this.modes[0].name, this.hive.prefix + this.names[0]].join("."),
57 /** @property {number} A unique ID for this mapping. */
59 /** @property {[Modes.Mode]} All of the modes for which this mapping applies. */
61 /** @property {function (number)} The function called to execute this mapping. */
63 /** @property {string} This mapping's description, as shown in :listkeys. */
64 description: Messages.Localized(""),
66 /** @property {boolean} Whether this mapping accepts an argument. */
68 /** @property {boolean} Whether this mapping accepts a count. */
71 * @property {boolean} Whether the mapping accepts a motion mapping
76 /** @property {boolean} Whether the RHS of the mapping should expand mappings recursively. */
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) {},
84 /** @property {boolean} Whether any output from the mapping should be echoed on the command line. */
87 /** @property {string} The literal RHS expansion of this mapping. */
90 /** @property {string} The type of this mapping. */
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.
101 * Returns whether this mapping can be invoked by a key sequence matching
104 * @param {string} name The name to query.
107 hasName: function (name) this.keys.indexOf(name) >= 0,
109 get keys() array.flatten(this.names.map(mappings.closure.expand)),
112 * Execute the action for this mapping.
114 * @param {object} args The arguments object for the given mapping.
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)
122 args = this.hive.makeArgs(this.hive.group.lastDocument,
126 let repeat = () => this.action(args);
127 if (this.names[0] != ".") // FIXME: Kludge.
128 mappings.repeat = repeat;
131 util.assert(!args.keypressEvents[0].isMacro,
132 _("map.recursive", args.command),
136 dactyl.triggerObserver("mappings.willExecute", this, args);
137 mappings.pushCommand();
138 this.preExecute(args);
139 this.executing = true;
143 events.feedingKeys = false;
144 dactyl.reportError(e, true);
147 this.executing = false;
148 mappings.popCommand();
149 this.postExecute(args);
150 dactyl.triggerObserver("mappings.executed", this, args);
161 var MapHive = Class("MapHive", Contexts.Hive, {
162 init: function init(group) {
163 init.supercall(this, group);
168 * Iterates over all mappings present in all of the given *modes*.
170 * @param {[Modes.Mode]} modes The modes for which to return mappings.
172 iterate: function (modes) {
173 let stacks = Array.concat(modes).map(this.closure.getStack);
174 return values(stacks.shift().sort(function (m1, m2) String.localeCompare(m1.name, m2.name))
175 .filter(function (map) map.rhs &&
176 stacks.every(function (stack) stack.some(function (m) m.rhs && m.rhs === map.rhs && m.name === map.name))));
180 * Adds a new key mapping.
182 * @param {[Modes.Mode]} modes The modes that this mapping applies to.
183 * @param {[string]} keys The key sequences which are bound to *action*.
184 * @param {string} description A description of the key mapping.
185 * @param {function} action The action invoked by each key sequence.
186 * @param {Object} extra An optional extra configuration hash.
189 add: function (modes, keys, description, action, extra) {
192 modes = Array.concat(modes);
193 if (!modes.every(util.identity))
194 throw TypeError(/*L*/"Invalid modes: " + modes);
196 let map = Map(modes, keys, description, action, extra);
197 map.definedAt = contexts.getCaller(Components.stack.caller);
200 if (this.name !== "builtin")
201 for (let [, name] in Iterator(map.names))
202 for (let [, mode] in Iterator(map.modes))
203 this.remove(mode, name);
205 for (let mode in values(map.modes))
206 this.getStack(mode).add(map);
211 * Returns the mapping stack for the given mode.
213 * @param {Modes.Mode} mode The mode to search.
216 getStack: function getStack(mode) {
217 if (!(mode in this.stacks))
218 return this.stacks[mode] = MapHive.Stack();
219 return this.stacks[mode];
223 * Returns the map from *mode* named *cmd*.
225 * @param {Modes.Mode} mode The mode to search.
226 * @param {string} cmd The map name to match.
227 * @returns {Map|null}
229 get: function (mode, cmd) this.getStack(mode).mappings[cmd],
232 * Returns a count of maps with names starting with but not equal to
235 * @param {Modes.Mode} mode The mode to search.
236 * @param {string} prefix The map prefix string to match.
239 getCandidates: function (mode, prefix) this.getStack(mode).candidates[prefix] || 0,
242 * Returns whether there is a user-defined mapping *cmd* for the specified
245 * @param {Modes.Mode} mode The mode to search.
246 * @param {string} cmd The candidate key mapping.
249 has: function (mode, cmd) this.getStack(mode).mappings[cmd] != null,
252 * Remove the mapping named *cmd* for *mode*.
254 * @param {Modes.Mode} mode The mode to search.
255 * @param {string} cmd The map name to match.
257 remove: function (mode, cmd) {
258 let stack = this.getStack(mode);
259 for (let [i, map] in array.iterItems(stack)) {
260 let j = map.names.indexOf(cmd);
263 map.names.splice(j, 1);
264 if (map.names.length == 0) // FIX ME.
265 for (let [mode, stack] in Iterator(this.stacks))
266 this.stacks[mode] = MapHive.Stack(stack.filter(function (m) m != map));
273 * Remove all user-defined mappings for *mode*.
275 * @param {Modes.Mode} mode The mode to remove all mappings from.
277 clear: function (mode) {
278 this.stacks[mode] = MapHive.Stack([]);
281 Stack: Class("Stack", Array, {
282 init: function (ary) {
283 let self = ary || [];
284 self.__proto__ = this.__proto__;
288 __iterator__: function () array.iterValues(this),
290 get candidates() this.states.candidates,
291 get mappings() this.states.mappings,
293 add: function (map) {
298 states: Class.Memoize(function () {
304 for (let map in this)
305 for (let name in values(map.keys)) {
306 states.mappings[name] = map;
308 for (let key in DOM.Event.iterKeys(name)) {
311 states.candidates[state] = (states.candidates[state] || 0) + 1;
322 var Mappings = Module("mappings", {
325 this._watchStack = 0;
329 afterCommands: function afterCommands(count, cmd, self) {
330 this.watches.push([cmd, self, Math.max(this._watchStack - 1, 0), count || 1]);
333 pushCommand: function pushCommand(cmd) {
335 this._yielders = util.yielders;
337 popCommand: function popCommand(cmd) {
338 this._watchStack = Math.max(this._watchStack - 1, 0);
339 if (util.yielders > this._yielders)
340 this._watchStack = 0;
342 this.watches = this.watches.filter(function (elem) {
343 if (this._watchStack <= elem[2])
346 elem[0].call(elem[1] || null);
351 repeat: Modes.boundProperty(),
353 get allHives() contexts.allGroups.mappings,
355 get userHives() this.allHives.filter(function (h) h !== this.builtin, this),
357 expandLeader: deprecated("your brain", function expandLeader(keyString) keyString),
359 prefixes: Class.Memoize(function () {
360 let list = Array.map("CASM", function (s) s + "-");
362 return iter(util.range(0, 1 << list.length)).map(function (mask)
363 list.filter(function (p, i) mask & (1 << i)).join("")).toArray().concat("*-");
366 expand: function expand(keys) {
367 if (!/<\*-/.test(keys))
370 res = util.debrace(DOM.Event.iterKeys(keys).map(function (key) {
371 if (/^<\*-/.test(key))
372 return ["<", this.prefixes, key.slice(3)];
374 }, this).flatten().array).map(function (k) DOM.Event.canonicalKeys(k));
376 if (keys != arguments[0])
377 return [arguments[0]].concat(keys);
381 iterate: function (mode) {
383 for (let hive in this.hives.iterValues())
384 for (let map in array(hive.getStack(mode)).iterValues())
385 if (!Set.add(seen, map.name))
389 // NOTE: just normal mode for now
390 /** @property {Iterator(Map)} */
391 __iterator__: function () this.iterate(modes.NORMAL),
393 getDefault: deprecated("mappings.builtin.get", function getDefault(mode, cmd) this.builtin.get(mode, cmd)),
394 getUserIterator: deprecated("mappings.user.iterator", function getUserIterator(modes) this.user.iterator(modes)),
395 hasMap: deprecated("group.mappings.has", function hasMap(mode, cmd) this.user.has(mode, cmd)),
396 remove: deprecated("group.mappings.remove", function remove(mode, cmd) this.user.remove(mode, cmd)),
397 removeAll: deprecated("group.mappings.clear", function removeAll(mode) this.user.clear(mode)),
400 * Adds a new default key mapping.
402 * @param {[Modes.Mode]} modes The modes that this mapping applies to.
403 * @param {[string]} keys The key sequences which are bound to *action*.
404 * @param {string} description A description of the key mapping.
405 * @param {function} action The action invoked by each key sequence.
406 * @param {Object} extra An optional extra configuration hash.
409 add: function add() {
410 let group = this.builtin;
411 if (!util.isDactyl(Components.stack.caller)) {
412 deprecated.warn(add, "mappings.add", "group.mappings.add");
416 let map = group.add.apply(group, arguments);
417 map.definedAt = contexts.getCaller(Components.stack.caller);
422 * Adds a new user-defined key mapping.
424 * @param {[Modes.Mode]} modes The modes that this mapping applies to.
425 * @param {[string]} keys The key sequences which are bound to *action*.
426 * @param {string} description A description of the key mapping.
427 * @param {function} action The action invoked by each key sequence.
428 * @param {Object} extra An optional extra configuration hash (see
429 * {@link Map#extraInfo}).
432 addUserMap: deprecated("group.mappings.add", function addUserMap() {
433 let map = this.user.add.apply(this.user, arguments);
434 map.definedAt = contexts.getCaller(Components.stack.caller);
439 * Returns the map from *mode* named *cmd*.
441 * @param {Modes.Mode} mode The mode to search.
442 * @param {string} cmd The map name to match.
445 get: function get(mode, cmd) this.hives.map(function (h) h.get(mode, cmd)).compact()[0] || null,
448 * Returns a count of maps with names starting with but not equal to
451 * @param {Modes.Mode} mode The mode to search.
452 * @param {string} prefix The map prefix string to match.
455 getCandidates: function (mode, prefix)
456 this.hives.map(function (h) h.getCandidates(mode, prefix))
457 .reduce(function (a, b) a + b, 0),
460 * Lists all user-defined mappings matching *filter* for the specified
461 * *modes* in the specified *hives*.
463 * @param {[Modes.Mode]} modes An array of modes to search.
464 * @param {string} filter The filter string to match. @optional
465 * @param {[MapHive]} hives The map hives to list. @optional
467 list: function (modes, filter, hives) {
468 let modeSign = modes.map(function (m) m.char || "").join("")
469 + modes.map(function (m) !m.char ? " " + m.name : "").join("");
470 modeSign = modeSign.replace(/^ /, "");
472 hives = (hives || mappings.userHives).map(function (h) [h, maps(h)])
473 .filter(function ([h, m]) m.length);
475 function maps(hive) {
476 let maps = iter.toArray(hive.iterate(modes));
478 maps = maps.filter(function (m) m.names[0] === filter);
482 let list = ["table", {},
483 ["tr", { highlight: "Title" },
485 ["td", { style: "padding-right: 1em;" }, _("title.Mode")],
486 ["td", { style: "padding-right: 1em;" }, _("title.Command")],
487 ["td", { style: "padding-right: 1em;" }, _("title.Action")]],
488 ["col", { style: "min-width: 6em; padding-right: 1em;" }],
489 hives.map(function ([hive, maps]) let (i = 0) [
490 ["tr", { style: "height: .5ex;" }],
491 maps.map(function (map)
492 map.names.map(function (name)
494 ["td", { highlight: "Title" }, !i++ ? hive.name : ""],
495 ["td", {}, modeSign],
497 ["td", {}, map.rhs || map.action.toSource()]])),
498 ["tr", { style: "height: .5ex;" }]])];
501 // // TODO: Move this to an ItemList to show this automatically
502 // if (list.*.length() === list.text().length() + 2)
503 // dactyl.echomsg(_("map.none"));
505 commandline.commandOutput(list);
509 contexts: function initContexts(dactyl, modules, window) {
510 update(Mappings.prototype, {
511 hives: contexts.Hives("mappings", MapHive),
512 user: contexts.hives.mappings.user,
513 builtin: contexts.hives.mappings.builtin
516 commands: function initCommands(dactyl, modules, window) {
517 function addMapCommands(ch, mapmodes, modeDescription) {
518 function map(args, noremap) {
519 let mapmodes = array.uniq(args["-modes"].map(findMode));
520 let hives = args.explicitOpts["-group"] ? [args["-group"]] : null;
523 mappings.list(mapmodes, null, hives);
527 if (args[1] && !/^<nop>$/i.test(args[1])
528 && !args["-count"] && !args["-ex"] && !args["-javascript"]
529 && mapmodes.every(function (m) m.count))
530 args[1] = "<count>" + args[1];
532 let [lhs, rhs] = args;
534 args["-builtin"] = true;
536 if (!rhs) // list the mapping
537 mappings.list(mapmodes, lhs, hives);
539 util.assert(args["-group"].modifiable,
540 _("map.builtinImmutable"));
542 args["-group"].add(mapmodes, [lhs],
543 args["-description"],
544 contexts.bindMacro(args, "-keys", function (params) params),
547 count: args["-count"] || !(args["-ex"] || args["-javascript"]),
548 noremap: args["-builtin"],
549 persist: !args["-nopersist"],
550 get rhs() String(this.action),
551 silent: args["-silent"]
558 completer: function (context, args) {
559 let mapmodes = array.uniq(args["-modes"].map(findMode));
560 if (args.length == 1)
561 return completion.userMapping(context, mapmodes, args["-group"]);
562 if (args.length == 2) {
563 if (args["-javascript"])
564 return completion.javascript(context);
566 return completion.ex(context);
573 names: ["-arg", "-a"],
574 description: "Accept an argument after the requisite key press"
577 names: ["-builtin", "-b"],
578 description: "Execute this mapping as if there were no user-defined mappings"
581 names: ["-count", "-c"],
582 description: "Accept a count before the requisite key press"
585 names: ["-description", "-desc", "-d"],
586 description: "A description of this mapping",
587 default: /*L*/"User-defined mapping",
588 type: CommandOption.STRING
591 names: ["-ex", "-e"],
592 description: "Execute this mapping as an Ex command rather than keys"
594 contexts.GroupFlag("mappings"),
596 names: ["-javascript", "-js", "-j"],
597 description: "Execute this mapping as JavaScript rather than keys"
599 update({}, modeFlag, {
600 names: ["-modes", "-mode", "-m"],
601 type: CommandOption.LIST,
602 description: "Create this mapping in the given modes",
603 default: mapmodes || ["n", "v"]
606 names: ["-nopersist", "-n"],
607 description: "Do not save this mapping to an auto-generated RC file"
610 names: ["-silent", "-s", "<silent>", "<Silent>"],
611 description: "Do not echo any generated keys to the command line"
614 serialize: function () {
615 return this.name != "map" ? [] :
616 array(mappings.userHives)
617 .filter(function (h) h.persist)
618 .map(function (hive) [
622 "-count": map.count ? null : undefined,
623 "-description": map.description,
624 "-group": hive.name == "user" ? undefined : hive.name,
625 "-modes": uniqueModes(map.modes),
626 "-silent": map.silent ? null : undefined
628 arguments: [map.names[0]],
632 for (map in userMappings(hive))
638 function userMappings(hive) {
640 for (let stack in values(hive.stacks))
641 for (let map in array.iterValues(stack))
642 if (!Set.add(seen, map.id))
646 modeDescription = modeDescription ? " in " + modeDescription + " mode" : "";
647 commands.add([ch ? ch + "m[ap]" : "map"],
648 "Map a key sequence" + modeDescription,
649 function (args) { map(args, false); },
652 commands.add([ch + "no[remap]"],
653 "Map a key sequence without remapping keys" + modeDescription,
654 function (args) { map(args, true); },
655 update({ deprecated: ":" + ch + "map -builtin" }, opts));
657 commands.add([ch + "unm[ap]"],
658 "Remove a mapping" + modeDescription,
660 util.assert(args["-group"].modifiable, _("map.builtinImmutable"));
662 util.assert(args.bang ^ !!args[0], _("error.argumentOrBang"));
664 let mapmodes = array.uniq(args["-modes"].map(findMode));
667 for (let mode in values(mapmodes))
669 args["-group"].clear(mode);
670 else if (args["-group"].has(mode, args[0])) {
671 args["-group"].remove(mode, args[0]);
675 if (!found && !args.bang)
676 dactyl.echoerr(_("map.noSuch", args[0]));
682 completer: opts.completer,
684 contexts.GroupFlag("mappings"),
685 update({}, modeFlag, {
686 names: ["-modes", "-mode", "-m"],
687 type: CommandOption.LIST,
688 description: "Remove mapping from the given modes",
689 default: mapmodes || ["n", "v"]
696 names: ["-mode", "-m"],
697 type: CommandOption.STRING,
698 validator: function (value) Array.concat(value).every(findMode),
699 completer: function () [[array.compact([mode.name.toLowerCase().replace(/_/g, "-"), mode.char]), mode.description]
700 for (mode in values(modes.all))
704 function findMode(name) {
706 for (let mode in values(modes.all))
707 if (name == mode || name == mode.char
708 || String.toLowerCase(name).replace(/-/g, "_") == mode.name.toLowerCase())
712 function uniqueModes(modes) {
713 let chars = [k for ([k, v] in Iterator(modules.modes.modeChars))
714 if (v.every(function (mode) modes.indexOf(mode) >= 0))];
715 return array.uniq(modes.filter(function (m) chars.indexOf(m.char) < 0)
716 .map(function (m) m.name.toLowerCase())
720 commands.add(["feedkeys", "fk"],
722 function (args) { events.feedkeys(args[0] || "", args.bang, false, findMode(args["-mode"])); },
728 update({}, modeFlag, {
729 description: "The mode in which to feed the keys"
734 addMapCommands("", [modes.NORMAL, modes.VISUAL], "");
736 for (let mode in modes.mainModes)
737 if (mode.char && !commands.get(mode.char + "map", true))
738 addMapCommands(mode.char,
739 [m.mask for (m in modes.mainModes) if (m.char == mode.char)],
743 getMode: function (args) findMode(args["-mode"]),
744 iterate: function (args, mainOnly) {
745 let modes = [this.getMode(args)];
747 modes = modes[0].allBases;
750 // Bloody hell. --Kris
751 for (let [i, mode] in Iterator(modes))
752 for (let hive in mappings.hives.iterValues())
753 for (let map in array.iterValues(hive.getStack(mode)))
754 for (let name in values(map.names))
755 if (!Set.add(seen, name)) {
759 i === 0 ? "" : ["span", { highlight: "Object", style: "padding-right: 1em;" },
761 hive == mappings.builtin ? "" : ["span", { highlight: "Object", style: "padding-right: 1em;" },
769 description: function (map) [
770 options.get("passkeys").has(map.name)
771 ? ["span", { highlight: "URLExtra" },
772 "(", template.linkifyHelp(_("option.passkeys.passedBy")), ")"]
774 template.linkifyHelp(map.description + (map.rhs ? ": " + map.rhs : ""))
776 help: function (map) let (char = array.compact(map.modes.map(function (m) m.char))[0])
777 char === "n" ? map.name : char ? char + "_" + map.name : "",
778 headings: ["Command", "Mode", "Group", "Description"]
782 dactyl.addUsageCommand({
784 name: ["listk[eys]", "lk"],
785 description: "List all mappings along with their short descriptions",
787 update({}, modeFlag, {
789 description: "The mode for which to list mappings"
794 iter.forEach(modes.mainModes, function (mode) {
795 if (mode.char && !commands.get(mode.char + "listkeys", true))
796 dactyl.addUsageCommand({
798 name: [mode.char + "listk[eys]", mode.char + "lk"],
799 iterateIndex: function (args)
800 let (self = this, prefix = /^[bCmn]$/.test(mode.char) ? "" : mode.char + "_",
801 haveTag = Set.has(help.tags))
802 ({ helpTag: prefix + map.name, __proto__: map }
803 for (map in self.iterate(args, true))
804 if (map.hive === mappings.builtin || haveTag(prefix + map.name))),
805 description: "List all " + mode.displayName + " mode mappings along with their short descriptions",
806 index: mode.char + "-map",
807 getMode: function (args) mode,
812 completion: function initCompletion(dactyl, modules, window) {
813 completion.userMapping = function userMapping(context, modes_, hive) {
814 hive = hive || mappings.user;
815 modes_ = modes_ || [modes.NORMAL];
816 context.keys = { text: function (m) m.names[0], description: function (m) m.description + ": " + m.action };
817 context.completions = hive.iterate(modes_);
820 javascript: function initJavascript(dactyl, modules, window) {
821 JavaScript.setCompleter([Mappings.prototype.get, MapHive.prototype.get],
824 function (context, obj, args) [[m.names, m.description] for (m in this.iterate(args[0]))]
827 mappings: function initMappings(dactyl, modules, window) {
828 mappings.add([modes.COMMAND],
829 ["\\"], "Emits <Leader> pseudo-key",
830 function () { events.feedkeys("<Leader>"); });
834 // vim: set fdm=marker sw=4 sts=4 ts=8 et: