Ergonomic mappings for code formatting in Vim - Phelipe Teles

Ergonomic mappings for code formatting in Vim

3 min.
Source code

Vim allows you to format text with an arbitrary external program with the :h gq operator. For example, say you want to format with prettier, you just need to set :h 'formatprg' option to npx prettier --stdin-filepath %, and gqip will format the paragraph with it (it also works if you select the text first and then use gq — vipgq).

While this feature is great as is, it’s still lacking

How to customize gq

We can customize gq by remapping it to use the g@ operator instead, which will run whatever function you pass to the :h 'operatorfunc' option. See :h :map-operator for more information.

vim
function! s:Format(...)
  " Some logic here to do the formatting
endfunction

nmap <silent> gq :set operatorfunc=<SID>Format<CR>g@
vmap <silent> gq :<C-U>set operatorfunc=<SID>Format<CR>gvg@

Now our job will consist of writing what the s:Format function does.

The most innocuous implementation is to simply execute the gq behavior, unchanged:

vim
function! s:Format(...)
  normal! '[v']gq
endfunction

Of course, this is pointless, so let’s add to that function to enhance our experience:

Avoid changing the jumplist

The first small tweak is to avoid changing the jumplist when we format text, which we do by prefixing the command with :h :keepjumps.

vim
function! s:Format(...)
  keepjumps normal! '[v']gq
endfunction

Silent execution

I also make the command execute silently, so I won’t be interrupted by hit-enter prompts and error messages will not show up in the message history:

vim
function! s:Format(...)
  silent keepjumps normal! '[v']gq
endfunction

Error handling

The external program might fail to format the file — e.g. there is a syntax error and prettier refuses to format it. The default experience is bad because your code will be replaced by error messages, which is absolutely not something anyone would want.

I originally learned about how to work around this in a GitHub gist by romainl:

vim
function! s:Format(...)
  silent keepjumps normal! '[v']gq
  if v:shell_error > 0
    silent undo
    echohl ErrorMsg
    echomsg 'formatprg "' . &formatprg . '" exited with status ' . v:shell_error
    echohl None
  endif
endfunction

After gq is used, we can check if an error occurred during the executing of formatprg with the v:shell_error special variable, which holds the program’s exit code. If it’s non-zero, it means the command failed so we undo the operation and show up an error message.

Format file preserving cursor position

I also learned this from romainl’s GitHub gist.

The procedure remains almost unchanged, if only slightly refactored into one function, mapped to gQ:

vim
function! s:FormatFile() abort
  let w:view = winsaveview()
  keepjumps normal! gg
  set operatorfunc=<SID>Format
  keepjumps normal! g@G
  keepjumps call winrestview(w:view)
  unlet w:view
endfunction

nmap <silent> gQ :call <SID>FormatFile()<CR>

It consists of saving the current window view with :h winsaveview(), running the operator as usual but moving over the entire file with ggg@G (but tries not to modify the jumplist), then restore the window view with :h winrestview().

Integration with coc.nvim

Since I use coc.nvim as my LSP client, I wish I could reuse gq to format using LSP, if it was available.

It turns out it was surprisingly simple to implement it:

vim
function! s:Format(type, ...)
  if CocHasProvider('formatRange')
    call CocAction('formatSelected', a:type)
    return
  endif

  " ...
endfunction

As you can see, I first check if check if the current buffer has an LSP server attached to it and is able to format text ranges with the CocHasProvider function.

I then use the formatSelected action, which receives the type of visual mode last used (either line, char or block, the output of :h visualmode()), which fortunately the operatorfunc function also receives as the first argument (see :h :map-operator).