Diagnostic
本文介绍了Neovim的诊断系统,它提供了一个统一框架来管理来自不同工具的代码问题反馈。
诊断信息由诊断提供者产生,需要创建命名空间、设置配置参数、生成诊断信息,并通过API将诊断信息设置到当前buffer。
诊断信息包含严重程度、位置、消息等字段。Nvim提供的API分为两类:一类作用于诊断生产者,一类作用于诊断消费者。诊断处理器负责渲染诊断信息,可通过vim.diagnostic.show()
函数显示给用户。
内置处理器包括virtual_text、virtual_lines、signs和underline。自定义处理器可以通过添加到vim.diagnostic.handlers
表来实现。
概要
Neovim 的诊断系统提供了一个统一的框架,用于管理来自各种工具(如 LSP 服务器、linter、静态分析工具等)的代码问题反馈。该诊断系统是对现有错误处理功能的扩展,例如quickfix list等现有错误处理功能的扩展。
能够产生诊断信息的工具称为“诊断提供者”,诊断提供者仅需要下面一些简单步骤就可产生诊断信息:
- 创建一个命名空间,可通过
nvim_create_namespace()
函数。注意必须是有名命名空间,匿名明明空间不会生效。 - 通过
vim.diagnostic.config()
函数为命名空间设置配置参数。 - 生成诊断信息
- 通过
vim.diagnostic.set()
函数将诊断信息设置到当前buffer。 - 重复步骤3。
通俗来讲,Nvim提供的API分为两类,一类是作用于诊断生产者,一类则作用于诊断消费者,例如为当前buffer读取或显示的终端用户。用于诊断生产者的API需要一个命名空间作为第1个参数,用于诊断消费者的API则不需要,遵循的规则是如果修改buffer的诊断信息,则需要命名空间,读取则不需要。
诊断信息存在severity
关键字,定义为vim.diagnostic.severity
,用于表示诊断的严重程度,其取值如下:
vim.diagnostic.severity.ERROR
vim.diagnostic.severity.WARN
vim.diagnostic.severity.INFO
vim.diagnostic.severity.HINT
可以通过给vim.diagnostic.get()
函数传入severity
参数获取当前buffer的诊断信息。有三种形式如下:
vim.diagnostic.get(0, { severity = vim.diagnostic,severity.WARN })
vim.diagnostic.get(0, { severity = { min = vim.diagnostic.severity.WARN }})
vim.diagnostic.get(0, { severity = { vim.diagnostic.severity.WARN, vim.diagnostic.severity.INFO }})
描述诊断的数据结构
vim.Diagnostic.Set结构体信息如下:
字段名 | 类型 | 默认值 | 描述 |
---|---|---|---|
lnum | integer | 无(必填) | 诊断信息的起始行号(0-based)。 |
col | integer | 0 | 诊断信息的起始列号(0-based)。 |
end_lnum | integer | lnum (同起始行) | 诊断信息的结束行号(0-based)。 |
end_col | integer | col (同起始列) | 诊断信息的结束列号(0-based)。 |
severity | vim.diagnostic.Severity | vim.diagnostic.severity.ERROR | 诊断的严重级别(枚举值,如 ERROR 、WARN 等)。 |
message | string | 无(必填) | 诊断的文本描述(如错误消息)。 |
source | string | 无(可选) | 诊断的来源(例如 linter 工具名或插件名)。 |
code | string 或 integer | 无(可选) | 诊断的唯一标识码(如错误代码 E123 )。 |
user_data | any | 无(可选) | 插件可添加的任意附加数据(用于扩展功能)。 |
vim.Diagnostic扩展了vim.Diagnostic.Set结构,因此其所有字段含义如下所示:
字段名 | 类型 | 默认值 | 描述 |
---|---|---|---|
bufnr | integer | 无(必填) | 缓冲区编号,标识诊断信息所属的 Neovim 缓冲区。 |
namespace | integer | 无(必填) | 命名空间 ID,用于区分不同插件/工具产生的诊断(如 LSP 或 Linter)。 |
lnum | integer | 无(必填) | 诊断起始行号(0-based),继承自 vim.Diagnostic.Set 。 |
col | integer | 0 | 诊断起始列号(0-based),继承自 vim.Diagnostic.Set 。 |
end_lnum | integer | lnum (同起始行) | 诊断结束行号(0-based),继承自 vim.Diagnostic.Set 。 |
end_col | integer | col (同起始列) | 诊断结束列号(0-based),继承自 vim.Diagnostic.Set 。 |
severity | vim.diagnostic.Severity | vim.diagnostic.severity.ERROR | 严重级别(枚举值:ERROR /WARN /INFO /HINT ),继承自基类。 |
message | string | 无(必填) | 诊断描述文本(如错误详情),继承自 vim.Diagnostic.Set 。 |
source | string | 无(可选) | 诊断来源(如 clangd 或 eslint ),继承自 vim.Diagnostic.Set 。 |
code | string 或 integer | 无(可选) | 诊断代码(如 E101 或 1001 ),继承自 vim.Diagnostic.Set 。 |
user_data | any | 无(可选) | 插件自定义数据,继承自 vim.Diagnostic.Set 。 |
默认快捷键
快捷键 | 作用 |
---|---|
]d | 跳转到当前buffer的下一个诊断 |
[d | 跳转到当前buffer的上一个诊断 |
]D | 跳转到当前buffer的最后一个诊断 |
[D | 跳转到当前buffer的第一个诊断 |
<C-w>d | 在浮动窗口显示当前诊断 |
诊断处理器
诊断数据最终会通过vim.diagnostic.show()
函数显示给用户。但是诊断的显示方式是由诊断处理器handlers来决定。
结构:每个handlers是一个Lua表(table),包含两个关键函数:
show(namespace, bufnr, diagnostics, opts)
:负责渲染诊断信息(如显示虚拟文本、符号标记)。hide(namespace, bufnr)
(可选):负责清理 show 产生的效果(如隐藏临时元素)。
调用时机:当调用vim.diagnostic.show()
时,系统会遍历所有已注册的处理器,执行其show
函数。
参数解析:
namespace
:用于隔离不同来源的诊断(如 LSP 插件、静态分析工具),避免冲突。通过vim.api.nvim_create_namespace()
创建唯一 ID。bufnr
:指定诊断所属的缓冲区编号,确保诊断精准绑定到具体文件(如vim.api.nvim_get_current_buf()
获取当前缓冲区)。diagnostics
:需显示的诊断列表,每条诊断需符合vim.Diagnostic
数据结构(包含lnum
,col
,message
等字段)。opts
:全局配置的解析结果:包含所有通过vim.diagnostic.config()
设置的选项,且已动态解析(如函数配置项会被提前执行)。
下面是neovim/runtime/lua/vim/diagnostic.lua
中vim.diagnostic.show
源码
function M.show(namespace, bufnr, diagnostics, opts)
vim.validate("namespace", namespace, "number", true)
vim.validate("bufnr", bufnr, "number", true)
vim.validate("diagnostics", diagnostics, vim.islist, true, "a list of diagnostics")
vim.validate("opts", opts, "table", true)
if not bufnr or not namespace then
assert(not diagnostics, "Cannot show diagnostics without a buffer and namespace")
if not bufnr then
for iter_bufnr in pairs(diagnostic_cache) do
M.show(namespace, iter_bufnr, nil, opts)
end
else
-- namespace is nil
bufnr = vim._resolve_bufnr(bufnr)
for iter_namespace in pairs(diagnostic_cache[bufnr]) do
M.show(iter_namespace, bufnr, nil, opts)
end
end
return
end
if not M.is_enabled({ bufnr = bufnr or 0, ns_id = namespace }) then
return
end
M.hide(namespace, bufnr)
diagnostics = diagnostics or get_diagnostics(bufnr, { namespace = namespace }, true)
if vim.tbl_isempty(diagnostics) then
return
end
local opts_res = get_resolved_options(opts, namespace, bufnr)
if opts_res.update_in_insert then
clear_scheduled_display(namespace, bufnr)
else
local mode = api.nvim_get_mode()
if mode.mode:sub(1, 1) == "i" then
schedule_display(namespace, bufnr, opts_res)
return
end
end
if opts_res.severity_sort then
if type(opts_res.severity_sort) == "table" and opts_res.severity_sort.reverse then
table.sort(diagnostics, function(a, b)
return a.severity < b.severity
end)
else
table.sort(diagnostics, function(a, b)
return a.severity > b.severity
end)
end
end
for handler_name, handler in pairs(M.handlers) do
if handler.show and opts_res[handler_name] then
local filtered = filter_by_severity(opts_res[handler_name].severity, diagnostics)
handler.show(namespace, bufnr, filtered, opts_res)
end
end
end
handlers的配置
handlers需要通过vim.diagnostic.config
进行统一配置,例如
vim.diagnostic.config({
-- 这两个是默认内置处理器
virtual_text = { prefix = "■" }, -- 配置 virtual_text 处理器
signs = { severity = vim.diagnostic.severity.ERROR } -- 仅显示错误级诊断
})
自定义处理器需添加到 vim.diagnostic.handlers
表(如 vim.diagnostic.handlers.my_custom_handler = { show = ... }
)。
若处理器配置中包含 severity
键(如 severity = vim.diagnostic.severity.WARN
),则传入的 diagnostics
列表会被自动过滤,仅保留匹配严重性的诊断。
内置的处理器
处理器类型 | 功能描述 |
---|---|
"virtual_text" | 在代码行内显示诊断文本(如错误消息) |
"virtual_lines" | 在代码行下方显示虚拟行(需插件支持) |
"signs" | 在行号列显示标记(如 >> 表示错误) |
"underline" | 在问题代码下方添加波浪线 |
自定义handler示例
示例1:在光标悬停时触发当前行的诊断信息
-- 定义浮动窗口处理器
local float_handler = {
show = function(namespace, bufnr, diagnostics, opts)
-- 仅在光标悬停时触发
vim.api.nvim_create_autocmd("CursorHold", {
buffer = bufnr,
callback = function()
local diag = vim.diagnostic.get(bufnr, { lnum = vim.fn.line(".") - 1 })[1]
if diag then
vim.diagnostic.open_float({ scope = "line" })
end
end
})
end,
hide = function(namespace, bufnr)
vim.api.nvim_clear_autocmds({ event = "CursorHold", buffer = bufnr })
end
}
-- 注册并启用
vim.diagnostic.handlers.my_float = float_handler
vim.diagnostic.config({ my_float = true }) -- 启用自定义处理器
示例2:使用vim.notify
展示当前buffer的ERROR级别的诊断
-- 使用vim.notify提示当前buffer中ERROR诊断数目
vim.diagnostic.handlers["my/notify"] = {
show = function(namespace, bufnr, diagnostics, opts)
-- 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)
vim.notify(msg, level)
end,
}
-- Users can configure the handler
vim.diagnostic.config({
["my/notify"] = {
log_level = vim.log.levels.INFO,
-- This handler will only receive "error" diagnostics.
severity = vim.diagnostic.severity.ERROR,
},
})
示例3:覆盖内置handler,使每行仅显示最高等级的诊断
local ns = vim.api.nvim_create_namespace("my_namespace")
local orig_signs_handler = vim.diagnostic.handlers.signs
vim.diagnostic.handlers.signs = {
show = function(_, bufnr, _, opts)
-- 获取当前缓冲区所有诊断
local diagnostics = vim.diagnostic.get(bufnr)
-- 按行筛选最严重诊断
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
-- 将过滤后的诊断传递给原始处理器
local filtered_diagnostics = vim.tbl_values(max_severity_per_line)
orig_signs_handler.show(ns, bufnr, filtered_diagnostics, opts)
end,
hide = function(_, bufnr)
orig_signs_handler.hide(ns, bufnr)
end,
}
常用接口
vim.diagnostic.config()
vim.diagnostic.config({opts},{namespace})
是 Neovim 中用于管理诊断信息(如错误、警告)显示的核心接口,通过分层配置机制实现全局、命名空间级和临时级设置。
参数 | 类型 | 作用 |
---|---|---|
{opts} | vim.diagnostic.Opts? | 配置表(可选)。若为空则返回当前配置;否则更新配置(如 { virtual_text = { prefix = "■" } } )。 |
{namespace} | integer? | 命名空间 ID(可选)。指定时更新该命名空间的配置;未指定则更新全局配置。 |
vim.diagnostic.Opts
字段说明:
字段名 | 类型/默认值 | 默认值 | 功能描述 |
---|---|---|---|
underline | boolean|vim.diagnostic.Opts.Underline|function(namespace, bufnr) | true | 为诊断信息添加下划线高亮,标记问题代码位置。 |
virtual_text | boolean|vim.diagnostic.Opts.VirtualText|function(namespace, bufnr) | false | 在代码行内显示虚拟文本(如错误消息)。 |
virtual_lines | boolean|vim.diagnostic.Opts.VirtualLines|function(namespace, bufnr) | false | 在代码行下方显示虚拟行(需插件支持),适用于多行错误描述。 |
signs | boolean|vim.diagnostic.Opts.Signs|function(namespace, bufnr) | true | 在行号列显示诊断标记 |
float | boolean|vim.diagnostic.Opts.Float|function(namespace, bufnr) | nil | 控制悬浮窗口行为,悬停光标时显示诊断详情(通过 vim.diagnostic.open_float() 触发)。 |
update_in_insert | boolean | false | 是否在插入模式(Insert Mode)更新诊断。 |
severity_sort | boolean|reverse: boolean | false | 按严重性排序诊断信息 |
jump | vim.diagnostic.Opts.Jump | 控制 vim.diagnostic.jump() 的默认行为(跳转到上一个/下一个诊断)。 |
优先级顺序(从高到低):
- 临时配置(
set()
/show()
调用时传入) - 命名空间配置(通过
config(namespace)
设置) - 全局配置(通过
config()
设置)
返回值与使用技巧
- 返回值:当
{opts}
为nil
时,返回当前生效的配置表(合并了全局和命名空间规则)。 - 动态调整:结合
namespace
隔离不同工具(如 LSP 与 Linter),避免显示冲突。 - 性能优化:限制单文件诊断数量(
max_signs = 100
)防止卡顿。
示例如下:
-- 全局启用虚拟文本
vim.diagnostic.config({ virtual_text = true })
-- 为命名空间 ns1 单独禁用虚拟文本
vim.diagnostic.config({ virtual_text = false }, ns1)
-- 临时调用时覆盖配置:本次显示禁用虚拟文本
vim.diagnostic.show(ns1, bufnr, diagnostics, { virtual_text = false })
vim.diagnostic.get()
vim.diagnostic.get({bufnr}, {opts})
获取指定缓冲区的诊断信息(错误/警告等)
参数说明:
bufnr 参数取值 | 含义 | 示例 |
---|---|---|
0 | 获取当前缓冲区的诊断 | vim.diagnostic.get(0) |
nil | 获取所有缓冲区的诊断(返回全局诊断列表) | vim.diagnostic.get(nil) |
正整数 | 获取指定编号缓冲区的诊断(通过 vim.api.nvim_get_current_buf() 获取编号) | vim.diagnostic.get(42) |
opts
表支持的字段信息为vim.Diagnostic.GetOpts
结构,其字段信息如下:
字段名 | 类型 | 作用 | 示例值 |
---|---|---|---|
namespace | 整数 | 过滤指定命名空间的诊断(通过 vim.api.nvim_create_namespace() 创建) | namespace = my_namespace_id |
lnum | 整数 | 过滤特定行的诊断(行号从 0 开始) | lnum = 42 |
severity | 表/整数 | 按严重性过滤(见 vim.diagnostic.severity ) | severity = vim.diagnostic.severity.ERROR |
all | 布尔值 | true 时忽略其他过滤条件,返回所有诊断(默认 false ) | all = true |
返回 vim.Diagnostic[]
类型数组(Lua 表),见上面。
vim.diagnostic.open_float()
vim.diagnostic.open_float({opts})
在浮动窗口中渲染诊断信息,适用于交互式查看问题(如光标悬停时展示错误详情)。其行为可通过 {opts}
表精细控制。如果未提供 {opts}
,则使用全局默认配置(通过 vim.diagnostic.config()
设置)。
参数 {opts}
详细解析:{opts}
是一个 Lua 表(vim.diagnostic.Opts.Float
),所有字段均为可选。以下字段覆盖全局配置(优先级高于 vim.diagnostic.config()
),并支持动态函数调用。
vim.diagnostic.Opts.Float字段信息如下表所示:
字段名 | 类型/默认值 | 功能描述 | 覆盖行为 |
---|---|---|---|
bufnr | integer | 指定显示诊断的源缓冲区编号。0 表示当前缓冲区。默认值: 当前缓冲区 | ❌ 不覆盖全局配置 |
namespace | integer|integer[] | 限制仅显示指定命名空间的诊断(允许多个命名空间)。例如 vim.diagnostic.open_float({namespace = ns1}) | ✅ 覆盖 vim.diagnostic.config() 的设置 |
scope | 'line'|'buffer'|'cursor'|'c'|'l'|'b' 默认值: line | 诊断显示范围 | ✅ 覆盖全局配置 |
pos | integer|[integer, integer] | 当 scope 为 line 或 cursor 时,替代光标位置 | ❌ 不影响全局配置 |
severity_sort | boolean|{ reverse?: boolean } 默认值: false | 按严重性排序诊断(影响显示顺序) | ✅ 覆盖 vim.diagnostic.config() 的设置 |
severity | vim.diagnostic.SeverityFilter | 按严重性过滤诊断(如仅显示错误) | ✅ 覆盖全局配置 |
header | string|[string, any] | 浮动窗口标题栏配置 | ✅ 覆盖 vim.diagnostic.config() 的设置 |
source | boolean|'if_many' | 是否显示诊断来源 | ✅ 覆盖全局配置 |
format | fun(diagnostic: vim.Diagnostic): string? | 自定义诊断消息格式的函数 | ✅ 覆盖全局配置 |
prefix | string|table|fun(diagnostic, i, total): string, string? | 诊断消息前缀配置 | ✅ 覆盖 vim.diagnostic.config() 的设置 |
suffix | string|table|fun(diagnostic, i, total): string, string? | 诊断消息后缀配置,格式同 prefix | ✅ 覆盖全局配置 |
focus_id | string | 浮动窗口焦点标识符。相同 focus_id 的浮动窗口会被复用而非新建 | ❌ 窗口管理专用 |
border | string | 浮动窗口边框样式(详见 :h nvim_open_win() ),可选值:"none" "single" "double" "rounded" 等 | ❌ 仅影响窗口样式 |
返回值:两个可选整数:
float_bufnr
:浮动窗口的缓冲区编号(integer?
),成功时返回编号,失败时为nil
。winid
:窗口ID(integer?
),用于后续操作(如关闭窗口)。
配置示例
-- diagnostic
vim.diagnostic.config({
virtual_lines = {
severity = { min = vim.diagnostic.severity.WARN },
current_line = true,
format = function(diag)
local severity_map = { [1] = "ERROR", [2] = "WARN", [3] = "INFO", [4] = "HINT" }
return string.format("%s [%s] %s", diag.source, severity_map[diag.severity], diag.message)
end,
},
severity_sort = true,
float = {
scope = "line",
severity_sort = true,
header = "Diagnostics",
source = "if_many",
format = function(diag)
local severity_map = { [1] = "ERROR", [2] = "WARN", [3] = "INFO", [4] = "HINT" }
return string.format("[%s] %s", severity_map[diag.severity], diag.message)
end,
border = "rounded", -- 使用内置圆角边框(直接生效)
},
})
-- 2. 优化 float 窗口打开行为(通过自定义函数封装)
local function open_optimized_diagnostic_float()
vim.diagnostic.open_float({
-- 可选:限制窗口最大宽度/高度
max_width = math.floor(vim.o.columns * 0.5), -- 最大宽度为屏幕50%
max_height = math.floor(vim.o.lines * 0.4), -- 最大高度为屏幕40%
})
end
vim.keymap.set("n", "<space>e", open_optimized_diagnostic_float, { desc = "Open optimized diagnostics" })
- 原文作者:生如夏花
- 原文链接:https://DBL2017.github.io/post/%E5%B7%A5%E5%85%B7%E4%BD%BF%E7%94%A8/%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91/neovim/diagnostic/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。