Time For More Cookin’
In first part, we went through the basics of mappings, and how they fit into the big picture of Vim design and use.
We are now going to see a few common recipes to write slightly more advanced mappings.
Follow The Yellow Power Cord
In a nutshell, <Plug>
is a Vim notation for some special key sequence
that the user cannot type. What use is it?
Imagine you are a plugin author and you have a complicated mapping whose LHS must be customizable by the user. The first option is to instruct them in the documentation to copy that complicated mapping in their vimrc and just change the LHS to their liking. This will work, but the user will have to deal with the internals of your plugin, which is not ideal. And if you want to change the RHS in a later version, your users will have to update their own version of the mapping.
The second option is to make an indirection: create a mapping to your internal RHS from a simple, intermediate LHS, and expose that LHS in your documentation. The user will then be able to map his own LHS to your simple LHS, and your implementation details will not be exposed. An example will make this clear:
" ]&MyPluginIndentLine is an intermediate, "pivot" LHS/RHS
nnoremap <silent> ]&MyPluginIndentLine ...internal RHS...
nmap <silent> <LocalLeader>f ]&MyPluginIndentLine
Now, if the user want to change the default mapping from <LocalLeader>f
to
something else, say, <Leader><Tab>
, they will just need to add this in their
vimrc:
nmap <Leader><Tab> ]&MyPluginIndentLine
Then, if you later modify the internal RHS part, your users will not have to
change anything. Note that all mappings except the one to the internal RHS are
not of the noremap family, since we do want all mappings to chain together;
:nnoremap
in any of those would break the chain.
This setting would work, but there is a flaw: the intermediate LHS, namely
]&MyPluginIndentLine
, interfers with normal usage. We paid attention to use a
leading sequence of ]&
that is not mapped by default, but the user might well
have created a mapping on that very sequence–and now, each time they will hit
]&
, there will be a slight delay while Vim waits for the duration of the
timeout to see if it should run ]&
or ]&MyPluginIndentLine
.
That is where <Plug>
comes in handy: since it is not a sequence made from
normal keys, it gets totally out of the way of other mappings that do not use
<Plug>
themselves. To use it, just replace the arbitrary ]&
prefix above
with <Plug>
:
nnoremap <silent> <Plug>MyPluginIndentLine ...internal RHS...
nmap <silent> <LocalLeader>f <Plug>MyPluginIndentLine
And in the user’s vimrc:
nmap <Leader><Tab> <Plug>MyPluginIndentLine
Note that the RHS comprises, firstly, the expansion of <Plug>
(we do not need
to know what it is exactly, though you can get an idea with :echo "\<Plug>"
),
followed by all the individual letters of “MyPluginIndentLine”. This is not some
special command-line or function-invoking mode! Therefore, conflicts could
theoretically arise. Suppose we write a snippet plugin with these two mappings:
nnoremap <silent> <Plug>MyPluginFor ...internal RHS 1...
nnoremap <silent> <Plug>MyPluginFori ...internal RHS 2...
The first mapping might insert some for-loop snippet, and the second one could insert a for-loop variant that uses a variable called ‘i’.
So far so good, but now a user finds it convenient to go into insert mode right after inserting the first for-loop variant, and they want to make a mapping for it:
" Intent: call <Plug>MyPluginFor and hit 'i' to go into insert mode
nmap <Leader>i <Plug>MyPluginFori
You see the problem: the mapping will inadvertently call the wrong mapping from our snippet plugin!
Even though those cases are rare, it is common practice to avoid them altogether
by surrounding the part after <Plug>
in braces:
nmap <silent> <LocalLeader>f <Plug>(MyPluginFor)
nmap <Leader><Tab> <Plug>(MyPluginFori)
In user’s vimrc:
" Intent: call <Plug>(MyPluginFor) and hit 'i' to go into insert mode
nmap <Leader>i <Plug>(MyPluginFor)i
Problem solved.
Never Give Up Control-R – W.W.
<C-r>
inserts the content of a register from insert and command-line
mode. It is notably useful in visual mode, when the mapping needs to work with
the selection eg.:
xnoremap <silent> <Leader>gf y:pedit <C-r><C-r>"<cr>
This will open the filename expected in the visual selection into the preview
window. Doubling the <C-r>
inserts the content literally, in case there were
some control characters in the filename that might be interpreted by Vim.
The expression register can also be used, opening some interesting possibilities:
inoremap <C-g><C-t> [<C-r>=strftime("%d/%b/%y %H:%M")<cr>]
That mapping will insert the current date and time between brackets.
Another example, from command-line mode:
cnoremap <C-x>_ <C-r>=split(histget('cmd', -1))[-1]<cr>
This will insert the last space-separated word from the last command-line, as
<M-_>
in Bash.
<C-r>
can also insert text present under the current cursor position, when
followed by some control characters. Here is an example with <C-r><C-f>
, which
inserts the filename under the cursor:
nnoremap <silent> <Leader>gf :pedit <C-r><C-f><cr>
This is the normal mode version of the preview mapping we saw above (the
filename recognition will depend on the 'isfname'
option). Note that on
the command-line, for a command where a filename is expected (like :e
), you
can also use a few special Vim notations to similar effects, eg. <cfile>
will
insert on the command-line the filename under the cursor, and <cword>
will
insert the current word. If a filename is not expected, you can always use
expand()
like this:
nnoremap <silent> c<Tab> :let @/=expand('<cword>')<cr>cgn
This mapping sets the last search pattern to the word under the cursor, and
changes it with the cgn
sequence–making the whole thing conveniently
repeatable with .
to apply the same replacement to some following occurrences.
The Mushroom Register: @=
The @
key executes the content of a register, and once again the
expression register offers a good deal of flexibility. As an example, consider
the <C-a>
normal mode command, that increases the number under the
cursor or the closest number on its right, on the same line, if any. A common
annoyance is words like file-1.txt
: hitting <C-a>
will turn it to
file0.txt
, to the surprise of many users, as Vim assumes the next number is
‘-1’, not ‘1’. Let’s write a mapping to change this behavior.
function! Increment() abort
call search('\d\@<!\d\+\%#\d', 'b')
call search('\d', 'c')
norm! v
call search('\d\+', 'ce')
exe "norm!" "\<C-a>"
return ''
endfun
The Increment()
function finds the sequence of digits under the cursor or
following it, then selects it in visual mode, and finally runs <C-a>
on it.
The visual mode version of <C-a>
is a relatively recent addition, so Vim 8 or
a late Vim 7 version is required. Now, let’s remap the normal mode <C-a>
to
our function:
nnoremap <silent> <C-a> @=Increment()<cr>
The effect is to execute the Increment()
function in the expression register,
which as we saw increases the next number ignoring leading minuses and returns
the empty string–leaving nothing to do for the @
command, since the job is
already done.
At first glance, this might just look like a fancy alternative to :call
Increment()<cr>
. There is a nice bonus to it, though: our mapping now accepts a
count, so that we can type 3<C-a>
to add three to the next number. This is
not something we could do with the :call
version, at least not without adding
more code to deal with the count.
Feeding Frenzy
The built-in feedkeys()
function inserts keys into the internal Vim
buffer containing all keys left to execute, either typed by the user or coming
from mappings. This can sound somewhat low-level, but it is a very useful tool.
nnoremap <silent> <C-g> :call feedkeys(nr2char(getchar()),'nt')<cr>
This mapping waits for a key after hitting <C-g>
and executes it, ignoring any
mapping for that key – a kind of “just-once-noremap”. getchar()
is first
executed: it waits for the user to hit a key, and returns its keycode.
nr2char()
converts that keycode into a character, and feedkeys()
puts that
key into the Vim internal buffer; the ‘nt’ options says not to use mappings, and
to process the key as though the user typed it. Even though it remaps the useful
<C-g>
built-in, it instantly makes it available again on <C-g><C-g>
.
Here’s a longer example (inspired from igemnace on #vim):
function! QuickBuffer(pattern) abort
if empty(a:pattern)
call feedkeys(":B \<C-d>")
return
elseif a:pattern is '*'
call feedkeys(":ls!\<cr>:B ")
return
elseif a:pattern =~ '^\d\+$'
execute 'buffer' a:pattern
return
endif
let l:globbed = '*' . join(split(a:pattern, ' '), '*') . '*'
try
execute 'buffer' l:globbed
catch
call feedkeys(':B ' . l:globbed . "\<C-d>\<C-u>B " . a:pattern)
endtry
endfun
command! -nargs=* -complete=buffer B call QuickBuffer(<q-args>)
nnoremap <Leader>b :B<cr>
Hitting <Leader>b
will run the user-defined Ex command B
, which will in turn
call the QuickBuffer()
function. When the latter is called without argument,
it will run feedkeys(":B \<C-d>")
, with the effect of listing the completion
options of the B
command – that is, showing the list of buffers, thanks to
the -complete=buffer
option of B
. The :B
is still on the command-line, so
now the user can pick its choice by entering a part of the wanted buffer
filename. All the conditionals of the QuickBuffer()
function will be skipped,
and the buffer
Ex command inside the try block will be run on the
argument with leading and trailing wildcards automatically added. If there is a
single match, the buffer will be displayed and the function ends. If there is no
match or more than one match, the choices will be shown and the :B
will be put
back on the command-line (in the ‘catch’ block).
The first elseif
allows for :B *
to show a full :ls!
listing, with hidden
buffers. The second elseif
lets the user select a buffer by number, eg.
:B 2
, skipping all wildcards addition.
Lazy And Gentlemen, Let’s Jump To The Conclusion
While a few mappings into your vimrc are quick to process, a larger amount of
them can take its toll on the overall startup time. Quite often, a group of
related mappings share a common prefix, eg. <Leader>x
; these mappings can deal
with some specific task, tool or plugin – something that you might not use
every time you run Vim. In other words, they stand out as prime candidates for
lazy loading, and that is what we will do in this final example.
vim-flattery is a plugin of mine
(shameless <Plug>
!) that overrides the f
key so as to provide new targets on
the alpha characters: for instance, fu
will jump to the next uppercase letter
on the current line, instead of jumping to the next ‘u’ letter. Not all letters
are overridden though, and the user can also choose which ones they want; for
the others, the key falls back to the default f
built-in.
The design choice was to create a <Plug>
mapping for each new target provided
by the plugin. This makes things easy to customize for the user, but it also
means creating quite a few mappings, all duplicated for f
and t
.
Lazy-loading them could definitely save some time during startup.
The initialization goes like this:
" in plugin/flattery.vim
if get(g:, 'flattery_autoload', 1)
for op in [s:flattery_f_map, s:flattery_t_map]
for cmd in ['nm', 'xm', 'om']
exe cmd '<silent><expr>' op
\ 'FlatteryLoad("'.op.'")'
exe cmd '<silent>' '<Plug>(flattery)'.op
\ op
endfor
endfor
else
call flattery#SetPlugMaps()
call flattery#SetUserMaps()
endif
If the g:flattery_autoload
variable is true or does not exist, this code will
create a mapping on s:flattery_f_map
and s:flattery_t_map
(script-local
variables containing "f"
and "t"
by default) to some FlatteryLoad()
function. This is similar to this:
nmap <silent><expr> f FlatteryLoad("f")
nmap <silent><expr> t FlatteryLoad("t")
nmap <silent> <Plug>(flattery)f f
nmap <silent> <Plug>(flattery)t t
This is done for normal, visual and operator-pending mode. The <Plug>
mappings
make it possible for the user to map them to what they want without setting
variables, and still benefit from lazy loading if needed.
Here is the FlatteryLoad()
function:
" in plugin/flattery.vim
function! FlatteryLoad(o) abort
call flattery#SetPlugMaps()
call flattery#SetUserMaps()
for op in [s:flattery_f_map, s:flattery_t_map]
for cmd in ['nun', 'xu', 'ou']
exe cmd op
endfor
endfor
return "\<Plug>(flattery)".a:o
endfun
It calls the autoloaded flattery#SetPlugMaps()
and flattery#SetUserMaps()
functions, which sets all the plugin mappings starting with f
and t
eg.
fa
, fb
, fu
etc. Then, it unmaps the initial “lazy-loader” mappings (those
who called this very function) for all modes, as the loading has just been done.
Finally, it returns a string containing a <Plug>
mapping that will be
processed as an RHS, since the mapping that called the FlatteryLoad()
function
had the <expr>
modifier. Consequently, the intended mapping will be executed.
With some effort, that mechanism can be made generic, and it can also load plugins on demand, for instance with the Vim 8 package management. That is how my current setting works, and it might be the topic of a following article.
Until then, merry xmaps to all!