Use Neovim as a language server to inject LSP diagnostics, code actions, and more via Lua.
Neovim's LSP ecosystem is growing, and plugins like telescope.nvim and trouble.nvim make it a joy to work with LSP features like code actions and diagnostics.
Unlike the VS Code and coc.nvim ecosystems, Neovim doesn't provide a way for non-LSP sources to hook into its LSP client. null-ls is, first of all, an attempt to bridge that gap and simplify the process of creating, sharing, and setting up LSP sources.
null-ls is also an attempt to reduce the confusion, bloat, and boilerplate required for general-purpose language servers like efm-langserver and diagnostic-languageserver. null-ls makes it straightforward to transform command-line output into a format that Neovim's LSP client can handle, using Lua and without any extra executables or overhead.
null-ls is in alpha status. I'll do my best to avoid breaking changes that affect users or integrations, but bugs are inevitable, and the plugin's behavior might not yet match user expectations. Please open an issue if something doesn't work the way you expect (or doesn't work at all).
Any and all feedback, criticism, or contributions about the plugin's features, code quality, and user experience are greatly appreciated.
null-ls sources are able to hook into the following LSP features:
-
Code actions
-
Diagnostics
-
Formatting
null-ls includes built-in sources for each of these features to provide out-of-the-box functionality. See BUILTINS for instructions on how to set up sources and a list of available sources.
Contributions to add more built-ins for any language are always welcome.
null-ls also provides helpers to streamline the process of spawning and transforming the output of command-line processes into an LSP-friendly format. If you want to create your own source, either for personal use or for a plugin, see HELPERS for details.
Install null-ls using your favorite package manager. The plugin depends on plenary.nvim.
To enable the plugin, you must call null_ls.setup {} somewhere in your LSP
configuration, which will set up the necessary autocommands. You must then
register a source, either manually or via an integration.
Please see CONFIG for information about setting up and configuring null-ls.
The definitive source for information about null-ls is its documentation, which is still a work in progress but contains information about how null-ls works, how to set it up, and how to create sources.
The following example demonstrates a (naive) filetype-independent source that
provides a code action to comment the current line using commentstring.
local null_ls = require("null-ls")
local api = vim.api
local comment_line = {
method = null_ls.methods.CODE_ACTION,
filetypes = {"*"},
generator = {
fn = function(params)
-- sources have access to a params object
-- containing info about the current file and editor state
local bufnr = params.bufnr
local line = params.content[params.row]
-- all nvim api functions are safe to call
local commentstring =
api.nvim_buf_get_option(bufnr, "commentstring")
-- null-ls combines and stores returned actions in its state
-- and will call action() on execute
return {
{
title = "Comment line",
action = function()
api.nvim_buf_set_lines(bufnr, params.row - 1,
params.row, false, {
string.format(commentstring, line)
})
end
}
}
end
}
}
null_ls.register(comment_line)The following example demonstrates a diagnostic source that will show instances
of the word really in the current text as LSP warnings.
local null_ls = require("null-ls")
local api = vim.api
local no_really = {
method = null_ls.methods.DIAGNOSTICS,
filetypes = {"markdown", "txt"},
generator = {
fn = function(params)
local diagnostics = {}
for i, line in ipairs(params.content) do
local col, end_col = string.find(line, "really")
if col and end_col then
-- null-ls fills in undefined positions
-- and converts source diagnostics into the required format
table.insert(diagnostics, {
row = i,
col = col - 1,
end_col = end_col,
source = "no-really",
message = "Don't use 'really!'",
severity = 2
})
end
end
return diagnostics
end
}
}
null_ls.register(no_really)Asynchronous sources have access to a done() callback that, when called, will
signal their completion. The client uses plenary.nvim's async library to run
asynchronous sources concurrently and wait for all results before sending them
to Neovim's LSP client.
The following example demonstrates an asynchronous source that provides a code
action to insert a comment at the top of the current file containing its size,
which it gets asynchronously via luv.
local uv = vim.loop
local file_size_comment = {
method = null_ls.methods.CODE_ACTION,
filetypes = {"*"},
generator = {
-- must be explictly defined
async = true,
fn = function(params, done)
local bufnr = params.bufnr
local commentstring =
api.nvim_buf_get_option(bufnr, "commentstring")
uv.fs_open(params.bufname, "r", 438, function(_, fd)
if not fd then return done() end
uv.fs_fstat(fd, function(_, stat)
return done({
{
title = "Insert file size",
action = function()
api.nvim_buf_set_lines(bufnr, 0, 0, false, {
string.format(commentstring,
"size: " .. stat.size)
})
end
}
})
end)
end)
end
}
}
null_ls.register(file_size_comment)This ESLint integration from one of my plugins demonstrates a more elaborate example of parsing JSON output from a command to generate sources for code actions, diagnostics, and formatting.
null-ls formatters run when you call vim.lsp.buf.formatting() or
vim.lsp.buf.formatting_sync().
If you have other language servers running that can format the current buffer,
Neovim will prompt you to choose a formatter. You can prevent actual LSP clients
from providing formatting by adding the following snippet to your LSP
on_attach callback:
-- add to the on_attach callback for the server you want to disable
on_attach = function(client)
client.resolved_capabilities.document_formatting = false
endSee the following snippet:
-- add to a specific server's on_attach,
-- or to a shared on_attach to enable for all supported filetypes
on_attach = function(client)
if client.resolved_capabilities.document_formatting then
vim.cmd("autocmd BufWritePost <buffer> lua vim.lsp.buf.formatting()")
end
endIn most cases, yes. null-ls tries to act like an actual LSP server as much as possible, so it should work seamlessly with most LSP-related plugins, but it makes some compromises when necessary. If you run into problems, please open an issue.
For a high-level overview: null-ls spawns a headless instance of Neovim as a minimal RPC server, whose main purpose is to respond to Neovim's LSP client with its capabilities. The client instance keeps the server alive and shuts it down on exit or after a period of inactivity.
Everything else happens by modifying the client's request and notify
handlers to redirect or override their default behavior, run defined sources,
and send their output back to Neovim's LSP client.
More testing is necessary, but informal metrics show that the performance impact of running a (nearly) inactive headless instance of Neovim is minimal compared to the overhead required by a general-purpose language server.
Since null-ls uses pure Lua, minimizes server communication, and removes the need to communicate via JSON, in most cases it should (theoretically) run faster than similar solutions.
Neovim's LSP ecosystem is wonderful, and I want to take advantage of it wherever I can. Other ecosystems are also much freer about what defines a "valid" code action or diagnostic - in fact, the inspiration for this plugin came from a desire to replicate a VS Code plugin that creates code actions from the output of a CLI program.
Arguably, general-purpose language servers are already "cheating" by creating what looks like LSP output from non-LSP programs. null-ls skips a step and keeps it within Neovim.
The test suite includes unit and integration tests and depends on plenary.nvim.
Run make test in the root of the project to run the suite or
FILE=filename_spec.lua make test-file to test an individual file.
- Continue improving documentation
- Add more built-ins
- Investigate other potential LSP integrations