Bespin Plugin Guide
Keymapping
Introduction
The keyboard mapping mechanism built into Bespin is designed to be extensible in a declarative way. This design allows a wide variety of keyboard mappings to be implemented without writing any code.
Quick Start
Whenever a key combination like Cmd+C or a new character like c
is detected
in the editor, Bespin's keyboard manager component searches through the
keybinding rules, selects the first binding that matches, and executes the
associated command. Keystrokes not handled in this way are inserted as ordinary
text.
In the simplest case, a keymapping plugin looks something like this:
"define metadata"; ({ "provides": [ { "ep": "command", "name": "alert", "key": "ctrl_i", "pointer": "#showMessage" } ] }); "end"; exports.showMessage = function() { alert("Greetings from the cloud!"); };
When this plugin is loaded and the user presses Ctrl+I on the keyboard, then
the command showMessage
is executed.
For most use cases, this is all that is needed. For those interested in adding more advanced keybindings such as those used in the vi and Emacs plugins, read on.
How Key Detection Works
Before diving into more complex use cases, an understanding of the way key commands work in Bespin is needed.
Whenever a key is pressed while a modifier key (Meta, Cmd, Ctrl, or Alt) is held down, the Bespin framework handles the browser's key event, performs translation, which most notably involves the creation of a symbolic name, and forwards the event to the Bespin keyboard manager. Some examples of symbolic names are:
Ctrl+A -> ctrl_a Alt+C -> alt_c Meta+Shift+Z -> meta_shift_z or ctrl_shift_z
The symbolic names are used to match against the "key" property above or the "regex" property (demonstrated later).
Keys pressed without a modifier key (as well as input events from other sources, such as IMEs) are also forwarded to the keyboard manager. The symbolic name for such events are simply equal to the text that would be inserted for each event.
NB: To aid the creation of cross-platform keybindings, by default the Meta
key is treated as though it were the Ctrl key. To avoid this and match the
Meta key explicitly, set the property handleMetaKey
on the keybinding to
true.
A simple vim-style keymapping
A simple vim-style modal keymapping looks like this:
"define metadata"; ({ "dependencies": { "canon": "0.0" }, "provides": [ { "ep": "keymapping", "handleMetaKey": false, "states": { "start": [ { "key": "i", "then": "insertMode" } ], "insertMode": [ { "key": "escape", "then": "start" } ] } } ] }); "end";
Keymappings support multiple states, which correspond to the modes of modal
keymappings such as vi. The start state is always called start
. Mode
transitions occur via the then
, property. In the example, pressing I
triggers a switch to the state insertMode
, which corresponds to vi's
insert mode. Pressing Esc in insert mode triggers a switch back to the start
state, corresponding to vi's normal mode.
The value of the key
property is treated as a regex. So, for example, you
could match either the K key or the up arrow key with one binding:
{ "key": "(k|up)", "exec": "move up", },
Remember that the text which this regex is matched against is the symbolic name of the key. So this binding will not match Ctrl+Up, Meta+Up, etc.
Buffering
More complex keymappings such as vi and Emacs typically feature commands that consist of multiple keystrokes. To support these, the keyboard manager stores all key events that have not yet mapped to a key binding in a keyboard buffer. After a binding matches, the buffer is cleared.
The buffer is simply a string of symbolic names, so for example the sequence of
keys d, 2, d maps to the string "d2d"
, and the sequence Ctrl+A maps to
the string "ctrl_a"
.
NB: On some international keyboards, the Alt key is used to insert special
characters. For instance, on a German keyboard, Alt+8 inserts the {
character. The keyboard manager is designed to detect this situation and, in
this case, will report the symbolic name "{"
, not "alt_8"
.
To access the characters stored in the buffer, the regex
property rather than
the key
property must be used:
{ "regex": "dd", "exec": "deleteLines", },
The supplied regex is anchored to the end of the buffer; for example, the value
of the regex
property in this example corresponds to the regex /dd$/
(matched against the contents of the buffer).
Command arguments
It's possible to use the regex
property to extract arguments that are passed
to commands:
{ "regex": "([0-9]*)j", "exec": "vim moveDown", "params": [ { "name": "n", "match": 1, "type": "number", "defaultValue": 1 } ] },
Let the keyboard buffer be "10j", then the regex
will match. The part of
the RegExp to match the number - ([0-9]*) - is grouped. Grouped parts of a RegExp
can be used as arguments. The arguments/params to pass to the command are defined
within the params
section. name
specifies the name of the argument. type
can be "number" or "text". match
means the match/group of the RegExp to use
for this argument. In this example the matched numbers are used as
argument for "n", where "n" means how many lines to move down. To access the
match of the first group, match
has to be 1. Counting starts at 1 and not 0
as this is parallel to how you access the groups in normal JS-RegExp-Exec-Results
where 0 represents the entire part of the matched text.
If there is no match, then the defaultValue
will be used.
Using predicates
In the same way you use predicates for commands you can use them for bindings:
{ "regex": "([0-9]*)j", "exec": "vim moveDown", "params": [ { "name": "n", "match": 1, "type": "number", "defaultValue": 1 } ], "predicates": { "isCommandKey": false } },
The predicate isCommandKey is set by the KeyboardManager. If the symbolic name
is a combination of a command key (CTRL/ALT/META) + a key, then isCommandKey
will be true, otherwise it's false.
Match the symbolic name and the buffer
So far, we can match either the keyboard buffer or the symbolic name, but not both. If you want to detect the "return" key and use former typed numbers as argument, you can write:
{ "regex": [ "([0-9]*)", "(j|down|return)" ], "exec": "vim moveDown", "params": [ { "name": "n", "match": 1, "type": "number", "defaultValue": 1 } ] },
The regex
property is an array now. Internal, this is converted to
regex = [ "([0-9]*)", "(j|down|return)" ]; key = new RegExp("^" + regex[1] + "$"); regex = new RegExp(regex.join('') + "$");
which means that the second element of the regex
array has to match the symbolic
name and the items in the array combined have to match the keyboard buffer.
This way, you can make sure that the user has not typed "1", "r", "e", "t", "u", "r", "n"
as individual characters which then matches the binding. The combination
of of a key
and regex
property is not allowed.
Disallow matches
Disallow matches is a way to tell the KeyboardManager to not use a binding if the
regex
property matches certain groups.
{ "regex": [ "(meta_[0-9]*)*", "([0-9]+)" ], "disallowMatches": [ 1 ], "exec": "insertText", "params": [ { "name": "text", "match": 2, "type": "text" } ], "predicates": { "isCommandKey": false } },
Let's say the keyboard buffer looks like "meta_12". In this case, the RegExp
will match the buffer and the first matched group will be "meta_1". But as we
specified in the disallowMatches
property that the first group is not allowed
to match, this binding doesn't fit.
Further reading
To get more familiar with keymappings in Bespin, take a look at the files
plugins/samples/vim.js
and plugins/samples/emacs.js
for starter
implementations of the vim and Emacs keybindings, respectively.