For a long time, I'd been hearing about null_ls on the neovim subreddit and how it solves all my non-language-server tooling problems.
I had given a brief look at null_ls
a while back, and didn't really comprehend what it wanted of me. I didn't understand how my neovim config applied, and how I setup my language servers would be applicable or not.
It wasn't until yesterday when I was playing around with custom root_dir
functions on tsserver
and glint
language server.
Normal language servers are set up by requiring lspconfig
and calling the appropriate server's setup method:
local lsp = require('lspconfig')
lsp[serverName].setup({
-- config here
})
and null_ls
is similar:
local null_ls = require('null-ls')
null_ls.setup({
-- config here
})
and that's the whole gist 🎉.
Of course, just being able to call setup
isn't enough, I wanted the following features:
- eslint
- diagnostics and code-actions
- run on save
- prettier
- diagnostics and code-actions
- run on save
- spell check diagnostics and actions
- a single machine-global file to store my custom words
- I had lost this when I switched away from CoC
It turns out that all of these integrations are built-in to null_ls
!!
As I was browsing for which built-in configs would work best for me, I learned that both prettier and eslint have daemon versions, eslint_d
and prettierd
. This is important to me because I want my editor to be as fast and as responsive as possible, and in order to accomplish that with node
's boot time, daemonizing just makes sense to help out with time and memory and block the main thread for the least amount of time possible while performing "format on save". Both README's for these two tools explain the philosophy behind their approaches.
To make null_ls
as obvious and as isolated as possible, I did all configuration in a separate file that I require from my main lsp-config lua file.
To set up all null_ls
stuff, I do this "somewhere":
require('plugin-config.lsp.integrations')
where I have a plugin-config/lsp/integrations.lua
file where all the null_ls
code lives.
The Code / config
In my file where I configure what plugins I want, I have this:
use {
'jose-elias-alvarez/null-ls.nvim',
requires = { "nvim-lua/plenary.nvim" }
}
I use packer for package management.
The official docs are really good, and I really just needed to have read them.
Then my plugin-config/lsp/integration.lua
file has these contents, below -- this is the combination of a bunch of docs, example code, and other people's solution to problems. I'll explain in comments in the snippet in case things may not be clear.
---------------------------------------------------------
--
-- NULL LS is for hooking up non-LSP tools to the LSP UX
--
--
-- Be sure to :checkhealth to see if any underlying tools are missing
--
-- pnpm add --global @fsouza/prettierd cspell typescript
--
---------------------------------------------------------
local null_ls = require('null-ls')
local augroup = vim.api.nvim_create_augroup("LspFormatting", {})
-- TODO: figure out how to wire up ember-template-lint
local lsp_formatting = function(buffer)
vim.lsp.buf.format({
filter = function(client)
-- By default, ignore any formatters provider by other LSPs
-- (such as those managed via lspconfig or mason)
-- Also "eslint as a formatter" doesn't work :(
return client.name == "null-ls"
end,
bufnr = buffer,
})
end
-- Format on save
-- https://github.com/jose-elias-alvarez/null-ls.nvim/wiki/Avoiding-LSP-formatting-conflicts#neovim-08
local on_attach = function(client, buffer)
-- the Buffer will be null in buffers like nvim-tree or new unsaved files
if (not buffer) then
return
end
if client.supports_method("textDocument/formatting") then
vim.api.nvim_clear_autocmds({ group = augroup, buffer = buffer })
vim.api.nvim_create_autocmd("BufWritePre", {
group = augroup,
buffer = buffer,
callback = function()
lsp_formatting(buffer)
end,
})
end
end
null_ls.setup({
sources = {
-- Prettier, but faster (daemonized)
null_ls.builtins.formatting.prettierd.with({
filetypes = {
"css", "json", "jsonc","javascript", "typescript",
"javascript.glimmer", "typescript.glimmer",
"handlebars"
}
}),
-- Code actions for staging hunks, blame, etc
null_ls.builtins.code_actions.gitsigns,
null_ls.builtins.completion.luasnip,
-- Spell check that has better tooling
-- all stored locally
-- https://github.com/streetsidesoftware/cspell
null_ls.builtins.diagnostics.cspell.with({
-- This file is symlinked from my dotfiles repo
extra_args = { "--config", "~/.cspell.json" }
}),
null_ls.builtins.code_actions.cspell.with({
-- This file is symlinked from my dotfiles repo
extra_args = { "--config", "~/.cspell.json" }
})
-- null_ls.builtins.completion.spell,
},
on_attach = on_attach
})
ESLint
The native ESLint LSP is similar to prettierd, in that it has a long-running process from within which to invoke eslint from.
I did this in a separate commit.
You can see that I tried out eslint_d
(and I've since removed it from the above code snippets in this post),
but I ran in to an issue where the way eslint_d
manages processes caused a lot of zombie processes when I'd eventually exit neovim.
Since node tends to be memory heavy, having so many zombie processes just would not be acceptable.
The thing missing from the ESLint LSP is formatting with the LSP formatting integration.
The main things to have when using ESLint with "--fix on save", are:
local eslint = require('lspconfig').eslint
eslint.setup({
on_attach = function(client, bufnr)
vim.api.nvim_create_autocmd("BufWritePre", {
buffer = bufnr,
command = "EslintFixAll",
})
end,
})
And then I could remove the entries for diagnostics.eslint_d
and formatting.eslint_d
in my null_ls.setup
.