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 formattingendfunction 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']gqendfunction
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']gqendfunction
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']gqendfunction
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 endifendfunction
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:viewendfunction 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
).