Vim and Progressive Enhancement
by | | level: advancedMany of my favorite Vim plug-ins build on and improve existing Vim functionality rather than entirely reinventing it. Some plug-ins that provide this sort of progressive enhancement to Vim built-ins include speeddating.vim, which teaches <C-a> and <C-x> how to manipulate dates, endwise.vim, which automatically inserts “end” keywords (such as endif
after you insert if
), and targets.vim, which provides various enhancements to built-in text objects.
What qualifies these plug-ins as progressive enhancements (a term I’m borrowing from Web development) is that they build on commands that already exist, while adding new functionality or conveniences. When I find myself without these plug-ins, it isn’t particularly problematic to use the built-in equivalents. Likewise, if someone entirely unfamiliar with these plug-ins were to install them, s/he wouldn’t run into many surprises. Here are a few examples of small improvements to built-in commands from my vimrc that fit with this theme:
Obvious Resize
Starting with a fairly simple example, we have Obvious Resize, a plug-in that exposes commands for resizing the current window more intuitively (instead of expanding or contracting the window, you can conceptualize moving the border between splits). :ObviousResizeLeft
expands a right-hand split to the left or contracts a left-hand split to the left, and so on. Each command also takes a count argument that specifies the distance to resize, so :ObviousResizeUp 5
would move the border between two horizontal splits up by five rows.
The documentation recommends mapping its four commands (one for each direction) to the arrow keys chorded with the control key. We can do one better by creating maps that work even when the plug-in isn’t available, by mapping to an intermediate function:
nnoremap <silent> <C-Up> :<C-u>call <SID>try_wincmd('ObviousResizeUp', '+')<CR>
nnoremap <silent> <C-Down> :<C-u>call <SID>try_wincmd('ObviousResizeDown', '-')<CR>
nnoremap <silent> <C-Left> :<C-u>call <SID>try_wincmd('ObviousResizeLeft', '<')<CR>
nnoremap <silent> <C-Right> :<C-u>call <SID>try_wincmd('ObviousResizeRight', '>')<CR>
We’ll add support for counts as well, since the commands also accept a count of sorts (e.g., 3<C-Left>
should call :ObviousResizeLeft 3
). We also pass a fallback that can be used as an argument to :wincmd
in case the Obvious Resize command is not available.
function! s:try_wincmd(cmd, default)
if exists(':' . a:cmd)
let cmd = v:count ? join([a:cmd, v:count]) : a:cmd
execute cmd
else
execute join([v:count, 'wincmd', a:default])
endif
endfunction
The if
clause simply checks to see if the command we’re calling is defined. If it isn’t defined, the function falls back to the equivalent built-in window command. In circumstances where the plug-in isn’t guaranteed to be available, this can be really convenient. The next examples take this idea further.
Dash.vim
By default normal mode K
will look up the man page of the keyword under the cursor. Vim provides a built-in setting 'keywordprg'
for defining what program is used to look up documentation. For instance, the Vim file-type plug-in is configured to look up documentation via :help
. Unless I’m writing a shell script or a C program, I rarely want to invoke man
but instead want to look up the current keyword with dash.vim, which provides its own <Plug>
map. I’d like to piggyback on the built-in K
map in this case rather than creating a <Leader>
map as the documentation suggests. This customization is a little cleverer than the last one. Instead of just checking whether the plug-in is available, we’re going to make sure a different 'keywordprg'
hasn’t already been set.
We’re going to again start by mapping to an intermediate function, using expression mappings this time:
nmap <expr> K <SID>doc("\<Plug>DashSearch")
nmap <expr> <Leader>K <SID>doc("\<Plug>DashGlobalSearch")
If the :Dash
command is available (meaning the Dash plug-in has been loaded) and 'keywordprg'
is set to its default of man
, the s:doc()
function returns the <Plug>
map argument. Otherwise, we fall back to the built-in K
.
function! s:doc(cmd)
if &keywordprg ==# 'man' && exists(':Dash')
return a:cmd
endif
return 'K'
endfunction
This effectively sets a new default action for K
, while allowing it to be overridden through the usual mechanism.
SplitJoin.vim
SplitJoin.vim transforms single-line statements to multi-line ones and vice versa. It can be thought of as a smarter version of the built-in J
that makes language-specific affordances, so why not combine them? This example is cleverer yet than the previous two (credit goes to @tpope’s vimrc). The concept is this: when invoking J
, first try :SplitjoinJoin
, and if it has no effect, then fall back to built-in J
. “No effect” could mean that SplitJoin.vim isn’t installed, but more importantly it can mean that there is no language-specific support for “joining” the form under the cursor, in which case no change would be made to the buffer. We’ll also add a fallback for :SplitjoinSplit
that performs an unintelligent split at the cursor position via r<CR>
.
As usual, this requires calling an intermediate function:
nnoremap <silent> J :<C-u>call <SID>try('SplitjoinJoin', 'J')<CR>
nnoremap <silent> S :<C-u>call <SID>try('SplitjoinSplit', "r\015")<CR>
This time, we’ll check whether the command is available and whether a count was supplied. Since a count doesn’t make sense with intelligent splitting/joining, we defer to the built-ins in those cases. Otherwise, we try the SplitJoin command and check whether a change was made to the buffer by examining b:changedtick
(automatically updated by Vim).
function! s:try(cmd, default)
if exists(':' . a:cmd) && !v:count
let tick = b:changedtick
execute a:cmd
if tick == b:changedtick
execute join(['normal!', a:default])
endif
else
execute join(['normal! ', v:count, a:default], '')
endif
endfunction
When the given SplitJoin command doesn’t produce a change in the buffer (i.e., the condition tick == b:changedtick
is satisfied), a normal join or split is performed. The above pattern can be applied to all sorts of situations where a fallback behavior is desired.