While there are many "tutorials" for writing plugins in Vim, I hope this one
will be a little bit different from what is out there, because it won't be
about writing plugin per se. If you want to find information about that then
you should check out
:h write-plugin. I want this article to be about how
plugins come to life, using my own experience on writing
vim-backscratch as an example.
All plugins should start with a problem. If there is no problem, then there should be no code, as there is no better code than no code. In this case, my problem was pretty trivial: I wanted a temporary buffer that would let me perform quick edits and view SQL queries while optimising them (and run them from there with Tim Pope's dadbod).
"Simple obvious solution"§
Now that we have defined the problem, we need to try the first possible solution. In our case, it is opening a new buffer in a new window, edit it, and then close it when no longer needed. It is simple in Vim:
:new " Edits :bd!
Unfortunately this has bunch of problems:
- if we forgot to close that buffer, then it will hang there indefinitely,
:bd!in the wrong buffer, can have unpleasant consequences,
- this buffer is still listed in
:ls, which is unneeded (as it is only temporary).
Systematic solution in Vim§
Fortunately Vim has solutions for all of our problems:
- the "scratch" section in
:h special-buffers, which solves the first two problems,
:h unlisted-buffer, which solves the third problem.
So now our solution looks like:
:new :setlocal nobuflisted buftype=nofile bufhidden=delete noswapfile " Edits :bd
However that is a long chain of commands to write. Of course we could condense the first two into a single one:
:new ++nobuflisted ++buftype=nofile ++bufhidden=delete ++noswapfile
But in reality this does not shorten anything.
Create a command§
Fortunately we can create our own commands in Vim, so we can shorten that to a single, easy to remember command:
command! Scratch new ++nobuflisted ++buftype=nofile ++bufhidden=delete ++noswap
For better flexibility, I prefer it to be:
command! Scratchify setlocal nobuflisted buftype=nofile bufhidden=delete noswap command! Scratch new +Scratchify
We can also add a bunch of new commands to give us better control over our new window's location:
command! VScratch vnew +Scratchify command! TScratch tabnew +Scratchify
Those commands will open a new scratch buffer in a new vertical window, and a new scratch buffer in a new tab page, respectively.
Make it a more "vimmy" citizen§
While our commands
:TScratch are nice, they are
still not flexible enough. In Vim we can use modifiers like
:h :aboveleft to define exactly where we want new windows to appear
and our current commands do not respect that. To fix this problem, we can
simply squash all the commands into one:
command! Scratch <mods>new +Scratchify
And we can remove
:TScratch as these can be now done via
:vert Scratch and
:tab Scratch (of course you can keep them if you like, I
just wanted the UX to be minimal).
Make it powerful§
This has been in my
$MYVIMRC for some time in the form described above until
I found out Romain Lafourcade's snippet that provided one additional
feature: it allowed to open a scratch buffer with the output of a Vim or shell
command. My first thought was - hey, I know that, but I know I can make it
better! So we can write a simple VimL function (which is mostly copied from
romainl's snippet, with a few improvements):
function! s:scratch(mods, cmd) abort if a:cmd is# '' let l:output =  elseif a:cmd is# '!' let l:cmd = a:cmd =~' %' ? substitute(a:cmd, ' %', ' ' . expand('%:p'), '') : a:cmd let l:output = systemlist(matchstr(l:cmd, '^!\zs.*')) else let l:output = split(execute(a:cmd), "\n") endif execute a:mods . ' new' Scratchify call setline(1, l:output) endfunction command! Scratchify setlocal nobuflisted noswapfile buftype=nofile bufhidden=delete command! -nargs=1 -complete=command Scratch call <SID>scratch('<mods>', <q-args>)
The main differences are:
- special case for empty command, it will just open an empty buffer,
- use of
- use of
:h execute()instead of
As it is quite self-contained and (let's be honest) too specific for
we can can extract it to its own location in
plugin/scratch.vim, but to do so properly we need
one additional thing, a command to prevent the script from being loaded twice:
if exists('g:loaded_scratch') finish endif let g:loaded_scratch = 1 function! s:scratch(mods, cmd) abort if a:cmd is# '' let l:output =  elseif a:cmd is# '!' let l:cmd = a:cmd =~' %' ? substitute(a:cmd, ' %', ' ' . expand('%:p'), '') : a:cmd let l:output = systemlist(matchstr(l:cmd, '^!\zs.*')) else let l:output = split(execute(a:cmd), "\n") endif execute a:mods . ' new' Scratchify call setline(1, l:output) endfunction command! Scratchify setlocal nobuflisted noswapfile buftype=nofile bufhidden=delete command! -nargs=1 -complete=command Scratch call <SID>scratch(<q-mods>, <q-args>)
To boldly go…§
Now my idea was, hey, I use Vim macros from time to time, and these are just
simple lists of keystrokes stored in Vim registers. Maybe it would be nice to have
access to that as well in our command. So we will just add a new condition that
a:cmd begins with the
@ sign and has a length of two. If so, then
l:output to the spliced content of the register:
function! s:scratch(mods, cmd) abort if a:cmd is# '' let l:output = '' elseif a:cmd is# '@' if strlen(a:cmd) is# 2 let l:output = getreg(a:cmd, 1, v:true) else throw 'Invalid register' endif elseif a:cmd is# '!' let l:cmd = a:cmd =~' %' ? substitute(a:cmd, ' %', ' ' . expand('%:p'), '') : a:cmd let l:output = systemlist(matchstr(l:cmd, '^!\zs.*')) else let l:output = split(execute(a:cmd), "\n") endif execute a:mods . ' new' Scratchify call setline(1, l:output) endfunction
This gives us a pretty powerful solution where we can use
:Scratch @a to open
a new scratch buffer with the content of register
A, edit it, and yank it
Now, it would be a shame to keep such a useful tool for ourselves so let's share it with the big world. In this case we need:
- a proper project structure,
- a good catchy name.
Finding a good name is something I can't help you with. I have picked
vim-backscratch, because I like back scratches (everyone likes them) and, as
a nice coincidence, because it contains the word "scratch".
Creating plugins for Vim is easy, but not every functionality needs to be a plugin from day one. Start easy and small. If something can be done with a simple command/mapping, then it should be done with a simple command/mapping at first. If you find your solution really useful, then, and only then, you should think about turning it into a plugin. The whole process described in this article didn't happen in a week or two. It took me about a year to reach the step Make it a more "vimmy" citizen, when I heard about romainl's script on IRC. I didn't need anything more, so take your time.
- make it small, big plugins will require a lot of maintenance, small plugins are much simpler to maintain,
- if something can be done via a command then it should be made as a command, do not force your mappings on users.