Zero-plugin linting and fixing in Neovim

October 10, 2022

Like Vim, Neovim comes with a bunch of preconfigured “compilers” (i.e., general settings for makeprg and errorformat), yet it is possible to define additional compilers when you’re not relying on a language server or, as in my case, when you want to use a specific linter via makeprg. Indeed, when it does not make sense to use a build system, it might be interesting to consider a linter as an alternative for Vim’s make feature. The results of invoking :make are displayed in the quickfix window, which I particularly like. Likewise, the formatprg option allows one to define a fixer for gq‘ing on a selection. We can also define a mapping to fix the whole buffer. This way, we can have linting and fixing facilities without relying on external plugins, like null-ls, ALE or efm-langserver.

Usually, I stand by the already defined compilers, but I added two specifications, one for pytest and the other for shellcheck. The following is for pytest, and it has been proposed by Phelipe:1

if exists('current_compiler')
let current_compiler = 'pytest'

if exists(':CompilerSet') != 2  " older Vim always used :setlocal
  command -nargs=* CompilerSet setlocal <args>

CompilerSet makeprg=pytest\ --tb=short\ -vv\ $*
CompilerSet errorformat=
      \%EE\ \ \ \ \ File\ \"%f\"\\,\ line\ %l,
      \%CE\ \ \ %p^,
      \%ZE\ \ \ %[%^\ ]%\\@=%m,
      \%Afile\ %f\\,\ line\ %l,
      \%+ZE\ %mnot\ found,
      \%CE\ %.%#,
      \%-G_%\\+\ ERROR%.%#\ _%\\+,
      \%A_%\\+\ %o\ _%\\+,
      \%C%f:%l:\ in\ %o,
      \%ZE\ %\\{3}%m,
      \%+G%[=]%\\+\ %*\\d\ passed%.%#,

function! FixColumnNumber()
  if b:current_compiler !=? 'pytest'

  let qflist = getqflist()
  for i in qflist
    let i.col = i.col + 1
  call setqflist(qflist)

augroup FixPytestQuickFix
  autocmd QuickFixCmdPost <buffer> call FixColumnNumber()
augroup END

For shellcheck, I have the following in my compiler directory:

CompilerSet makeprg=shellcheck\ -f\ gcc
CompilerSet errorformat=%f:%l:%c:\ %trror:\ %m\ [SC%n],
               \%f:%l:%c:\ %tarning:\ %m\ [SC%n],
               \%f:%l:%c:\ %tote:\ %m\ [SC%n],

In both cases above, the tricky part is to manage the errorformat. Once you have defined your custom compiler, you can declare it in your filetype file in after/ftplugin and optionally defined an appropriate makeprg command. For instance, in the case of shellcheck I use the following:

compiler shellcheck
setlocal makeprg=shellcheck\ -f\ gcc\ %
nmap <buffer> <silent> g= :!shfmt -i 2 -ln posix -sr -ci -s -w %<cr>:redraw!<cr>

The last line define a mapping to format the whole buffer using shfmt. I use g= for vim.lsp.buf.format() when a language server is available too. Of course I realize that the ALE or null-ls plugins handle all of that in a more elegant and efficient way, especially since it is asynchroneous contrary to the approach presented above and you are limited to one linter unless you write a shell script to gather several commands,2 but that’s my take for exploiting Neovim’s builtins.

I summarized sone of the options I have in my vimrc folder in the following table.

Filetype LSP Linter Fixer
pythonpyrightbuilt-inblack, isort
shnot usedshellcheckshfmt
javascriptnot usedquick-lint-jsprettier

♪ Greg Abate • Sunshower

  1. It is available on GitHub↩︎

  2. I don’t really mind formatting a buffer using a shortcut, although it is possible to use an autocommand to format it on save. ↩︎

See Also

» Speeding up Neovim » Unified colors of TUIs » Debugging in Vim » Haskell and Vim » On wrapping and reflowing text