On The Genealogy of Modality
What makes Vim so different from other editors is, arguably, its rich set of motion commands. Once you can move around with precision, you not only gain time, as you do not need to reach for the mouse, but you can also turn pretty much every modification into tiny “programs”, either to accomplish ad hoc tasks via recorded macros, or to address more general cases, thanks to commands, functions–and mappings.
A mapping is the binding of a key or a sequence of keys, called the left-hand
side (LHS), to a series of keystrokes, the right-hand side (RHS). The RHS is
similar to a macro recorded with q
; contrary to some other tools or
editors, mappings do not reach for some ‘core’ functions or commands, like
hypothetical next-word
or delete-char
, that the default bindings would just
call. In Vim, motions and modification commands are so central that it would not
make much sense to decouple them from their standard keys, since they are the
building blocks of what we could call the “Vim editing programming language”.
For instance, dw
is like a program statement that deletes chars from cursor to
the beginning of next word; it’s a piece of code every Vim user will understand,
like an ‘if’ statement in C or in another programming language. Thus, in Vim the
medium (the key) is definitely the message (the function to execute).
Obviously, since alphanumeric keys are used to write “editing programs”, there is
a need to separate “programming mode” where the user hits dw
to delete until next word,
from “insertion mode”, where the user just wants to type text and not execute
programs. That is why Vim is a modal editor, contrary to, say, Emacs. Once you
go the modal road, it makes sense to define extra modes for things like
visual selection or the command-line. Again, mappings make it possible to
automate tasks for each of these modes.
So, if Vim is programming, custom mappings are no less than its user-defined functions.
Maps. Very dangerous… You Go First
The general syntax to create mappings is:
{map-cmd} [modifiers] {lhs} {rhs}
{map-cmd}
is one of the several mapping-defining Ex commands we will examine
shortly. [modifiers]
is an optional list of modifiers. {lhs}
is the key
sequence that will trigger the mapping: if it contains a literal blank (space,
tab or linefeed), it must be escaped. {rhs}
is the key sequence that will be
triggered, as though the user typed these keys directly (for the most
part–there are a few differences).
Mad Maps Beyond Thousand Modes
Each Vim mode has its own mapping-defining commands:
Modes | Command |
---|---|
normal mode | nmap |
insert mode | imap |
visual and select mode | vmap |
visual mode only | xmap |
command-line mode | cmap |
operator-pending mode | omap |
terminal mode | tmap |
Most are self-explanatory. vmap
defines mappings both for visual mode and
select mode; if you do not know what select mode is, it is similar to
visual mode, but hitting a printable key like alphanumerics will replace the
visual selection with that key and switch to insert mode. Select mode is not
often used, so many Vim users do as though it did not exist and use vmap
to
define visual mode mappings. However, some plugins can take advantage of it, for
instance snippet plugins that expand shortcuts into text or code with parts that
the user may want to change, like some default variable name. Selecting the
variable in select mode enables the user to type the new name “over” the visual
selection, or to hit some control key (eg. <Tab>
) to go to the next
occurrence. Unfortunately, if the user made some mappings with vmap
, this can
interfere with that process, if there are mappings starting with a printable
character like <Space>
or ,
. Thus, it is best to use xmap
to define mappings
for visual mode (and smap
for select mode if you ever need it).
omap
is for operator-pending mode, ie. the mode where extra key(s) are
expected, typically to define the selection where the current command will
operate. This is the mode you get when you hit y
, c
or d
and Vim waits for
the user to define what will be yanked, changed or deleted. This command
enables the user to create custom text objects of sorts.
tmap
lets users define mappings for the new terminal mode of Vim,
available inside a buffer containing a terminal. This requires Vim 8 or the
backported patches.
Finally, map
is the original mapping command from vi, and in Vim it
simultaneously defines a mapping for normal, visual and operator-pending mode.
map!
(also from vi) defines a mapping for insert and command-line mode. The
mode-specific versions (nmap
, imap
etc.) are generally preferred in Vim.
GG no RE
Say you want to extend the <Return>
key in normal mode, so that in addition to
its standard function (going to the first non-blank of the next line), it also
temporarily turns off search highlighting. You come up with the following
command:
nmap <Return> :nohls<Return><Return>
It calls the :nohls
Ex command on the command-line, runs it, and then
finally sends a <Return>
to go to the next line. You execute this nmap
command, hit <Return>
, and… Vim seems to hang (hit <C-c>
to interrupt).
What gives?
The :nohls<Return>
part runs just fine, but the issue is the last <Return>
:
remember, the RHS of a mapping runs as though the user typed it. So, when you
hit <Return>
to run the mapping, the mapping will also “hit” <Return>
at the
end–and you get an infinite recursion!
nmap <Return> :nohls<Return><Return>
" ^ |
" `---[recursive call]---'
What you meant, of course, was to execute the core function of the unmapped
<Return>
, not to run the mapping again. In other words, you do not want
mappings to apply in the RHS. That is exactly what the noremap version
of mapping-defining commands do. Try:
nnoremap <Return> :nohls<Return><Return>
" ^ [Built-in <Return>]
Bingo, everything works as intended.
Each mapping-creating command has its noremap version:
Modes | Recursive command | Non-recursive command |
---|---|---|
normal mode | nmap |
nnoremap |
insert mode | imap |
inoremap |
visual and select mode | vmap |
vnoremap |
visual mode only | xmap |
xnoremap |
command-line mode | cmap |
cnoremap |
operator-pending mode | omap |
onoremap |
terminal mode | tmap |
tnoremap |
As a rule of thumb, use the noremap versions unless you actually need to run mappings in the RHS.
Modifiers: The Bestiary
A few modifiers are available to tweak the created mapping; they all have the
form <modifier>
, between angle brackets. Here are the main ones; consult the
documentation for the whole list.
Silence, RHSling
<silent>
is one of the most used modifiers. As its name suggests, it
turns off echo area visual feedback for the duration of the mapping execution.
This is especially useful if you run Ex commands in the RHS that you do not want
to expose to the user. For instance:
xnoremap <silent> p p:if v:register == '"'<Bar>let @@=@0<Bar>endif<cr>
This extends p
in visual mode (note the noremap), so that if you paste from
the unnamed (ie. default) register, that register content is not replaced by
the visual selection you just pasted over–which is the default behavior. This
enables the user to yank some text and paste it over several places in a row,
without using a named register (eg. "ay
, "ap
etc.).
In the previous mapping, it would be annoying to see the :if v:register...
command-line each time you pasted in visual mode; thus, the <silent>
modifier
was added, and nothing is displayed when the mapping runs. As for the rest of
the mapping, the v:register
special variable contains the name of the
register, if any, that was specified for the current mapping when the user typed
something like "ap
. The statement let @@=@0
re-assigns the value of the @0
register, which contains the last yanked text, to the unnamed register (@@
)
once the paste is done. <Bar>
stands for the |
character that separates Ex
commands on the command-line, and <cr>
is another way to get a carriage
return, along with <Enter>
or <Return>
.
Don’t Map For Me Next Door Neighbor
<buffer>
makes the new mapping buffer-local, ie. it will be defined only
for the buffer that was current when the mapping was created. It is useful in
filetype-specific settings, eg. in /.vim/after/ftplugin/help.vim
:
nnoremap <silent><buffer> zl
\ :call search('<Bar>[^ <Bar>]\+<Bar>\<Bar>''[A-Za-z0-9_-]\{2,}''')<cr>
A buffer-local mapping on zl
is created on buffers with the help
filetype,
ie. help buffers created with :help
. The mapping jumps to the next tag in the
current buffer. Obviously, this only makes sense in help buffers, so we should
not make this mapping global. Again, note the <silent>
modifier, since we
do not want to show this RHS on the command-line each time we use the mapping.
Here is the backward-jumping version of the mapping:
nnoremap <silent><buffer> zh
\ :call search('<Bar>[^ <Bar>]\+<Bar>\<Bar>''[A-Za-z0-9_-]\{2,}''','b')<cr>
Unique: Is This Seat Taken?
<unique>
prevents the clobbering of an existing mapping on the same LHS.
This can be useful for plugin authors, who want to offer default mappings but
are still careful not to override the users’ own mappings:
nnoremap <unique><silent> <LocalLeader>a :call ThisPluginFunction()<cr>
If there already is a mapping on <LocalLeader>a
, then the :nnoremap
above
will fail (the error can be silenced with silent! nnoremap <unique> ...
).
<LocalLeader>
is expanded to some user-defined key, and is typically used for
buffer-local settings.
Expr-esso: What Else To Eval
<expr>
changes the meaning of the RHS: it is no longer a sequence of
keys to run like a macro, but an expression that will be evaluated each time
the mapping is triggered, and the result of that evaluation will be a string
containing the key sequence to run. So this is a level of indirection to make
things more dynamic; typically, it contains some conditional to either run one
sequence or the other. Here is an example:
inoremap <expr> jk pumvisible() ? "<C-e>" : "<Esc>"
This is the “classic” jk
to exit insert mode, with a twist: if the pop-up menu
is visible (this is the menu showing completion candidates), then it will close
it with <C-e>
instead of exiting insert mode with <Esc>
. The whole RHS
is a single expression, here, a ternary conditional, and when you hit jk
, the
expression is evaluated. The pumvisible()
function is called, returning
true or false depending on whether the pop-up menu is visible, and if it is then
the expression evaluates to <C-e>
, otherwise it evaluates to <Esc>
. The
result of that evaluation becomes the final RHS.
Here is another example:
onoremap <expr> il ':<C-u>norm! `['.strpart(getregtype(), 0, 1).'`]<cr>'
This is an operator-pending mapping, that selects the last piece of changed text
(most often, some pasted text), in the same visual mode (character, line or block) as
that of the used register. It can be used to indent some pasted lines, with
>il
. The <expr>
modifier is necessary to evaluate the strpart(...)
part of
the RHS; it is then concatenated to the leading and trailing literal strings, to
form the final RHS.
As for the rest of the mapping: the <C-u>
clears the command-line, since in
some circumstances Vim can fill it automatically with a line range, for instance
if :
is hit from visual mode, or if a count is given to it (either directly,
or from a mapping, eg. if we hit something like 3il
). norm
will
execute normal mode commands, and the !
says not to use mappings in those,
like noremap
. The [
and ]
marks are automatically set on both ends
of the last changed text, so the first backtick goes to one end, then the right
visual mode is set by the strpart(...)
part, and the second backtick goes to
the other end of the changed text. Making a visual selection from an
operator-pending mapping defines the object of the current command, eg. y
, c
or d
.
Escaping and Notation
Better Get Used To These Bars, Kid
Mapping-defining commands are standard Ex commands, so they can be separated by
the |
character. What do you think the following command will do?
nnoremap <F7> :echo "foo"|echo "bar"
This command actually contains two parts: first, the nnoremap
Ex command, then
the echo "bar"
one. So, it will map <F7>
to :echo "foo"
, then it will
run echo "bar"
just once, when the <F7>
mapping is defined. This is probably
not what the user intended!
In order to map <F7>
to echo "foo"|echo "bar"
, the |
character must be
escaped, either with \|
or with the Vim notation <Bar>
(in five characters).
The following two commands do the same thing:
" same thing
nnoremap <F7> :echo "foo"\|echo "bar"
nnoremap <F7> :echo "foo"<Bar>echo "bar"
Pick your favorite method. Also, do not forget to add a trailing <cr>
to those
mappings if you want to run the command-line when calling the mappings;
otherwise, they will stay there on the command-line, for the user to modify, run
or cancel.
We Don’t Need No True Control
You can use literal control chars in your mappings, by hitting <C-v>
followed by the control character in insert or command-line mode. For instance,
pressing <C-v>
then the tab key will insert a literal tab character (you can use
:set list
to show them). However, it is not very convenient to work with
literal control chars: the graphic representation can be confusing (eg. ^[
for
the Escape key), and it can insert a terminal-dependent sequence, like ^[OP
,
instead of the generic character (here, the F1 key).
The best practice is to use the Vim notation like we did so far, eg. <C-x>
, in
five chars (<
+ C
+ -
+ x
+ >
), for Control+x. All keys have a
notation, eg. <Tab>
, <Return>
or <Esc>
; check out the documentation
for the complete list.
Vim will expand Vim notation in mappings commands as long as the <
flag does
not appear in 'cpoptions'
(it does not by default). You can also include
the Vim notation in strings by prepending the notation with a backslash, eg.
"\<Tab>"
is a string that will contain a single literal tab when the code is
evaluated.
And All The Keys That Lead You There Were Mappings
As a convenience, Vim provides two customizable Vim notation expansions that you
can use in your mappings: <Leader>
and <LocalLeader>
. You can
set their value via the mapleader
and maplocalleader
global variables, eg.:
let mapleader = "\<Space>"
let maplocalleader = "&"
You can then use it like this:
nnoremap <silent> <Leader>w :w<cr>
Now hitting <Space>w
will save the current buffer.
It is called “leader” as it is generally the first key of a group of mappings. A common setting is to undefine the original behavior of that key, so that it does not interfer or trigger inadvertently:
nnoremap <Space> <Nop>
Leader and LocalLeader let the user change several mappings at one fell swoop
just by changing the value of the mapleader
and maplocalleader
variables,
but this is not something that occurs very often. Also, note that the mechanism
is a mere convenience: <Leader>
is expanded in mapping commands, but not
elsewhere. This does NOT echo a space:
" No expansion: echoes `<Leader>` in eight chars
:echo "\<Leader>"
And if you show a mapping containing Leader, you will see its expansion (the
<Space>
character):
:nmap <Leader>w
So all in all, there is not a whole lot to gain with Leader and LocalLeader, but they at least show intent, and it can make it easier to search through your Vim files for mappings.
Come Out To Part II, We’ll Get Together, Have A Few Laughs
In this first part, we went through a whirlwind tour of the mappings concepts and syntax; hopefully this should get you started if you never wrote mappings before, or it might have given you some ideas for your future mappings.
In part two, we will present a few mapping-related tools and tricks to help you
unleash the madman<C-w>
power user in you.
That’s all for now, vimfolks!