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 check whether null-ls has already registered them to prevent duplicate registration.
Sources may define a condition, which determines whether null-ls should register the source.
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.
The list below is non-exhaustive. See the matching source file for a full list.
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
Source callbacks have access to a params
table as their first argument, which
is broadly analogous to the language server protocol's params
object. The
table contains information about the current buffer, editor state, and other
method-specific information.
local params = {
client_id, -- null-ls client id (number)
lsp_method, -- LSP method that triggered request / notification (string)
lsp_params, -- original LSP params from request / notification (table)
options, -- options from LSP params (e.g. formattingOptions) (table|nil)
content, -- current buffer content (string[])
bufnr, -- current buffer's number (number)
method, -- null-ls method (string)
row, -- cursor's current row (number, zero-indexed)
col, -- cursor's current column (number)
bufname, -- full path to current buffer (string)
filetype, -- current buffer's filetype (string)
root, -- current buffer's root directory (string)
-- method == null_ls.methods.RANGE_FORMATTING
range, -- converted LSP range (table)
-- method == null_ls.methods.COMPLETION
word_to_complete, -- keyword under cursor (string)
}
The params
table has access to methods, which source callbacks can use to
access information about the parent source:
Returns the parent source of the params in question.
Returns a table containing user-specific configuration for the parent source. If the user hasn't explicitly set this key, this method returns an empty table.
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. -
utils.root_has_file_matches
: accepts a Lua string matcher pattern. Returnstrue
if a file 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,
}
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 explicitly 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.