Vim and Progressive Enhancement

by Noah Frederick | | level: advanced

Many 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.

Further Reading