Neovim Diagnostics
In its journey to provide a superb developer experience Neovim has extended the Vim concept of the quickfix list with an improved, modern version called diagnostics (term I suspect comes from the world of LSPs). Neovim diagnostics are however independent from LSPs, they are a framework for displaying errors and warnings from any external tools (linters, LSPs, etc) or on demand with user defined errors and/or warnings.
Table of Contents
Getting Started
Anything that reports diagnostics to neovim is referred to as a diagnostic producer. In order to hook a producer of diagnostics into neovim’s diagnostics one needs to:
-- 1. Create a namespace that identifies the producer:
local ns_id = vim.api.nvim_create_namespace("My diagnostics producer")
-- 2. (Optionally) configure options for the diagnostic namespace:
vim.diagnostic.config
-- 3. Generate diagnostics (this would happen based on whichever business logic
-- generates these diagnostics)
-- 4. Set the diagnostics for the buffer:
vim.diagnostic.set()
Diagnostic Message
The diagnostic itself, the error or warning one wants to have appear in neovim is a lua table that has a number of fields to describe the message. Some of the most interesting are:
lnum
: Starting line number (required)col
: Starting column (required)message
: Message (required)bufnr
: Buffer numberseverity
: Severity of the diagnostic - Error, Warning, Info or Hint (:h vim.diagnostic serverity
)end_lnum
: Ending line numberend_col
: Ending column
See :h diagnostic-structure
for additional fields.
Showing Diagnostics
You can show diagnostics to the user using the vim.diagnostic.show()
method.
The display of diagnostics is normally managed through the use of handlers. A handler is a table with a “show” and (optionally) a “hide” methods:
show = function(namespace, bufnr, diagnostics, opts)
hide = function(namespace, bufnr)
Handlers can be added by creating a new key in vim.diagnostic.handlers
and configured using the vim.diagnostic.config()
method:
-- This example comes from :h diagnostic-handlers
-- It's good practice to namespace custom handlers to avoid collisions
vim.diagnostic.handlers["my/notify"] = {
show = function(namespace, bufnr, diagnostics, opts)
-- The opts table passed to a handler contains the handler configuration
-- that a user can configure via vim.diagnostic.config.
-- In our example, the opts table has a "log_level" option
local level = opts["my/notify"].log_level
local name = vim.diagnostic.get_namespace(namespace).name
local msg = string.format("%d diagnostics in buffer %d from %s",
#diagnostics,
bufnr,
name)
-- The call to vim.notify notifies the user of diagnostics
-- which is similar to `:echo "hello diagnostic"`. This doesn't
-- show a diagnostic. So there's no need to implement hide.
vim.notify(msg, level)
end,
}
-- Users can configure the handler
vim.diagnostic.config({
["my/notify"] = {
-- This table here are the *opts* parameter sent to the handler
log_level = vim.log.levels.INFO
}
})
Neovim provides a number of handlers by default: “virtual_text”, “signs” and “underline”. These and any other handler can be overriden:
-- Create a custom namespace. This will aggregate signs from all other
-- namespaces and only show the one with the highest severity on a
-- given line
local ns = vim.api.nvim_create_namespace("my_namespace")
-- Get a reference to the original signs handler
local orig_signs_handler = vim.diagnostic.handlers.signs
-- Override the built-in signs handler
vim.diagnostic.handlers.signs = {
show = function(_, bufnr, _, opts)
-- Get all diagnostics from the whole buffer rather than just the
-- diagnostics passed to the handler
local diagnostics = vim.diagnostic.get(bufnr)
-- Find the "worst" diagnostic per line
local max_severity_per_line = {}
for _, d in pairs(diagnostics) do
local m = max_severity_per_line[d.lnum]
if not m or d.severity < m.severity then
max_severity_per_line[d.lnum] = d
end
end
-- Pass the filtered diagnostics (with our custom namespace) to
-- the original handler
local filtered_diagnostics = vim.tbl_values(max_severity_per_line)
-- This will result in showing diagnostics for real
orig_signs_handler.show(ns, bufnr, filtered_diagnostics, opts)
end,
hide = function(_, bufnr)
orig_signs_handler.hide(ns, bufnr)
end,
}
Diagnostic Highlights
The highlights defined for diagnotics begin with Diagnostic followed by the type of highlight and severity. For example:
DiagnosticSignError
DiagnosticUnderlineWarn
You can access these highlights via the :highlight
ex command:
# show highlight
:highlight DiagnosticError
# clear highlight
:highlight clear DiagnosticError
# set highlight (see :h highlight-args for actual
# key-value pairs that are available)
:highlight DiagnosticError {key}={arg} ...
:hi DiagnosticError guifg=#db4b4b
Diagnostic Severity
:h vim.diagnostic.severity
Diagnostic signs
Neovim diagnostics defines signs for each type of diagnostic serverity. The default text for each sign is the first letter of the severity name: E, W, I, H.
Signs can be customized using the :sign
ex-command:
sign define DiagnosticSignError text=E texthl=DiagnosticSignError linehl= numhl=
When the severity-sort
option is set the priority of each sign depends on the severity of the diagnostic (otherwise all signs have the same priority).
Diagnostic Events
Diagnostic events can be used to configure autocommands:
DiagnosticChanged
: diagnostics have changed
vim.api.nvim_create_autocmd('DiagnosticChanged', {
callback = function(args)
local diagnostics = args.data.diagnostics
-- print diagnostics as a message
vim.pretty_print(diagnostics)
end,
})
API
The vim.diagnostic
api lives under the vim.diagnostic
namespace. So all methods before should be prepended with vim.diagnostic
e.g. vim.diagnostic.config
.
Config
config(opts, namespace)
: Config diagnostics globally or for a given namespace
Diagnostics config can be provided globally, per namespace or for a single call to vim.diagnostic.show()
. Each of these has more priority than the last.
The opts table contains the following properties:
underline
: (defaults to true) Use underline for diagnostics. Alternative provide a specific severity to underline.signs
: (defaults to true) Use signs for diagnostics. Alternative specify severity or priority.virtual_text
: (default true) Use virtual text for diagnostics. There’s lots of config options for how the virtual text looks like, take a look at:h vim.diagnostic.config
for more info.float
: Options for floating windows. See:h vim.diagnostic.open_float()
.update_in_insert
: (default false) Update diagnostics in Insert mode (if false, diagnostics are updated on InsertLeave)severity_sort
: (default false) Sort diagnostics by severity. This affects the order in which signs and virtual text are displayed. When true, higher severities are displayed before lower severities. You can reverse the priority withreverse
.
-- The `virtual_text` config allows you to define a `format` function that
-- takes a diagnostic as input and returns a string. The return value is the
-- text used to display the diagnostic.
function(diagnostic)
if diagnostic.severity == vim.diagnostic.severity.ERROR then
return string.format("E: %s", diagnostic.message)
end
return diagnostic.message
end
You can call vim.diagnostic.config()
to get the current global config, or vim.diagnostic.config(nil, my_namespace)
to get the config for a given namespace.
Enable and Disable
disable(bufnr, namespace)
: disable diagnostics globally, in a current buffer (0) or a given buffer, and optionally for a given namespace.enable(bufnr, namespace)
: like above but enable
Quickfix Integration
fromqflist(list)
: convert a list of quickfix items to a list of diagnostics. Thelist
can be retrieved usinggetqflist()
orgetloclist()
.
Get diagnostics
get(bufnr, {namespace, lnum, severity})
: Get current diagnosticsget_next(opts)
: Get next diagnostic closes to cursorget_next_pos(opts)
: Get position of the next diagnostic in the current buffer(row, col)
.get_prev(opts)
: Get previous diagnostic closest to the cursor.get_prev_pos(opts)
: Get position of the previous diagnostic(row, col)
.goto_next(opts)
: Move to the next diagnostic. Where some interesting properties in theopts
table are:namespace
cursor_position
as(row, col)
tuplewrap
whether to wrap around fileseverity
float
open float after movingwin_id
window id
goto_prev(opts)
: Like above but move to previous diagnostic.
Interact with diagnostics
hide(namespace, bufrn)
: hide currently displayed diagnostic
Utilities to produce diagnostics
match(str, pat, groups, severity_map, defaults)
: parse a diagnostic from a string. This is something that you could use to integrate third party linters or other diagnostic producing tools
-- this example comes from :h vim.diagnostics.match.
-- You can appreciate how it uses a pattern regex to
-- extract all the portions needed to create a
-- diagnostic
local s = "WARNING filename:27:3: Variable 'foo' does not exist"
local pattern = "^(%w+) %w+:(%d+):(%d+): (.+)$"
local groups = { "severity", "lnum", "col", "message" }
vim.diagnostic.match(s, pattern, groups, { WARNING = vim.diagnostic.WARN })
Get metadata
diagnostic.get_namespace()
: Get namespace metadatadiagnostic.get_namespaces()
: Get current diagnostics namespaces.
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.