Ergonomic mappings for code formatting in Vim - Phelipe Teles

Ergonomic mappings for code formatting in Vim

3 min.

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 gqvipgq).

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 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:

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

vim
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:

vim
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:

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  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:

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: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:

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