Ergonomic mappings for code formatting in Vim
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.
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:
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.
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:
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:
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:
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:
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).