Splits into the following documents:
-
MAIN (this document), which describes general concepts and implementation details
-
CONFIG, which describes the available configuration options
-
BUILTINS, an automatically generated list of available built-in sources
-
BUILTIN_CONFIG.md, which describes how to configure built-in sources
-
SOURCES, which describes the source API
-
HELPERS, which describes available helpers and how to use them to make new sources
-
TESTING, which describes best practices for testing null-ls integrations
The basic unit of null-ls operation is a source.
Each source must have a method, which defines when it runs, a generator, which runs in response to a request matching the source's method, and a list of filetypes, which determines when the method is active.
To see a list of active sources for the current buffer in a human-readable
format, use the command :NullLsInfo
.
Sources must be registered, either by the user or by an integration, before they are active. You can get, set, and manipulate sources using the source API, as described in SOURCES.
Sources can also define a name, which allows integrations to see if sources null-ls has already registered their sources to prevent duplicate registration.
Sources may define a condition, which determines whether null-ls should register the source.
The document describes these parameters below.
null-ls methods are analogous to LSP methods, but the plugin uses internal
methods to avoid collisions. The safest way to use methods in a source
definition is by referencing the methods
object.
local null_ls = require("null-ls")
local my_source = {}
-- source will run on LSP code action request
my_source.method = null_ls.methods.CODE_ACTION
-- source will run on LSP diagnostics request
my_source.method = null_ls.methods.DIAGNOSTICS
-- source will run on LSP formatting request
my_source.method = null_ls.methods.FORMATTING
-- source will run on LSP hover request
my_source.method = null_ls.methods.HOVER
-- source will run on LSP completion request
my_source.method = null_ls.methods.COMPLETION
A source has a list of filetypes, which define when the source is active. The list can contain a single filetype, more than one filetype, or nothing, which indicates that the source should activate for all filetypes.
local my_source = {}
-- single
my_source.filetypes = { "lua" }
-- more than one
my_source.filetypes = { "lua", "teal" }
-- all filetypes
my_source.filetypes = {}
Sources can also include a list of disabled_filetypes
. null-ls checks these
first, so they'll supersede any defined filetypes.
null-ls can register sources via the setup
method (intended for a user's
configuration) or via the register
method (intended for integrations or
advanced use cases).
local null_ls = require("null-ls")
-- using setup method
null_ls.setup({ sources = my_sources })
-- using register method
null_ls.register({ my_sources })
Both options accept a single source, a list of sources, or a table containing more than one source with shared configuration.
local null_ls = require("null-ls")
-- single source
null_ls.register({ my_source })
-- list of sources with independent configuration
null_ls.register({ my_source, my_other_source })
-- more than one source with shared configuration
null_ls.register({ name = "my-sources", filetypes = { "lua" }, sources = { my_source, my_other_source } })
Note that dynamically registering a diagnostic source will refresh diagnostics for buffers affected by the new source.
null-ls optionally saves source names to prevent duplicate registration. This
means integrations can call register()
any number of times without worrying
about duplicate registration.
local null_ls = require("null-ls")
-- registered
null_ls.register({ name = "my-sources", ... })
-- not registered
null_ls.register({ name = "my-sources", ... })
null-ls also exposes a method, is_registered()
, that returns a
boolean value indicating whether it has already registered a source.
local null_ls = require("null-ls")
local name = "my_sources"
print(null_ls.is_registered(name)) -- false
null_ls.register({ name = "my-sources", ... })
print(null_ls.is_registered(name)) -- true
A source may define a condition, which is an optional callback that accepts one
argument. The argument (described here as utils
) is a table of utilities to
handle common conditional checks.
-
utils.root_has_file
: accepts either a table (indicating more than one file) or a string (indicating a single file). Returnstrue
if at least one file exists at the project's root. -
utils.root_matches
: accepts a Lua string matcher pattern. Returnstrue
if the root matches the specified pattern.
On registration, null-ls will store conditional sources in state and check
condition
at the first opportunity (typically upon entering a named buffer).
If condition
returns a truthy value at that point, null-ls will register the
source.
Once checked, null-ls will not check the same condition again.
null-ls generators define what a source provides when it receives a request that
matches its method. Generators must define the key-value pair fn
, which is the
callback that runs when null-ls calls the source.
A generator's fn
is schedule-wrapped, making it safe to call any API function.
It's also wrapped to handle errors, meaning it'll catch errors thrown from
within fn
and show them to the user as warnings.
local my_source = {}
my_source.generator = {
fn = function(params)
return {
{
col = 1,
row = 1,
message = "There is something wrong with this file!",
severity = 1,
source = "my-source",
},
}
end,
}
For convenience, all generator functions have access to a params
table as
their first argument, which contains information about the current file and
editor state.
local params = {
content, -- current buffer content (table, split at newline)
lsp_method, -- lsp method that triggered request (string)
method, -- null-ls method that triggered generator (string)
row, -- cursor's current row (number, zero-indexed)
col, -- cursor's current column (number)
bufnr, -- current buffer's number (number)
bufname, -- current buffer's full path (string)
ft, -- current buffer's filetype (string)
root, -- current buffer's root directory (string)
}
Setting async = true
inside a generator will run it as an asynchronous
generator. Asynchronous generators have access to a done()
callback as their
second argument. Processing will pause until all async generators have called
done()
, either with nil
or a list of results.
local my_source = {}
my_source.generator = {
-- must be explictly set
async = true,
fn = function(params, done)
-- always return done() to prevent timeouts
if not string.match(params.content, "something wrong") then
return done()
end
-- return results as normal inside the done() callback
my_async_function(params, function()
return done({
{
col = 1,
row = 1,
message = "There is something wrong with this file!",
severity = 1,
source = "my-source",
},
})
end)
end,
}
If generator.multiple_files
is true
, the generator can return results for
more than one file at a time. At the moment, null-ls supports multi-file
generators for diagnostics (see below).
Generators must return nil
or a list containing their results. The structure
of each item in the list varies depending on the null-ls method that invoked the
generator.
All return values are required unless specified as optional.
return { {
title, -- string
action, -- function (callback with no arguments)
} }
Once generated, null-ls stores code action results in its internal state and calls them if selected by the user. It clears and re-generates non-selected actions on the next request.
Like generator functions, code action callbacks are schedule-wrapped, making it safe to call any API function.
-- null-ls assumes ranges are 1-indexed, so sources should offset if not
return { {
row, -- number, optional (defaults to first line)
col, -- number, optional (defaults to beginning of line)
end_row, -- number, optional (defaults to row)
end_col, -- number, optional (defaults to end of line),
source, -- string, optional (defaults to "null-ls")
code, -- number, optional
message, -- string
severity, -- 1 (error), 2 (warning), 3 (information), 4 (hint)
filename, -- string, optional (requires generator.multiple_files)
bufnr, -- number, optional (requires generator.multiple_files)
} }
null-ls generates diagnostics in response to LSP notifications and publishes
them via the vim.diagnostic
API.
When generator.multiple_files
is true, specifying filename
or bufnr
publishes diagnostics to the specified buffer. Otherwise, null-ls publishes
diagnostics to the buffer that triggered the LSP diagnostic (in most cases the
active buffer).
Note that filename
(if specified) should be an absolute path to the file.
severity
should be one of the values described in :help diagnostic-severity
.
If unspecified, severity
falls back to the config option fallback_severity
,
described in CONFIG.
return { {
row, -- number, optional (see diagnostics for defaults)
col, -- number, optional
end_row, -- number, optional
end_col, -- number, optional
text, -- string
} }
null-ls applies formatting results to the matching buffer and, depending on the user's settings, will optionally write the buffer.
Users can register an arbitrary number of formatters for a single filetype. To match expected behavior, formatters run sequentially in the order in which the user has registered them (manually or via an integration).
Same as formatting.
Note that getting range-formatted output requires converting the visually
selected range into the required format and modifying the spawn arguments
accordingly. See range_formatting_args_factory
in HELPERS for an
example of how null-ls handles this for built-in
sources.
return { "First line", "Second line", "And so on" }
Hover sources should return a list of plaintext strings, where each element represents a single line.
null-ls will combine the results of each of its hover sources when calling the handler, so 2+ null-ls hover sources are okay, but note that running more than one LSP server with hover capabilities is not well-supported (by default, the second popup will wipe out the first).
return {
{
items = { label = "Item #1", insertText = "Item #1", documentation = "A test completion item" },
isIncomplete = true,
},
}
Completion sources must return a
CompletionList.
You can leverage the full attributes of CompletionItem
from the LSP
specification. Other plugins (e.g completion plugins) can then use these
attributes to provide more context about the highlighted completion item.