本文介绍了Neovim的诊断系统,它提供了一个统一框架来管理来自不同工具的代码问题反馈。

诊断信息由诊断提供者产生,需要创建命名空间、设置配置参数、生成诊断信息,并通过API将诊断信息设置到当前buffer。

诊断信息包含严重程度、位置、消息等字段。Nvim提供的API分为两类:一类作用于诊断生产者,一类作用于诊断消费者。诊断处理器负责渲染诊断信息,可通过vim.diagnostic.show()函数显示给用户。

内置处理器包括virtual_text、virtual_lines、signs和underline。自定义处理器可以通过添加到vim.diagnostic.handlers表来实现。

概要

Neovim 的诊断系统提供了一个统一的框架,用于管理来自各种工具(如 LSP 服务器、linter、静态分析工具等)的代码问题反馈。该诊断系统是对现有错误处理功能的扩展,例如quickfix list等现有错误处理功能的扩展。

graph TB A[诊断源] --> |LSP| B[vim.lsp] A --> |Linter| C[nvim-lint] A --> |其他工具| D[自定义提供者] B & C & D --> E[生成诊断] E --> F[转换为 vim.Diagnostic.Set] F --> N[转换为 vim.Diagnostic] F --> G[vim.diagnostic.set] N --> O[vim.diagnostic.get] O --> P[自定义插件] P --> Q[自定义显示方式] P --> I[显示子系统] G --> H[诊断存储] H --> I[显示子系统] I --> J[虚拟文本] I --> K[位置列表] I --> L[符号列标记] I --> M[浮动窗口]

能够产生诊断信息的工具称为“诊断提供者”,诊断提供者仅需要下面一些简单步骤就可产生诊断信息:

  1. 创建一个命名空间,可通过nvim_create_namespace()函数。注意必须是有名命名空间,匿名明明空间不会生效。
  2. 通过vim.diagnostic.config()函数为命名空间设置配置参数。
  3. 生成诊断信息
  4. 通过vim.diagnostic.set()函数将诊断信息设置到当前buffer。
  5. 重复步骤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结构体信息如下:

字段名类型默认值描述
lnuminteger(必填)诊断信息的起始行号(0-based)。
colinteger0诊断信息的起始列号(0-based)。
end_lnumintegerlnum(同起始行)诊断信息的结束行号(0-based)。
end_colintegercol(同起始列)诊断信息的结束列号(0-based)。
severityvim.diagnostic.Severityvim.diagnostic.severity.ERROR诊断的严重级别(枚举值,如 ERRORWARN 等)。
messagestring(必填)诊断的文本描述(如错误消息)。
sourcestring(可选)诊断的来源(例如 linter 工具名或插件名)。
codestringinteger(可选)诊断的唯一标识码(如错误代码 E123)。
user_dataany(可选)插件可添加的任意附加数据(用于扩展功能)。

vim.Diagnostic扩展了vim.Diagnostic.Set结构,因此其所有字段含义如下所示:

字段名类型默认值描述
bufnrinteger(必填)缓冲区编号,标识诊断信息所属的 Neovim 缓冲区。
namespaceinteger(必填)命名空间 ID,用于区分不同插件/工具产生的诊断(如 LSP 或 Linter)。
lnuminteger(必填)诊断起始行号(0-based),继承自 vim.Diagnostic.Set
colinteger0诊断起始列号(0-based),继承自 vim.Diagnostic.Set
end_lnumintegerlnum(同起始行)诊断结束行号(0-based),继承自 vim.Diagnostic.Set
end_colintegercol(同起始列)诊断结束列号(0-based),继承自 vim.Diagnostic.Set
severityvim.diagnostic.Severityvim.diagnostic.severity.ERROR严重级别(枚举值:ERROR/WARN/INFO/HINT),继承自基类。
messagestring(必填)诊断描述文本(如错误详情),继承自 vim.Diagnostic.Set
sourcestring(可选)诊断来源(如 clangdeslint),继承自 vim.Diagnostic.Set
codestringinteger(可选)诊断代码(如 E1011001),继承自 vim.Diagnostic.Set
user_dataany(可选)插件自定义数据,继承自 vim.Diagnostic.Set

默认快捷键

快捷键作用
]d跳转到当前buffer的下一个诊断
[d跳转到当前buffer的上一个诊断
]D跳转到当前buffer的最后一个诊断
[D跳转到当前buffer的第一个诊断
<C-w>d在浮动窗口显示当前诊断

诊断处理器

诊断数据最终会通过vim.diagnostic.show()函数显示给用户。但是诊断的显示方式是由诊断处理器handlers来决定。

结构:每个handlers是一个Lua表(table),包含两个关键函数:

  1. show(namespace, bufnr, diagnostics, opts):负责渲染诊断信息(如显示虚拟文本、符号标记)。
  2. hide(namespace, bufnr)(可选):负责清理 show 产生的效果(如隐藏临时元素)。

调用时机:当调用vim.diagnostic.show()时,系统会遍历所有已注册的处理器,执行其show函数。

graph LR A[vim.diagnostic.show调用] --> B[遍历所有handlers] B --> C[执行handler.show] C --> D[根据opts过滤并渲染诊断] D --> E[用户界面显示]

参数解析:

  1. namespace :用于隔离不同来源的诊断(如 LSP 插件、静态分析工具),避免冲突。通过 vim.api.nvim_create_namespace() 创建唯一 ID。
  2. bufnr :指定诊断所属的缓冲区编号,确保诊断精准绑定到具体文件(如 vim.api.nvim_get_current_buf() 获取当前缓冲区)。
  3. diagnostics :需显示的诊断列表,每条诊断需符合 vim.Diagnostic 数据结构(包含 lnum, col, message 等字段)。
  4. opts全局配置的解析结果:包含所有通过 vim.diagnostic.config() 设置的选项,且已动态解析(如函数配置项会被提前执行)。

下面是neovim/runtime/lua/vim/diagnostic.luavim.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,使每行仅显示最高等级的诊断

graph LR A[原始诊断] --> B(按行过滤最高严重级诊断) B --> C[调用原生渲染器] C --> D[仅显示每行一个标记] style A fill:#f9f,stroke:#333 style D fill:#bbf,stroke:#333
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字段说明:

字段名类型/默认值默认值功能描述
underlineboolean|vim.diagnostic.Opts.Underline|function(namespace, bufnr)true为诊断信息添加下划线高亮,标记问题代码位置。
virtual_textboolean|vim.diagnostic.Opts.VirtualText|function(namespace, bufnr)false在代码行内显示虚拟文本(如错误消息)。
virtual_linesboolean|vim.diagnostic.Opts.VirtualLines|function(namespace, bufnr)false在代码行下方显示虚拟行(需插件支持),适用于多行错误描述。
signsboolean|vim.diagnostic.Opts.Signs|function(namespace, bufnr)true在行号列显示诊断标记
floatboolean|vim.diagnostic.Opts.Float|function(namespace, bufnr)nil控制悬浮窗口行为,悬停光标时显示诊断详情(通过 vim.diagnostic.open_float() 触发)。
update_in_insertbooleanfalse是否在插入模式(Insert Mode)更新诊断。
severity_sortboolean|reverse: booleanfalse按严重性排序诊断信息
jumpvim.diagnostic.Opts.Jump控制 vim.diagnostic.jump() 的默认行为(跳转到上一个/下一个诊断)。

优先级顺序(从高到低):

  1. 临时配置set()/show() 调用时传入)
  2. 命名空间配置(通过 config(namespace) 设置)
  3. 全局配置(通过 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.severityseverity = vim.diagnostic.severity.ERROR
all布尔值true 时忽略其他过滤条件,返回所有诊断(默认 falseall = 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字段信息如下表所示:

字段名类型/默认值功能描述覆盖行为
bufnrinteger指定显示诊断的源缓冲区编号。0 表示当前缓冲区。默认值: 当前缓冲区❌ 不覆盖全局配置
namespaceinteger|integer[]限制仅显示指定命名空间的诊断(允许多个命名空间)。例如 vim.diagnostic.open_float({namespace = ns1})✅ 覆盖 vim.diagnostic.config() 的设置
scope'line'|'buffer'|'cursor'|'c'|'l'|'b'默认值: line诊断显示范围✅ 覆盖全局配置
posinteger|[integer, integer]scopelinecursor 时,替代光标位置❌ 不影响全局配置
severity_sortboolean|{ reverse?: boolean }默认值: false按严重性排序诊断(影响显示顺序)✅ 覆盖 vim.diagnostic.config() 的设置
severityvim.diagnostic.SeverityFilter按严重性过滤诊断(如仅显示错误)✅ 覆盖全局配置
headerstring|[string, any]浮动窗口标题栏配置✅ 覆盖 vim.diagnostic.config() 的设置
sourceboolean|'if_many'是否显示诊断来源✅ 覆盖全局配置
formatfun(diagnostic: vim.Diagnostic): string?自定义诊断消息格式的函数✅ 覆盖全局配置
prefixstring|table|fun(diagnostic, i, total): string, string?诊断消息前缀配置✅ 覆盖 vim.diagnostic.config() 的设置
suffixstring|table|fun(diagnostic, i, total): string, string?诊断消息后缀配置,格式同 prefix✅ 覆盖全局配置
focus_idstring浮动窗口焦点标识符。相同 focus_id 的浮动窗口会被复用而非新建❌ 窗口管理专用
borderstring浮动窗口边框样式(详见 :h nvim_open_win()),可选值:"none" "single" "double" "rounded"❌ 仅影响窗口样式

返回值:两个可选整数:

  1. float_bufnr:浮动窗口的缓冲区编号(integer?),成功时返回编号,失败时为 nil
  2. 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" })