mirror of
https://github.com/neovim/neovim.git
synced 2026-08-28 01:51:52 +00:00
refactor(diagnostic): split diagnostic module
Extract the diagnostic implementation from runtime/lua/vim/diagnostic.lua into focused internal modules covering config, display, float rendering, jump/list helpers, namespace and storage management, severity/shared utilities, and statusline support. Move the builtin handlers into runtime/lua/vim/diagnostic/handlers/ and keep runtime/lua/vim/diagnostic.lua as the public facade that lazily dispatches to the split modules. This preserves the external vim.diagnostic API while making the implementation easier to navigate and reason about. AI-assisted: Codex
This commit is contained in:
committed by
Lewis Russell
parent
9e80f63c30
commit
1f53abf54b
File diff suppressed because it is too large
Load Diff
157
runtime/lua/vim/diagnostic/_config.lua
Normal file
157
runtime/lua/vim/diagnostic/_config.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
local store = require('vim.diagnostic._store')
|
||||
|
||||
--- @class vim.diagnostic.OptsResolved
|
||||
--- @field float vim.diagnostic.Opts.Float
|
||||
--- @field update_in_insert boolean
|
||||
--- @field underline vim.diagnostic.Opts.Underline
|
||||
--- @field virtual_text vim.diagnostic.Opts.VirtualText
|
||||
--- @field virtual_lines vim.diagnostic.Opts.VirtualLines
|
||||
--- @field signs vim.diagnostic.Opts.Signs
|
||||
--- @field severity_sort {reverse?:boolean}
|
||||
|
||||
--- @class (private) vim.diagnostic._config
|
||||
local M = {}
|
||||
|
||||
--- @type vim.diagnostic.Opts
|
||||
local global_diagnostic_options = {
|
||||
signs = true,
|
||||
underline = true,
|
||||
virtual_text = false,
|
||||
virtual_lines = false,
|
||||
float = true,
|
||||
update_in_insert = false,
|
||||
severity_sort = false,
|
||||
jump = {
|
||||
-- Wrap around buffer
|
||||
wrap = true,
|
||||
},
|
||||
}
|
||||
|
||||
--- @param name 'signs'|'underline'|'virtual_text'|'virtual_lines'|'float'
|
||||
function M.enable_handler(name)
|
||||
if global_diagnostic_options[name] == nil then
|
||||
global_diagnostic_options[name] = true
|
||||
end
|
||||
end
|
||||
|
||||
--- @param option string
|
||||
--- @param namespace integer?
|
||||
--- @return table
|
||||
local function enabled_value(option, namespace)
|
||||
local ns = namespace and vim.diagnostic.get_namespace(namespace) or {}
|
||||
if ns.opts and type(ns.opts[option]) == 'table' then
|
||||
return ns.opts[option]
|
||||
end
|
||||
|
||||
local global_opt = global_diagnostic_options[option]
|
||||
if type(global_opt) == 'table' then
|
||||
return global_opt
|
||||
end
|
||||
|
||||
return {}
|
||||
end
|
||||
|
||||
--- @param option string
|
||||
--- @param value any?
|
||||
--- @param namespace integer?
|
||||
--- @param bufnr integer
|
||||
--- @return any
|
||||
local function resolve_optional_value(option, value, namespace, bufnr)
|
||||
if not value then
|
||||
return false
|
||||
elseif value == true then
|
||||
return enabled_value(option, namespace)
|
||||
elseif type(value) == 'function' then
|
||||
local val = value(namespace, bufnr) --- @type any
|
||||
if val == true then
|
||||
return enabled_value(option, namespace)
|
||||
else
|
||||
return val
|
||||
end
|
||||
elseif type(value) == 'table' then
|
||||
return value
|
||||
end
|
||||
error('Unexpected option type: ' .. vim.inspect(value))
|
||||
end
|
||||
|
||||
--- @param opts vim.diagnostic.Opts?
|
||||
--- @param namespace integer?
|
||||
--- @param bufnr integer
|
||||
--- @return vim.diagnostic.OptsResolved
|
||||
function M.get_resolved_options(opts, namespace, bufnr)
|
||||
local ns = namespace and vim.diagnostic.get_namespace(namespace) or {}
|
||||
-- Do not use tbl_deep_extend so that an empty table can be used to reset to default values
|
||||
local resolved = vim.tbl_extend('keep', opts or {}, ns.opts or {}, global_diagnostic_options) --- @type table<string,any>
|
||||
for k in pairs(global_diagnostic_options) do
|
||||
if resolved[k] ~= nil then
|
||||
resolved[k] = resolve_optional_value(k, resolved[k], namespace, bufnr)
|
||||
end
|
||||
end
|
||||
return resolved --[[@as vim.diagnostic.OptsResolved]]
|
||||
end
|
||||
|
||||
--- @param opts vim.diagnostic.Opts? When omitted or `nil`, retrieve the current
|
||||
--- configuration. Otherwise, a configuration table (see |vim.diagnostic.Opts|).
|
||||
--- @param namespace integer? Update the options for the given namespace.
|
||||
--- When omitted, update the global diagnostic options.
|
||||
--- @return vim.diagnostic.Opts? : Current diagnostic config if {opts} is omitted.
|
||||
function M.config(opts, namespace)
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
vim.validate('namespace', namespace, 'number', true)
|
||||
|
||||
local t --- @type vim.diagnostic.Opts
|
||||
if namespace then
|
||||
local ns = vim.diagnostic.get_namespace(namespace)
|
||||
t = ns.opts
|
||||
else
|
||||
t = global_diagnostic_options
|
||||
end
|
||||
|
||||
if not opts then
|
||||
-- Return current config
|
||||
return vim.deepcopy(t, true)
|
||||
end
|
||||
|
||||
local jump_opts = opts.jump --[[@as vim.diagnostic.JumpOpts1]]
|
||||
if jump_opts and jump_opts.float ~= nil then --- @diagnostic disable-line
|
||||
vim.deprecate('opts.jump.float', 'opts.jump.on_jump', '0.14')
|
||||
|
||||
local float_opts = jump_opts.float
|
||||
if float_opts then
|
||||
float_opts = type(float_opts) == 'table' and float_opts or {}
|
||||
|
||||
jump_opts.on_jump = function(_, bufnr)
|
||||
vim.diagnostic.open_float(vim.tbl_extend('keep', float_opts, {
|
||||
bufnr = bufnr,
|
||||
scope = 'cursor',
|
||||
focus = false,
|
||||
}))
|
||||
end
|
||||
end
|
||||
|
||||
opts.jump.float = nil --- @diagnostic disable-line
|
||||
end
|
||||
|
||||
for k, v in
|
||||
pairs(opts --[[@as table<any,any>]])
|
||||
do
|
||||
t[k] = v
|
||||
end
|
||||
|
||||
if namespace then
|
||||
for _, bufnr in ipairs(store.get_bufnrs()) do
|
||||
local namespaces = store.get_buf_namespaces(bufnr)
|
||||
if vim.list_contains(namespaces, namespace) then
|
||||
vim.diagnostic.show(namespace, bufnr)
|
||||
end
|
||||
end
|
||||
else
|
||||
for _, bufnr in ipairs(store.get_bufnrs()) do
|
||||
for _, ns in ipairs(store.get_buf_namespaces(bufnr)) do
|
||||
vim.diagnostic.show(ns, bufnr)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
193
runtime/lua/vim/diagnostic/_display.lua
Normal file
193
runtime/lua/vim/diagnostic/_display.lua
Normal file
@@ -0,0 +1,193 @@
|
||||
local api = vim.api
|
||||
|
||||
local diagnostic_modules = vim._defer_require('vim.diagnostic', {
|
||||
_config = ..., --- @module 'vim.diagnostic._config'
|
||||
_severity = ..., --- @module 'vim.diagnostic._severity'
|
||||
_shared = ..., --- @module 'vim.diagnostic._shared'
|
||||
_store = ..., --- @module 'vim.diagnostic._store'
|
||||
})
|
||||
|
||||
--- @class (private) vim.diagnostic._display
|
||||
local M = {}
|
||||
|
||||
-- Metatable that automatically creates an empty table when assigning to a missing key
|
||||
local bufnr_and_namespace_cacher_mt = {
|
||||
--- @param t table<integer,table>
|
||||
--- @param bufnr integer
|
||||
--- @return table
|
||||
__index = function(t, bufnr)
|
||||
assert(bufnr > 0, 'Invalid buffer number')
|
||||
t[bufnr] = {}
|
||||
return t[bufnr]
|
||||
end,
|
||||
}
|
||||
|
||||
--- @type table<integer,table<integer,table>>
|
||||
local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt)
|
||||
|
||||
--- @type table<string,true>
|
||||
local registered_autocmds = {}
|
||||
|
||||
--- Table of autocmd events to fire the update for displaying new diagnostic information
|
||||
local insert_leave_auto_cmds = { 'InsertLeave', 'CursorHoldI' }
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
--- @return string
|
||||
local function make_augroup_key(namespace, bufnr)
|
||||
local ns = vim.diagnostic.get_namespace(namespace)
|
||||
return string.format('nvim.diagnostic.insertleave.%s.%s', bufnr, ns.name)
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
local function execute_scheduled_display(namespace, bufnr)
|
||||
local args = bufs_waiting_to_update[bufnr][namespace]
|
||||
if not args then
|
||||
return
|
||||
end
|
||||
|
||||
-- Clear the args so we don't display unnecessarily.
|
||||
bufs_waiting_to_update[bufnr][namespace] = nil
|
||||
|
||||
M.show(namespace, bufnr, nil, args)
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
--- @param args vim.diagnostic.OptsResolved
|
||||
local function schedule_display(namespace, bufnr, args)
|
||||
bufs_waiting_to_update[bufnr][namespace] = args
|
||||
|
||||
local key = make_augroup_key(namespace, bufnr)
|
||||
if not registered_autocmds[key] then
|
||||
local group = api.nvim_create_augroup(key, { clear = true })
|
||||
api.nvim_create_autocmd(insert_leave_auto_cmds, {
|
||||
group = group,
|
||||
buffer = bufnr,
|
||||
callback = function()
|
||||
execute_scheduled_display(namespace, bufnr)
|
||||
end,
|
||||
desc = 'vim.diagnostic: display diagnostics',
|
||||
})
|
||||
registered_autocmds[key] = true
|
||||
end
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
local function clear_scheduled_display(namespace, bufnr)
|
||||
local key = make_augroup_key(namespace, bufnr)
|
||||
|
||||
if registered_autocmds[key] then
|
||||
api.nvim_del_augroup_by_name(key)
|
||||
registered_autocmds[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- @param namespace integer? Diagnostic namespace. When omitted, hide
|
||||
--- diagnostics from all namespaces.
|
||||
--- @param bufnr integer? Buffer number, or 0 for current buffer. When
|
||||
--- omitted, hide diagnostics in all buffers.
|
||||
function M.hide(namespace, bufnr)
|
||||
vim.validate('namespace', namespace, 'number', true)
|
||||
vim.validate('bufnr', bufnr, 'number', true)
|
||||
|
||||
local buffers = bufnr and { vim._resolve_bufnr(bufnr) } or diagnostic_modules._store.get_bufnrs()
|
||||
for _, iter_bufnr in ipairs(buffers) do
|
||||
local namespaces = namespace and { namespace }
|
||||
or diagnostic_modules._store.get_buf_namespaces(iter_bufnr)
|
||||
for _, iter_namespace in ipairs(namespaces) do
|
||||
for _, handler in pairs(vim.diagnostic.handlers) do
|
||||
if handler.hide then
|
||||
handler.hide(iter_namespace, iter_bufnr)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param namespace integer? Diagnostic namespace. When omitted, show
|
||||
--- diagnostics from all namespaces.
|
||||
--- @param bufnr integer? Buffer number, or 0 for current buffer. When omitted, show
|
||||
--- diagnostics in all buffers.
|
||||
--- @param diagnostics vim.Diagnostic[]? The diagnostics to display. When omitted, use the
|
||||
--- saved diagnostics for the given namespace and
|
||||
--- buffer. This can be used to display a list of diagnostics
|
||||
--- without saving them or to display only a subset of
|
||||
--- diagnostics. May not be used when {namespace}
|
||||
--- or {bufnr} is nil.
|
||||
--- @param opts? vim.diagnostic.Opts Display options.
|
||||
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 ipairs(diagnostic_modules._store.get_bufnrs()) do
|
||||
M.show(namespace, iter_bufnr, nil, opts)
|
||||
end
|
||||
else
|
||||
-- namespace is nil
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
for _, iter_namespace in ipairs(diagnostic_modules._store.get_buf_namespaces(bufnr)) do
|
||||
M.show(iter_namespace, bufnr, nil, opts)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if not vim.diagnostic.is_enabled({ bufnr = bufnr or 0, ns_id = namespace }) then
|
||||
return
|
||||
end
|
||||
|
||||
M.hide(namespace, bufnr)
|
||||
|
||||
diagnostics = diagnostics
|
||||
or diagnostic_modules._store.get_diagnostics(bufnr, {
|
||||
namespace = namespace,
|
||||
}, true)
|
||||
|
||||
if vim.tbl_isempty(diagnostics) then
|
||||
return
|
||||
end
|
||||
|
||||
local opts_res = diagnostic_modules._config.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 diagnostic_modules._shared.diagnostic_cmp(a, b, 'severity', false)
|
||||
end)
|
||||
else
|
||||
table.sort(diagnostics, function(a, b)
|
||||
return diagnostic_modules._shared.diagnostic_cmp(a, b, 'severity', true)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
for handler_name, handler in pairs(vim.diagnostic.handlers) do
|
||||
if handler.show and opts_res[handler_name] then
|
||||
local filtered = diagnostic_modules._severity.filter_by_severity(
|
||||
opts_res[handler_name].severity,
|
||||
diagnostics
|
||||
)
|
||||
handler.show(namespace, bufnr, filtered, opts_res)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
284
runtime/lua/vim/diagnostic/_float.lua
Normal file
284
runtime/lua/vim/diagnostic/_float.lua
Normal file
@@ -0,0 +1,284 @@
|
||||
local api, if_nil = vim.api, vim.F.if_nil
|
||||
local shared = require('vim.diagnostic._shared')
|
||||
local store = require('vim.diagnostic._store')
|
||||
|
||||
--- @class (private) vim.diagnostic._float
|
||||
local M = {}
|
||||
|
||||
local severity = vim.diagnostic.severity
|
||||
|
||||
--- @type table<vim.diagnostic.Severity, string>
|
||||
local floating_highlight_map = {
|
||||
[severity.ERROR] = 'DiagnosticFloatingError',
|
||||
[severity.WARN] = 'DiagnosticFloatingWarn',
|
||||
[severity.INFO] = 'DiagnosticFloatingInfo',
|
||||
[severity.HINT] = 'DiagnosticFloatingHint',
|
||||
}
|
||||
|
||||
--- @param opts vim.diagnostic.Opts.Float
|
||||
--- @param bufnr integer
|
||||
--- @return vim.diagnostic.Opts.Float, vim.diagnostic.Opts
|
||||
local function resolve_float_opts(opts, bufnr)
|
||||
-- Resolve options with user settings from vim.diagnostic.config
|
||||
-- Unlike the other decoration functions (e.g. set_virtual_text, set_signs, etc.) `open_float`
|
||||
-- does not have a dedicated table for configuration options; instead, the options are mixed in
|
||||
-- with its `opts` table. We create a dedicated options table (`float_opts`) that inherits
|
||||
-- missing keys from the global configuration (`global_diagnostic_options.float`), which can
|
||||
-- be a table or a function.
|
||||
local global_opts = assert(vim.diagnostic.config())
|
||||
local float_opts = global_opts.float
|
||||
local resolved_float_opts = type(float_opts) == 'table' and float_opts
|
||||
or (type(float_opts) == 'function' and float_opts(opts.namespace, bufnr) or {})
|
||||
|
||||
return vim.tbl_extend('keep', opts, resolved_float_opts), global_opts
|
||||
end
|
||||
|
||||
--- @param opts vim.diagnostic.Opts.Float?
|
||||
--- @return integer? float_bufnr
|
||||
--- @return integer? winid
|
||||
function M.open(opts, ...)
|
||||
-- Support old (bufnr, opts) signature
|
||||
local bufnr --- @type integer?
|
||||
if opts == nil or type(opts) == 'number' then
|
||||
bufnr = opts
|
||||
opts = ... --- @type vim.diagnostic.Opts.Float
|
||||
else
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
end
|
||||
|
||||
opts = opts or {}
|
||||
bufnr = vim._resolve_bufnr(bufnr or opts.bufnr)
|
||||
local global_opts --- @type vim.diagnostic.Opts
|
||||
opts, global_opts = resolve_float_opts(opts, bufnr)
|
||||
|
||||
local scope = ({ l = 'line', c = 'cursor', b = 'buffer' })[opts.scope] or opts.scope or 'line'
|
||||
local lnum, col --- @type integer, integer
|
||||
local opts_pos = opts.pos
|
||||
if scope == 'line' or scope == 'cursor' then
|
||||
if not opts_pos then
|
||||
local pos = api.nvim_win_get_cursor(0)
|
||||
lnum = pos[1] - 1
|
||||
col = pos[2]
|
||||
elseif type(opts_pos) == 'number' then
|
||||
lnum = opts_pos
|
||||
elseif type(opts_pos) == 'table' then
|
||||
lnum, col = opts_pos[1], opts_pos[2]
|
||||
else
|
||||
error("Invalid value for option 'pos'")
|
||||
end
|
||||
elseif scope ~= 'buffer' then
|
||||
error("Invalid value for option 'scope'")
|
||||
end
|
||||
|
||||
local diagnostics = store.get_diagnostics(bufnr, opts --[[@as vim.diagnostic.GetOpts]], true)
|
||||
|
||||
if scope == 'line' then
|
||||
--- @param diagnostic vim.Diagnostic
|
||||
local function line_filter(diagnostic)
|
||||
local d_lnum, _, d_end_lnum, d_end_col = shared.get_logical_pos(diagnostic)
|
||||
return lnum >= d_lnum
|
||||
and lnum <= d_end_lnum
|
||||
and (d_lnum == d_end_lnum or lnum ~= d_end_lnum or d_end_col ~= 0)
|
||||
end
|
||||
diagnostics = vim.tbl_filter(line_filter, diagnostics)
|
||||
elseif scope == 'cursor' then
|
||||
-- If `col` is past the end of the line, show if the cursor is on the last char in the line
|
||||
local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
|
||||
--- @param diagnostic vim.Diagnostic
|
||||
local function cursor_filter(diagnostic)
|
||||
local d_lnum, d_col, d_end_lnum, d_end_col = shared.get_logical_pos(diagnostic)
|
||||
return lnum >= d_lnum
|
||||
and lnum <= d_end_lnum
|
||||
and (lnum ~= d_lnum or col >= math.min(d_col, line_length - 1))
|
||||
and ((d_lnum == d_end_lnum and d_col == d_end_col) or lnum ~= d_end_lnum or col < d_end_col)
|
||||
end
|
||||
diagnostics = vim.tbl_filter(cursor_filter, diagnostics)
|
||||
end
|
||||
|
||||
if vim.tbl_isempty(diagnostics) then
|
||||
return
|
||||
end
|
||||
|
||||
local severity_sort = if_nil(opts.severity_sort, global_opts.severity_sort)
|
||||
if severity_sort then
|
||||
if type(severity_sort) == 'table' and severity_sort.reverse then
|
||||
table.sort(diagnostics, function(a, b)
|
||||
return shared.diagnostic_cmp(a, b, 'severity', true)
|
||||
end)
|
||||
else
|
||||
table.sort(diagnostics, function(a, b)
|
||||
return shared.diagnostic_cmp(a, b, 'severity', false)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
local lines = {} --- @type string[]
|
||||
local highlights = {} --- @type { hlname: string, prefix?: { length: integer, hlname: string? }, suffix?: { length: integer, hlname: string? } }[]
|
||||
local header = if_nil(opts.header, 'Diagnostics:')
|
||||
if header then
|
||||
vim.validate('header', header, { 'string', 'table' }, "'string' or 'table'")
|
||||
if type(header) == 'table' then
|
||||
-- Don't insert any lines for an empty string
|
||||
if #(header[1] or '') > 0 then
|
||||
lines[#lines + 1] = header[1]
|
||||
highlights[#highlights + 1] = { hlname = header[2] or 'Bold' }
|
||||
end
|
||||
elseif #header > 0 then
|
||||
lines[#lines + 1] = header
|
||||
highlights[#highlights + 1] = { hlname = 'Bold' }
|
||||
end
|
||||
end
|
||||
|
||||
if opts.format then
|
||||
diagnostics = shared.reformat_diagnostics(opts.format, diagnostics)
|
||||
end
|
||||
|
||||
if opts.source and (opts.source ~= 'if_many' or shared.count_sources(bufnr) > 1) then
|
||||
diagnostics = shared.prefix_source(diagnostics)
|
||||
end
|
||||
|
||||
local prefix_opt = opts.prefix
|
||||
or (scope == 'cursor' and #diagnostics <= 1) and ''
|
||||
or function(_, i)
|
||||
return string.format('%d. ', i)
|
||||
end
|
||||
|
||||
local prefix, prefix_hl_group --- @type string?, string?
|
||||
if prefix_opt then
|
||||
vim.validate(
|
||||
'prefix',
|
||||
prefix_opt,
|
||||
{ 'string', 'table', 'function' },
|
||||
"'string' or 'table' or 'function'"
|
||||
)
|
||||
if type(prefix_opt) == 'string' then
|
||||
prefix, prefix_hl_group = prefix_opt, 'NormalFloat'
|
||||
elseif type(prefix_opt) == 'table' then
|
||||
prefix, prefix_hl_group = prefix_opt[1] or '', prefix_opt[2] or 'NormalFloat'
|
||||
end
|
||||
end
|
||||
|
||||
local suffix_opt = opts.suffix
|
||||
or function(diagnostic)
|
||||
return diagnostic.code and string.format(' [%s]', diagnostic.code) or ''
|
||||
end
|
||||
|
||||
local suffix, suffix_hl_group --- @type string?, string?
|
||||
if suffix_opt then
|
||||
vim.validate(
|
||||
'suffix',
|
||||
suffix_opt,
|
||||
{ 'string', 'table', 'function' },
|
||||
"'string' or 'table' or 'function'"
|
||||
)
|
||||
if type(suffix_opt) == 'string' then
|
||||
suffix, suffix_hl_group = suffix_opt, 'NormalFloat'
|
||||
elseif type(suffix_opt) == 'table' then
|
||||
suffix, suffix_hl_group = suffix_opt[1] or '', suffix_opt[2] or 'NormalFloat'
|
||||
end
|
||||
end
|
||||
|
||||
local related_info_locations = {} --- @type table<integer, lsp.Location>
|
||||
for i, diagnostic in ipairs(diagnostics) do
|
||||
if type(prefix_opt) == 'function' then
|
||||
local prefix0, prefix_hl_group0 = prefix_opt(diagnostic, i, #diagnostics)
|
||||
prefix, prefix_hl_group = prefix0 or '', prefix_hl_group0 or 'NormalFloat'
|
||||
end
|
||||
if type(suffix_opt) == 'function' then
|
||||
local suffix0, suffix_hl_group0 = suffix_opt(diagnostic, i, #diagnostics)
|
||||
suffix, suffix_hl_group = suffix0 or '', suffix_hl_group0 or 'NormalFloat'
|
||||
end
|
||||
|
||||
local hiname = floating_highlight_map[diagnostic.severity]
|
||||
local message_lines = vim.split(diagnostic.message, '\n')
|
||||
local default_pre = string.rep(' ', #prefix)
|
||||
for j = 1, #message_lines do
|
||||
local pre = j == 1 and prefix or default_pre
|
||||
local suf = j == #message_lines and suffix or ''
|
||||
lines[#lines + 1] = pre .. message_lines[j] .. suf
|
||||
highlights[#highlights + 1] = {
|
||||
hlname = hiname,
|
||||
prefix = {
|
||||
length = j == 1 and #prefix or 0,
|
||||
hlname = prefix_hl_group,
|
||||
},
|
||||
suffix = {
|
||||
length = #suf,
|
||||
hlname = suffix_hl_group,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
--- @type lsp.DiagnosticRelatedInformation[]
|
||||
local related_info = vim.tbl_get(diagnostic, 'user_data', 'lsp', 'relatedInformation') or {}
|
||||
-- Below the diagnostic, show its LSP related information (if any) in the form of file name and
|
||||
-- range, plus description.
|
||||
for _, info in ipairs(related_info) do
|
||||
local location = info.location
|
||||
local file_name = vim.fs.basename(vim.uri_to_fname(location.uri))
|
||||
local info_suffix = ': ' .. info.message
|
||||
related_info_locations[#lines + 1] = location
|
||||
lines[#lines + 1] = string.format(
|
||||
'%s%s:%s:%s%s',
|
||||
default_pre,
|
||||
file_name,
|
||||
location.range.start.line + 1,
|
||||
location.range.start.character + 1,
|
||||
info_suffix
|
||||
)
|
||||
highlights[#highlights + 1] = {
|
||||
hlname = '@string.special.path',
|
||||
prefix = {
|
||||
length = #default_pre,
|
||||
hlname = prefix_hl_group,
|
||||
},
|
||||
suffix = {
|
||||
length = #info_suffix,
|
||||
hlname = 'NormalFloat',
|
||||
},
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- Used by open_floating_preview to allow the float to be focused
|
||||
if not opts.focus_id then
|
||||
opts.focus_id = scope
|
||||
end
|
||||
|
||||
--- @diagnostic disable-next-line: param-type-mismatch
|
||||
local float_bufnr, winnr = vim.lsp.util.open_floating_preview(lines, 'plaintext', opts)
|
||||
vim.bo[float_bufnr].path = vim.bo[bufnr].path
|
||||
|
||||
-- TODO: Handle this generally (like vim.ui.open()), rather than overriding gf.
|
||||
vim.keymap.set('n', 'gf', function()
|
||||
local cursor_row = api.nvim_win_get_cursor(0)[1]
|
||||
local location = related_info_locations[cursor_row]
|
||||
if location then
|
||||
-- Split the window before calling `show_document` so the window doesn't disappear.
|
||||
vim.cmd.split()
|
||||
vim.lsp.util.show_document(location, 'utf-16', { focus = true })
|
||||
else
|
||||
vim.cmd.normal({ 'gf', bang = true })
|
||||
end
|
||||
end, { buf = float_bufnr, remap = false })
|
||||
|
||||
--- @diagnostic disable-next-line: deprecated
|
||||
local add_highlight = api.nvim_buf_add_highlight
|
||||
|
||||
for i, hl in ipairs(highlights) do
|
||||
local line = lines[i]
|
||||
local prefix_len = hl.prefix and hl.prefix.length or 0
|
||||
local suffix_len = hl.suffix and hl.suffix.length or 0
|
||||
if prefix_len > 0 then
|
||||
add_highlight(float_bufnr, -1, hl.prefix.hlname, i - 1, 0, prefix_len)
|
||||
end
|
||||
add_highlight(float_bufnr, -1, hl.hlname, i - 1, prefix_len, #line - suffix_len)
|
||||
if suffix_len > 0 then
|
||||
add_highlight(float_bufnr, -1, hl.suffix.hlname, i - 1, #line - suffix_len, -1)
|
||||
end
|
||||
end
|
||||
|
||||
return float_bufnr, winnr
|
||||
end
|
||||
|
||||
return M
|
||||
732
runtime/lua/vim/diagnostic/_handlers.lua
Normal file
732
runtime/lua/vim/diagnostic/_handlers.lua
Normal file
@@ -0,0 +1,732 @@
|
||||
local api = vim.api
|
||||
local diagnostic = vim.diagnostic
|
||||
local diagnostic_shared = require('vim.diagnostic._shared')
|
||||
|
||||
local severity = diagnostic.severity
|
||||
|
||||
--- @class vim.diagnostic.Handler
|
||||
--- @field show? fun(namespace: integer, bufnr: integer, diagnostics: vim.Diagnostic[], opts?: vim.diagnostic.OptsResolved)
|
||||
--- @field hide? fun(namespace:integer, bufnr:integer)
|
||||
|
||||
--- @class (private) vim.diagnostic._handlers._extmark : vim.api.keyset.get_extmark_item
|
||||
--- @field [1] integer extmark_id
|
||||
--- @field [2] integer row
|
||||
--- @field [3] integer col
|
||||
--- @field [4] vim.api.keyset.extmark_details
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Default diagnostic highlights
|
||||
--- @type table<vim.diagnostic.Severity, string>
|
||||
local severity_names = {
|
||||
[severity.ERROR] = 'Error',
|
||||
[severity.WARN] = 'Warn',
|
||||
[severity.INFO] = 'Info',
|
||||
[severity.HINT] = 'Hint',
|
||||
}
|
||||
|
||||
--- @param base_name string
|
||||
--- @return table<vim.diagnostic.Severity, string>
|
||||
local function make_highlight_map(base_name)
|
||||
local result = {} --- @type table<vim.diagnostic.Severity, string>
|
||||
|
||||
for level, name in pairs(severity_names) do
|
||||
result[level] = ('Diagnostic%s%s'):format(base_name, name)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local sign_highlight_map = make_highlight_map('Sign')
|
||||
local underline_highlight_map = make_highlight_map('Underline')
|
||||
local virtual_text_highlight_map = make_highlight_map('VirtualText')
|
||||
local virtual_lines_highlight_map = make_highlight_map('VirtualLines')
|
||||
|
||||
-- Metatable that automatically creates an empty table when assigning to a missing key
|
||||
local bufnr_and_namespace_cacher_mt = {
|
||||
--- @param t table<integer, table>
|
||||
--- @param bufnr integer
|
||||
--- @return table
|
||||
__index = function(t, bufnr)
|
||||
assert(bufnr > 0, 'Invalid buffer number')
|
||||
t[bufnr] = {}
|
||||
return t[bufnr]
|
||||
end,
|
||||
}
|
||||
|
||||
--- @type table<integer, table<integer, vim.diagnostic._handlers._extmark[]>>
|
||||
local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt)
|
||||
|
||||
--- @type table<integer, true>
|
||||
local diagnostic_attached_buffers = {}
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param last integer
|
||||
local function restore_extmarks(bufnr, last)
|
||||
for ns, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do
|
||||
local extmarks_current = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, { details = true })
|
||||
local found = {} --- @type table<integer, true>
|
||||
|
||||
for _, extmark in ipairs(extmarks_current) do
|
||||
-- nvim_buf_set_lines will move any extmark to the line after the last
|
||||
-- nvim_buf_set_text will move any extmark to the last line
|
||||
if extmark[2] ~= last + 1 then
|
||||
found[extmark[1]] = true
|
||||
end
|
||||
end
|
||||
|
||||
for _, extmark in ipairs(extmarks) do
|
||||
if not found[extmark[1]] then
|
||||
local opts = extmark[4]
|
||||
--- @diagnostic disable-next-line: inject-field
|
||||
opts.id = extmark[1]
|
||||
pcall(api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr? integer
|
||||
local function save_extmarks(namespace, bufnr)
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
|
||||
if not diagnostic_attached_buffers[bufnr] then
|
||||
api.nvim_buf_attach(bufnr, false, {
|
||||
on_lines = function(_, _, _, _, _, last)
|
||||
restore_extmarks(bufnr, last - 1)
|
||||
end,
|
||||
on_detach = function()
|
||||
diagnostic_cache_extmarks[bufnr] = nil
|
||||
end,
|
||||
})
|
||||
diagnostic_attached_buffers[bufnr] = true
|
||||
end
|
||||
|
||||
diagnostic_cache_extmarks[bufnr][namespace] =
|
||||
api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, { details = true })
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param namespace integer
|
||||
local function clear_extmarks(bufnr, namespace)
|
||||
diagnostic_cache_extmarks[bufnr][namespace] = {}
|
||||
if api.nvim_buf_is_valid(bufnr) then
|
||||
api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param fn fun()
|
||||
--- @return integer?
|
||||
local function once_buf_loaded(bufnr, fn)
|
||||
if api.nvim_buf_is_loaded(bufnr) then
|
||||
fn()
|
||||
else
|
||||
return api.nvim_create_autocmd('BufRead', {
|
||||
buffer = bufnr,
|
||||
once = true,
|
||||
callback = function()
|
||||
fn()
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
--- @param autocmd_key string
|
||||
--- @param ns vim.diagnostic.NS
|
||||
local function cleanup_show_autocmd(autocmd_key, ns)
|
||||
if ns.user_data[autocmd_key] then
|
||||
api.nvim_del_autocmd(ns.user_data[autocmd_key])
|
||||
--- @type integer?
|
||||
ns.user_data[autocmd_key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- @param autocmd_key string
|
||||
--- @param ns vim.diagnostic.NS
|
||||
--- @param bufnr integer
|
||||
--- @param fn fun()
|
||||
local function show_once_loaded(autocmd_key, ns, bufnr, fn)
|
||||
cleanup_show_autocmd(autocmd_key, ns)
|
||||
|
||||
--- @type integer?
|
||||
ns.user_data[autocmd_key] = once_buf_loaded(bufnr, function()
|
||||
--- @type integer?
|
||||
ns.user_data[autocmd_key] = nil
|
||||
fn()
|
||||
end)
|
||||
end
|
||||
|
||||
--- @param priority integer
|
||||
--- @param opts? { severity_sort?: {reverse?:boolean} }
|
||||
--- @return fun(severity: vim.diagnostic.Severity): integer
|
||||
local function severity_to_extmark_priority(priority, opts)
|
||||
opts = opts or {}
|
||||
if opts.severity_sort then
|
||||
if type(opts.severity_sort) == 'table' and opts.severity_sort.reverse then
|
||||
return function(level)
|
||||
return priority + (level - severity.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
return function(level)
|
||||
return priority + (severity.HINT - level)
|
||||
end
|
||||
end
|
||||
|
||||
return function()
|
||||
return priority
|
||||
end
|
||||
end
|
||||
|
||||
M.signs = {}
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
--- @param diagnostics vim.Diagnostic[]
|
||||
--- @param opts? vim.diagnostic.OptsResolved
|
||||
function M.signs.show(namespace, bufnr, diagnostics, opts)
|
||||
vim.validate('namespace', namespace, 'number')
|
||||
vim.validate('bufnr', bufnr, 'number')
|
||||
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
vim.validate('opts.signs', (opts and opts or {}).signs, 'table', true)
|
||||
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
|
||||
local sopts = opts and opts.signs or {}
|
||||
local ns = diagnostic.get_namespace(namespace)
|
||||
show_once_loaded('sign_show_autocmd', ns, bufnr, function()
|
||||
-- 10 is the default sign priority when none is explicitly specified
|
||||
local priority = sopts.priority or 10
|
||||
local get_priority = severity_to_extmark_priority(priority, opts)
|
||||
|
||||
if not ns.user_data.sign_ns then
|
||||
ns.user_data.sign_ns =
|
||||
api.nvim_create_namespace(string.format('nvim.%s.diagnostic.signs', ns.name))
|
||||
end
|
||||
|
||||
local text = {} --- @type table<vim.diagnostic.Severity|string, string>
|
||||
for level in pairs(severity) do
|
||||
if sopts.text and sopts.text[level] then
|
||||
text[level] = sopts.text[level]
|
||||
elseif type(level) == 'string' and not text[level] then
|
||||
text[level] = level:sub(1, 1):upper()
|
||||
end
|
||||
end
|
||||
|
||||
local numhl = sopts.numhl or {}
|
||||
local linehl = sopts.linehl or {}
|
||||
local line_count = api.nvim_buf_line_count(bufnr)
|
||||
|
||||
for _, diagnostic0 in ipairs(diagnostics) do
|
||||
if diagnostic0.lnum <= line_count then
|
||||
api.nvim_buf_set_extmark(bufnr, ns.user_data.sign_ns, diagnostic0.lnum, 0, {
|
||||
sign_text = text[diagnostic0.severity] or text[severity[diagnostic0.severity]] or 'U',
|
||||
sign_hl_group = sign_highlight_map[diagnostic0.severity],
|
||||
number_hl_group = numhl[diagnostic0.severity],
|
||||
line_hl_group = linehl[diagnostic0.severity],
|
||||
priority = get_priority(diagnostic0.severity),
|
||||
})
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
function M.signs.hide(namespace, bufnr)
|
||||
local ns = diagnostic.get_namespace(namespace)
|
||||
cleanup_show_autocmd('sign_show_autocmd', ns)
|
||||
if ns.user_data.sign_ns and api.nvim_buf_is_valid(bufnr) then
|
||||
api.nvim_buf_clear_namespace(bufnr, ns.user_data.sign_ns, 0, -1)
|
||||
end
|
||||
end
|
||||
|
||||
M.underline = {}
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
--- @param diagnostics vim.Diagnostic[]
|
||||
--- @param opts? vim.diagnostic.OptsResolved
|
||||
function M.underline.show(namespace, bufnr, diagnostics, opts)
|
||||
vim.validate('namespace', namespace, 'number')
|
||||
vim.validate('bufnr', bufnr, 'number')
|
||||
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
|
||||
local ns = diagnostic.get_namespace(namespace)
|
||||
show_once_loaded('underline_show_autocmd', ns, bufnr, function()
|
||||
if not ns.user_data.underline_ns then
|
||||
ns.user_data.underline_ns =
|
||||
api.nvim_create_namespace(string.format('nvim.%s.diagnostic.underline', ns.name))
|
||||
end
|
||||
|
||||
local underline_ns = ns.user_data.underline_ns
|
||||
local get_priority = severity_to_extmark_priority(vim.hl.priorities.diagnostics, opts)
|
||||
|
||||
for _, diagnostic0 in ipairs(diagnostics) do
|
||||
local higroups = { underline_highlight_map[diagnostic0.severity] }
|
||||
|
||||
if diagnostic0._tags then
|
||||
if diagnostic0._tags.unnecessary then
|
||||
table.insert(higroups, 'DiagnosticUnnecessary')
|
||||
end
|
||||
if diagnostic0._tags.deprecated then
|
||||
table.insert(higroups, 'DiagnosticDeprecated')
|
||||
end
|
||||
end
|
||||
|
||||
local lines =
|
||||
api.nvim_buf_get_lines(diagnostic0.bufnr, diagnostic0.lnum, diagnostic0.lnum + 1, true)
|
||||
|
||||
for _, higroup in ipairs(higroups) do
|
||||
vim.hl.range(
|
||||
bufnr,
|
||||
underline_ns,
|
||||
higroup,
|
||||
{ diagnostic0.lnum, math.min(diagnostic0.col, #lines[1] - 1) },
|
||||
{ diagnostic0.end_lnum, diagnostic0.end_col },
|
||||
{ priority = get_priority(diagnostic0.severity) }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
save_extmarks(underline_ns, bufnr)
|
||||
end)
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
function M.underline.hide(namespace, bufnr)
|
||||
local ns = diagnostic.get_namespace(namespace)
|
||||
cleanup_show_autocmd('underline_show_autocmd', ns)
|
||||
if ns.user_data.underline_ns then
|
||||
clear_extmarks(bufnr, ns.user_data.underline_ns)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param line_diags table<integer, vim.Diagnostic>
|
||||
--- @param opts vim.diagnostic.Opts.VirtualText
|
||||
--- @return [string, any][]?
|
||||
local function get_virt_text_chunks(line_diags, opts)
|
||||
if #line_diags == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
opts = opts or {}
|
||||
local prefix = opts.prefix or '■'
|
||||
local suffix = opts.suffix or ''
|
||||
local spacing = opts.spacing or 4
|
||||
|
||||
-- Create a little more space between virtual text and contents
|
||||
local virt_texts = { { string.rep(' ', spacing) } }
|
||||
|
||||
for i = 1, #line_diags do
|
||||
local resolved_prefix = prefix
|
||||
if type(prefix) == 'function' then
|
||||
resolved_prefix = prefix(line_diags[i], i, #line_diags) or ''
|
||||
end
|
||||
|
||||
table.insert(
|
||||
virt_texts,
|
||||
{ resolved_prefix, virtual_text_highlight_map[line_diags[i].severity] }
|
||||
)
|
||||
end
|
||||
|
||||
local last = line_diags[#line_diags]
|
||||
-- TODO(tjdevries): Allow different servers to be shown first somehow?
|
||||
-- TODO(tjdevries): Display server name associated with these?
|
||||
if last.message then
|
||||
if type(suffix) == 'function' then
|
||||
suffix = suffix(last) or ''
|
||||
end
|
||||
|
||||
table.insert(virt_texts, {
|
||||
string.format(' %s%s', last.message:gsub('\r', ''):gsub('\n', ' '), suffix),
|
||||
virtual_text_highlight_map[last.severity],
|
||||
})
|
||||
|
||||
return virt_texts
|
||||
end
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
--- @param diagnostics table<integer, vim.Diagnostic[]>
|
||||
--- @param opts vim.diagnostic.Opts.VirtualText
|
||||
local function render_virtual_text(namespace, bufnr, diagnostics, opts)
|
||||
local lnum = api.nvim_win_get_cursor(0)[1] - 1
|
||||
local buf_len = api.nvim_buf_line_count(bufnr)
|
||||
api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
|
||||
|
||||
--- @param line integer
|
||||
--- @return boolean
|
||||
local function should_render(line)
|
||||
if
|
||||
line >= buf_len
|
||||
or (opts.current_line == true and line ~= lnum)
|
||||
or (opts.current_line == false and line == lnum)
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
for line, line_diagnostics in pairs(diagnostics) do
|
||||
if should_render(line) then
|
||||
local virt_texts = get_virt_text_chunks(line_diagnostics, opts)
|
||||
if virt_texts then
|
||||
api.nvim_buf_set_extmark(bufnr, namespace, line, 0, {
|
||||
hl_mode = opts.hl_mode or 'combine',
|
||||
virt_text = virt_texts,
|
||||
virt_text_pos = opts.virt_text_pos,
|
||||
virt_text_hide = opts.virt_text_hide,
|
||||
virt_text_win_col = opts.virt_text_win_col,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
M.virtual_text = {}
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
--- @param diagnostics vim.Diagnostic[]
|
||||
--- @param opts? vim.diagnostic.OptsResolved
|
||||
function M.virtual_text.show(namespace, bufnr, diagnostics, opts)
|
||||
vim.validate('namespace', namespace, 'number')
|
||||
vim.validate('bufnr', bufnr, 'number')
|
||||
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
local vopts = opts and opts.virtual_text or {}
|
||||
|
||||
local ns = diagnostic.get_namespace(namespace)
|
||||
show_once_loaded('virtual_text_show_autocmd', ns, bufnr, function()
|
||||
if vopts.format then
|
||||
diagnostics = diagnostic_shared.reformat_diagnostics(vopts.format, diagnostics)
|
||||
end
|
||||
|
||||
if
|
||||
vopts.source and (vopts.source ~= 'if_many' or diagnostic_shared.count_sources(bufnr) > 1)
|
||||
then
|
||||
diagnostics = diagnostic_shared.prefix_source(diagnostics)
|
||||
end
|
||||
|
||||
if not ns.user_data.virt_text_ns then
|
||||
ns.user_data.virt_text_ns =
|
||||
api.nvim_create_namespace(string.format('nvim.%s.diagnostic.virtual_text', ns.name))
|
||||
end
|
||||
if not ns.user_data.virt_text_augroup then
|
||||
ns.user_data.virt_text_augroup = api.nvim_create_augroup(
|
||||
string.format('nvim.%s.diagnostic.virt_text', ns.name),
|
||||
{ clear = true }
|
||||
)
|
||||
end
|
||||
|
||||
api.nvim_clear_autocmds({ group = ns.user_data.virt_text_augroup, buffer = bufnr })
|
||||
|
||||
local line_diagnostics = diagnostic_shared.diagnostic_lines(diagnostics, true)
|
||||
|
||||
if vopts.current_line ~= nil then
|
||||
api.nvim_create_autocmd('CursorMoved', {
|
||||
buffer = bufnr,
|
||||
group = ns.user_data.virt_text_augroup,
|
||||
callback = function()
|
||||
render_virtual_text(ns.user_data.virt_text_ns, bufnr, line_diagnostics, vopts)
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
render_virtual_text(ns.user_data.virt_text_ns, bufnr, line_diagnostics, vopts)
|
||||
save_extmarks(ns.user_data.virt_text_ns, bufnr)
|
||||
end)
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
function M.virtual_text.hide(namespace, bufnr)
|
||||
local ns = diagnostic.get_namespace(namespace)
|
||||
cleanup_show_autocmd('virtual_text_show_autocmd', ns)
|
||||
if ns.user_data.virt_text_ns then
|
||||
clear_extmarks(bufnr, ns.user_data.virt_text_ns)
|
||||
if api.nvim_buf_is_valid(bufnr) then
|
||||
api.nvim_clear_autocmds({ group = ns.user_data.virt_text_augroup, buffer = bufnr })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param lnum integer
|
||||
--- @param start_col integer
|
||||
--- @param end_col integer
|
||||
--- @return integer
|
||||
local function distance_between_cols(bufnr, lnum, start_col, end_col)
|
||||
return api.nvim_buf_call(bufnr, function()
|
||||
local s = vim.fn.virtcol({ lnum + 1, start_col })
|
||||
local e = vim.fn.virtcol({ lnum + 1, end_col + 1 })
|
||||
return e - 1 - s
|
||||
end)
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
--- @param diagnostics vim.Diagnostic[]
|
||||
local function render_virtual_lines(namespace, bufnr, diagnostics)
|
||||
table.sort(diagnostics, function(d1, d2)
|
||||
return diagnostic_shared.diagnostic_cmp(d1, d2, 'lnum', false)
|
||||
end)
|
||||
|
||||
api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
|
||||
|
||||
if not next(diagnostics) then
|
||||
return
|
||||
end
|
||||
|
||||
-- This loop reads each line, putting them into stacks with some extra data since
|
||||
-- rendering each line requires understanding what is beneath it.
|
||||
local ElementType = { Space = 1, Diagnostic = 2, Overlap = 3, Blank = 4 } --- @enum ElementType
|
||||
--- @type table<integer, [ElementType, string|vim.diagnostic.Severity|vim.Diagnostic][]>
|
||||
local line_stacks = {}
|
||||
--- @type table<integer, integer>
|
||||
local line_anchor = {}
|
||||
local prev_lnum = -1
|
||||
local prev_col = 0
|
||||
|
||||
for _, diag in ipairs(diagnostics) do
|
||||
if not line_stacks[diag.lnum] then
|
||||
line_stacks[diag.lnum] = {}
|
||||
end
|
||||
|
||||
local stack = line_stacks[diag.lnum]
|
||||
local end_lnum = diag.end_lnum or diag.lnum
|
||||
if not line_anchor[diag.lnum] or end_lnum > line_anchor[diag.lnum] then
|
||||
line_anchor[diag.lnum] = end_lnum
|
||||
end
|
||||
|
||||
if diag.lnum ~= prev_lnum then
|
||||
table.insert(stack, {
|
||||
ElementType.Space,
|
||||
string.rep(' ', distance_between_cols(bufnr, diag.lnum, 0, diag.col)),
|
||||
})
|
||||
elseif diag.col ~= prev_col then
|
||||
table.insert(stack, {
|
||||
ElementType.Space,
|
||||
-- +1 because indexing starts at 0 in one API but at 1 in the other.
|
||||
string.rep(' ', distance_between_cols(bufnr, diag.lnum, prev_col + 1, diag.col)),
|
||||
})
|
||||
else
|
||||
table.insert(stack, { ElementType.Overlap, diag.severity })
|
||||
end
|
||||
|
||||
if diag.message:find('^%s*$') then
|
||||
table.insert(stack, { ElementType.Blank, diag })
|
||||
else
|
||||
table.insert(stack, { ElementType.Diagnostic, diag })
|
||||
end
|
||||
|
||||
prev_lnum, prev_col = diag.lnum, diag.col
|
||||
end
|
||||
|
||||
local chars = {
|
||||
cross = '┼',
|
||||
horizontal = '─',
|
||||
horizontal_up = '┴',
|
||||
up_right = '└',
|
||||
vertical = '│',
|
||||
vertical_right = '├',
|
||||
}
|
||||
|
||||
for lnum, stack in pairs(line_stacks) do
|
||||
local virt_lines = {}
|
||||
|
||||
-- Note that we read in the order opposite to insertion.
|
||||
for i = #stack, 1, -1 do
|
||||
if stack[i][1] == ElementType.Diagnostic then
|
||||
local diagnostic0 = stack[i][2]
|
||||
local left = {} --- @type [string, string]
|
||||
local overlap = false
|
||||
local multi = false
|
||||
|
||||
-- Iterate the stack for this line to find elements on the left.
|
||||
for j = 1, i - 1 do
|
||||
local element_type = stack[j][1]
|
||||
local data = stack[j][2]
|
||||
if element_type == ElementType.Space then
|
||||
if multi then
|
||||
--- @cast data string
|
||||
table.insert(left, {
|
||||
string.rep(chars.horizontal, data:len()),
|
||||
virtual_lines_highlight_map[diagnostic0.severity],
|
||||
})
|
||||
else
|
||||
table.insert(left, { data, '' })
|
||||
end
|
||||
elseif element_type == ElementType.Diagnostic then
|
||||
-- If an overlap follows this line, don't add an extra column.
|
||||
if stack[j + 1][1] ~= ElementType.Overlap then
|
||||
table.insert(left, { chars.vertical, virtual_lines_highlight_map[data.severity] })
|
||||
end
|
||||
overlap = false
|
||||
elseif element_type == ElementType.Blank then
|
||||
if multi then
|
||||
table.insert(
|
||||
left,
|
||||
{ chars.horizontal_up, virtual_lines_highlight_map[data.severity] }
|
||||
)
|
||||
else
|
||||
table.insert(left, { chars.up_right, virtual_lines_highlight_map[data.severity] })
|
||||
end
|
||||
multi = true
|
||||
elseif element_type == ElementType.Overlap then
|
||||
overlap = true
|
||||
end
|
||||
end
|
||||
|
||||
local center_char --- @type string
|
||||
if overlap and multi then
|
||||
center_char = chars.cross
|
||||
elseif overlap then
|
||||
center_char = chars.vertical_right
|
||||
elseif multi then
|
||||
center_char = chars.horizontal_up
|
||||
else
|
||||
center_char = chars.up_right
|
||||
end
|
||||
|
||||
local center = {
|
||||
{
|
||||
string.format('%s%s', center_char, string.rep(chars.horizontal, 4) .. ' '),
|
||||
virtual_lines_highlight_map[diagnostic0.severity],
|
||||
},
|
||||
}
|
||||
|
||||
-- We can draw on the left side if and only if:
|
||||
-- a. Is the last one stacked this line.
|
||||
-- b. Has enough space on the left.
|
||||
-- c. Is just one line.
|
||||
-- d. Is not an overlap.
|
||||
for msg_line in diagnostic0.message:gmatch('([^\n]+)') do
|
||||
local vline = {}
|
||||
vim.list_extend(vline, left)
|
||||
vim.list_extend(vline, center)
|
||||
vim.list_extend(vline, {
|
||||
{ msg_line, virtual_lines_highlight_map[diagnostic0.severity] },
|
||||
})
|
||||
|
||||
table.insert(virt_lines, vline)
|
||||
|
||||
-- Special-case for continuation lines:
|
||||
if overlap then
|
||||
center = {
|
||||
{ chars.vertical, virtual_lines_highlight_map[diagnostic0.severity] },
|
||||
{ ' ', '' },
|
||||
}
|
||||
else
|
||||
center = { { ' ', '' } }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
api.nvim_buf_set_extmark(bufnr, namespace, line_anchor[lnum] or lnum, 0, {
|
||||
virt_lines_overflow = 'scroll',
|
||||
virt_lines = virt_lines,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
--- @param diagnostic0 vim.Diagnostic
|
||||
--- @return string
|
||||
local function format_virtual_lines(diagnostic0)
|
||||
if diagnostic0.code then
|
||||
return string.format('%s: %s', diagnostic0.code, diagnostic0.message)
|
||||
end
|
||||
|
||||
return diagnostic0.message
|
||||
end
|
||||
|
||||
M.virtual_lines = {}
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
--- @param diagnostics vim.Diagnostic[]
|
||||
--- @param opts? vim.diagnostic.OptsResolved
|
||||
function M.virtual_lines.show(namespace, bufnr, diagnostics, opts)
|
||||
vim.validate('namespace', namespace, 'number')
|
||||
vim.validate('bufnr', bufnr, 'number')
|
||||
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
local vopts = opts and opts.virtual_lines or {}
|
||||
|
||||
local ns = diagnostic.get_namespace(namespace)
|
||||
show_once_loaded('virtual_lines_show_autocmd', ns, bufnr, function()
|
||||
if not ns.user_data.virt_lines_ns then
|
||||
ns.user_data.virt_lines_ns =
|
||||
api.nvim_create_namespace(string.format('nvim.%s.diagnostic.virtual_lines', ns.name))
|
||||
end
|
||||
if not ns.user_data.virt_lines_augroup then
|
||||
ns.user_data.virt_lines_augroup = api.nvim_create_augroup(
|
||||
string.format('nvim.%s.diagnostic.virt_lines', ns.name),
|
||||
{ clear = true }
|
||||
)
|
||||
end
|
||||
|
||||
api.nvim_clear_autocmds({ group = ns.user_data.virt_lines_augroup, buffer = bufnr })
|
||||
|
||||
diagnostics =
|
||||
diagnostic_shared.reformat_diagnostics(vopts.format or format_virtual_lines, diagnostics)
|
||||
|
||||
if vopts.current_line == true then
|
||||
-- Create a mapping from line -> diagnostics so that we can quickly get the
|
||||
-- diagnostics we need when the cursor line doesn't change.
|
||||
local line_diagnostics = diagnostic_shared.diagnostic_lines(diagnostics, true)
|
||||
api.nvim_create_autocmd('CursorMoved', {
|
||||
buffer = bufnr,
|
||||
group = ns.user_data.virt_lines_augroup,
|
||||
callback = function()
|
||||
render_virtual_lines(
|
||||
ns.user_data.virt_lines_ns,
|
||||
bufnr,
|
||||
diagnostic_shared.diagnostics_at_cursor(line_diagnostics)
|
||||
)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Also show diagnostics for the current line before the first CursorMoved event.
|
||||
render_virtual_lines(
|
||||
ns.user_data.virt_lines_ns,
|
||||
bufnr,
|
||||
diagnostic_shared.diagnostics_at_cursor(line_diagnostics)
|
||||
)
|
||||
else
|
||||
render_virtual_lines(ns.user_data.virt_lines_ns, bufnr, diagnostics)
|
||||
end
|
||||
|
||||
save_extmarks(ns.user_data.virt_lines_ns, bufnr)
|
||||
end)
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
function M.virtual_lines.hide(namespace, bufnr)
|
||||
local ns = diagnostic.get_namespace(namespace)
|
||||
cleanup_show_autocmd('virtual_lines_show_autocmd', ns)
|
||||
if ns.user_data.virt_lines_ns then
|
||||
clear_extmarks(bufnr, ns.user_data.virt_lines_ns)
|
||||
if api.nvim_buf_is_valid(bufnr) then
|
||||
api.nvim_clear_autocmds({ group = ns.user_data.virt_lines_augroup, buffer = bufnr })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
289
runtime/lua/vim/diagnostic/_jump.lua
Normal file
289
runtime/lua/vim/diagnostic/_jump.lua
Normal file
@@ -0,0 +1,289 @@
|
||||
local api, if_nil = vim.api, vim.F.if_nil
|
||||
local shared = require('vim.diagnostic._shared')
|
||||
local store = require('vim.diagnostic._store')
|
||||
|
||||
--- @class (private) vim.diagnostic._JumpOpts : vim.diagnostic.JumpOpts
|
||||
--- @field _highest? boolean
|
||||
--- @field win_id? integer
|
||||
--- @field cursor_position? [integer, integer]
|
||||
--- @field float? table|boolean
|
||||
|
||||
--- @class (private) vim.diagnostic._jump
|
||||
local M = {}
|
||||
|
||||
--- @param diagnostics vim.Diagnostic[]
|
||||
local function filter_highest(diagnostics)
|
||||
table.sort(diagnostics, function(a, b)
|
||||
return shared.diagnostic_cmp(a, b, 'severity', false)
|
||||
end)
|
||||
|
||||
-- Find the first diagnostic where the severity does not match the highest severity, and remove
|
||||
-- that element and all subsequent elements from the array
|
||||
local worst = (diagnostics[1] or {}).severity
|
||||
local len = #diagnostics
|
||||
for i = 2, len do
|
||||
if diagnostics[i].severity ~= worst then
|
||||
for j = i, len do
|
||||
diagnostics[j] = nil
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param search_forward boolean
|
||||
--- @param opts vim.diagnostic.JumpOpts?
|
||||
--- @param use_logical_pos boolean
|
||||
--- @return vim.Diagnostic?
|
||||
local function next_diagnostic(search_forward, opts, use_logical_pos)
|
||||
opts = opts or {}
|
||||
--- @cast opts vim.diagnostic._JumpOpts
|
||||
|
||||
-- Support deprecated win_id alias
|
||||
if opts.win_id then
|
||||
vim.deprecate('opts.win_id', 'opts.winid', '0.13')
|
||||
opts.winid = opts.win_id
|
||||
opts.win_id = nil --- @diagnostic disable-line
|
||||
end
|
||||
|
||||
-- Support deprecated cursor_position alias
|
||||
if opts.cursor_position then
|
||||
vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
|
||||
opts.pos = opts.cursor_position
|
||||
opts.cursor_position = nil --- @diagnostic disable-line
|
||||
end
|
||||
|
||||
local winid = opts.winid or api.nvim_get_current_win()
|
||||
local bufnr = api.nvim_win_get_buf(winid)
|
||||
local position = opts.pos or api.nvim_win_get_cursor(winid)
|
||||
|
||||
-- Adjust row to be 0-indexed
|
||||
position[1] = position[1] - 1
|
||||
|
||||
local wrap = if_nil(opts.wrap, true)
|
||||
local diagnostics = store.get_diagnostics(bufnr, opts, true)
|
||||
|
||||
if opts._highest then
|
||||
filter_highest(diagnostics)
|
||||
end
|
||||
|
||||
local line_diagnostics = shared.diagnostic_lines(diagnostics, use_logical_pos)
|
||||
|
||||
--- @param diagnostic vim.Diagnostic
|
||||
--- @return integer
|
||||
local function col_fn(diagnostic)
|
||||
return use_logical_pos and select(2, shared.get_logical_pos(diagnostic)) or diagnostic.col
|
||||
end
|
||||
|
||||
local line_count = api.nvim_buf_line_count(bufnr)
|
||||
for i = 0, line_count do
|
||||
local offset = i * (search_forward and 1 or -1)
|
||||
local lnum = position[1] + offset
|
||||
if lnum < 0 or lnum >= line_count then
|
||||
if not wrap then
|
||||
return
|
||||
end
|
||||
lnum = (lnum + line_count) % line_count
|
||||
end
|
||||
|
||||
if line_diagnostics[lnum] and not vim.tbl_isempty(line_diagnostics[lnum]) then
|
||||
local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
|
||||
local sort_diagnostics, is_next --- @type function, function
|
||||
if search_forward then
|
||||
sort_diagnostics = function(a, b)
|
||||
return shared.diagnostic_cmp(a, b, 'col', false, col_fn)
|
||||
end
|
||||
is_next = function(diagnostic)
|
||||
return math.min(col_fn(diagnostic), math.max(line_length - 1, 0)) > position[2]
|
||||
end
|
||||
else
|
||||
sort_diagnostics = function(a, b)
|
||||
return shared.diagnostic_cmp(a, b, 'col', true, col_fn)
|
||||
end
|
||||
is_next = function(diagnostic)
|
||||
return math.min(col_fn(diagnostic), math.max(line_length - 1, 0)) < position[2]
|
||||
end
|
||||
end
|
||||
|
||||
table.sort(line_diagnostics[lnum], sort_diagnostics)
|
||||
if i == 0 then
|
||||
for _, diagnostic in ipairs(line_diagnostics[lnum]) do
|
||||
if is_next(diagnostic) then
|
||||
return diagnostic
|
||||
end
|
||||
end
|
||||
else
|
||||
return line_diagnostics[lnum][1]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- @param diagnostic vim.Diagnostic?
|
||||
--- @param opts vim.diagnostic.JumpOpts?
|
||||
local function goto_diagnostic(diagnostic, opts)
|
||||
if not diagnostic then
|
||||
api.nvim_echo({ { 'No more valid diagnostics to move to', 'WarningMsg' } }, true, {})
|
||||
return
|
||||
end
|
||||
|
||||
opts = opts or {}
|
||||
--- @cast opts vim.diagnostic._JumpOpts
|
||||
|
||||
-- Support deprecated win_id alias
|
||||
if opts.win_id then
|
||||
vim.deprecate('opts.win_id', 'opts.winid', '0.13')
|
||||
opts.winid = opts.win_id
|
||||
opts.win_id = nil --- @diagnostic disable-line
|
||||
end
|
||||
|
||||
local winid = opts.winid or api.nvim_get_current_win()
|
||||
local lnum, col = shared.get_logical_pos(diagnostic)
|
||||
|
||||
vim._with({ win = winid }, function()
|
||||
-- Save position in the window's jumplist
|
||||
vim.cmd("normal! m'")
|
||||
api.nvim_win_set_cursor(winid, { lnum + 1, col })
|
||||
-- Open folds under the cursor
|
||||
vim.cmd('normal! zv')
|
||||
end)
|
||||
|
||||
if opts.float then
|
||||
vim.deprecate('opts.float', 'opts.on_jump', '0.14')
|
||||
local float_opts = opts.float
|
||||
float_opts = type(float_opts) == 'table' and float_opts or {}
|
||||
|
||||
opts.on_jump = function(_, bufnr)
|
||||
vim.diagnostic.open_float(vim.tbl_extend('keep', float_opts, {
|
||||
bufnr = bufnr,
|
||||
scope = 'cursor',
|
||||
focus = false,
|
||||
}))
|
||||
end
|
||||
|
||||
opts.float = nil --- @diagnostic disable-line
|
||||
end
|
||||
|
||||
if opts.on_jump then
|
||||
vim.schedule(function()
|
||||
opts.on_jump(diagnostic, api.nvim_win_get_buf(winid))
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param opts? vim.diagnostic.JumpOpts
|
||||
--- @return vim.Diagnostic?
|
||||
function M.get_prev(opts)
|
||||
return next_diagnostic(false, opts, false)
|
||||
end
|
||||
|
||||
--- @param opts? vim.diagnostic.JumpOpts
|
||||
--- @return table|false
|
||||
function M.get_prev_pos(opts)
|
||||
vim.deprecate(
|
||||
'vim.diagnostic.get_prev_pos()',
|
||||
'access the lnum and col fields from get_prev() instead',
|
||||
'0.13'
|
||||
)
|
||||
local prev = M.get_prev(opts)
|
||||
if not prev then
|
||||
return false
|
||||
end
|
||||
|
||||
return { prev.lnum, prev.col }
|
||||
end
|
||||
|
||||
--- @param opts? vim.diagnostic.JumpOpts
|
||||
function M.goto_prev(opts)
|
||||
vim.deprecate('vim.diagnostic.goto_prev()', 'vim.diagnostic.jump()', '0.13')
|
||||
opts = opts or {}
|
||||
opts.float = if_nil(opts.float, true) --- @diagnostic disable-line
|
||||
goto_diagnostic(M.get_prev(opts), opts)
|
||||
end
|
||||
|
||||
--- @param opts? vim.diagnostic.JumpOpts
|
||||
--- @return vim.Diagnostic?
|
||||
function M.get_next(opts)
|
||||
return next_diagnostic(true, opts, false)
|
||||
end
|
||||
|
||||
--- @param opts? vim.diagnostic.JumpOpts
|
||||
--- @return table|false
|
||||
function M.get_next_pos(opts)
|
||||
vim.deprecate(
|
||||
'vim.diagnostic.get_next_pos()',
|
||||
'access the lnum and col fields from get_next() instead',
|
||||
'0.13'
|
||||
)
|
||||
local next = M.get_next(opts)
|
||||
if not next then
|
||||
return false
|
||||
end
|
||||
|
||||
return { next.lnum, next.col }
|
||||
end
|
||||
|
||||
--- @param opts vim.diagnostic.JumpOpts
|
||||
--- @return vim.Diagnostic?
|
||||
function M.jump(opts)
|
||||
vim.validate('opts', opts, 'table')
|
||||
|
||||
-- One of "diagnostic" or "count" must be provided
|
||||
assert(
|
||||
opts.diagnostic or opts.count,
|
||||
'One of "diagnostic" or "count" must be specified in the options to vim.diagnostic.jump()'
|
||||
)
|
||||
|
||||
-- Apply configuration options from vim.diagnostic.config()
|
||||
local config = assert(vim.diagnostic.config()).jump or {}
|
||||
opts = vim.tbl_deep_extend('keep', opts, config)
|
||||
--- @cast opts vim.diagnostic._JumpOpts
|
||||
|
||||
if opts.diagnostic then
|
||||
goto_diagnostic(opts.diagnostic, opts)
|
||||
return opts.diagnostic
|
||||
end
|
||||
|
||||
local count = opts.count
|
||||
if count == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Support deprecated cursor_position alias
|
||||
if opts.cursor_position then
|
||||
vim.deprecate('opts.cursor_position', 'opts.pos', '0.13')
|
||||
opts.pos = opts.cursor_position
|
||||
opts.cursor_position = nil --- @diagnostic disable-line
|
||||
end
|
||||
|
||||
local diagnostic --- @type vim.Diagnostic?
|
||||
while count ~= 0 do
|
||||
local next = next_diagnostic(count > 0, opts, true)
|
||||
if not next then
|
||||
break
|
||||
end
|
||||
|
||||
-- Update cursor position
|
||||
opts.pos = { next.lnum + 1, next.col }
|
||||
|
||||
if count > 0 then
|
||||
count = count - 1
|
||||
else
|
||||
count = count + 1
|
||||
end
|
||||
diagnostic = next
|
||||
end
|
||||
|
||||
goto_diagnostic(diagnostic, opts)
|
||||
return diagnostic
|
||||
end
|
||||
|
||||
--- @param opts? vim.diagnostic.JumpOpts
|
||||
function M.goto_next(opts)
|
||||
vim.deprecate('vim.diagnostic.goto_next()', 'vim.diagnostic.jump()', '0.13')
|
||||
opts = opts or {}
|
||||
opts.float = if_nil(opts.float, true) --- @diagnostic disable-line
|
||||
goto_diagnostic(M.get_next(opts), opts)
|
||||
end
|
||||
|
||||
return M
|
||||
109
runtime/lua/vim/diagnostic/_severity.lua
Normal file
109
runtime/lua/vim/diagnostic/_severity.lua
Normal file
@@ -0,0 +1,109 @@
|
||||
local severity = vim.diagnostic.severity
|
||||
|
||||
--- @class (private) vim.diagnostic._severity
|
||||
local M = {}
|
||||
|
||||
--- @param value string|vim.diagnostic.Severity?
|
||||
--- @return vim.diagnostic.Severity?
|
||||
function M.to_severity(value)
|
||||
if type(value) == 'string' then
|
||||
local ret = severity[value:upper()] --[[@as vim.diagnostic.Severity?]]
|
||||
if not ret then
|
||||
error(('Invalid severity: %s'):format(value))
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
return value --[[@as vim.diagnostic.Severity?]]
|
||||
end
|
||||
|
||||
--- @param filter vim.diagnostic.SeverityFilter
|
||||
--- @return fun(d: vim.Diagnostic):boolean
|
||||
function M.severity_predicate(filter)
|
||||
if type(filter) ~= 'table' then
|
||||
local severity0 = M.to_severity(filter)
|
||||
--- @param d vim.Diagnostic
|
||||
return function(d)
|
||||
return d.severity == severity0
|
||||
end
|
||||
end
|
||||
|
||||
--- @diagnostic disable-next-line: undefined-field
|
||||
if filter.min or filter.max then
|
||||
--- @cast filter {min:vim.diagnostic.Severity,max:vim.diagnostic.Severity}
|
||||
local min_severity = M.to_severity(filter.min) or severity.HINT
|
||||
local max_severity = M.to_severity(filter.max) or severity.ERROR
|
||||
|
||||
--- @param d vim.Diagnostic
|
||||
return function(d)
|
||||
return d.severity <= min_severity and d.severity >= max_severity
|
||||
end
|
||||
end
|
||||
|
||||
--- @cast filter vim.diagnostic.Severity[]
|
||||
local severities = {} --- @type table<vim.diagnostic.Severity,true>
|
||||
for _, s in ipairs(filter) do
|
||||
severities[assert(M.to_severity(s))] = true
|
||||
end
|
||||
|
||||
--- @param d vim.Diagnostic
|
||||
return function(d)
|
||||
return severities[d.severity]
|
||||
end
|
||||
end
|
||||
|
||||
--- @param filter vim.diagnostic.SeverityFilter?
|
||||
--- @param diagnostics vim.Diagnostic[]
|
||||
--- @return vim.Diagnostic[]
|
||||
function M.filter_by_severity(filter, diagnostics)
|
||||
if not filter then
|
||||
return diagnostics
|
||||
end
|
||||
|
||||
return vim.tbl_filter(M.severity_predicate(filter), diagnostics)
|
||||
end
|
||||
|
||||
--- Parse a diagnostic from a string.
|
||||
---
|
||||
--- @param str string String to parse diagnostics from.
|
||||
--- @param pat string Lua pattern with capture groups.
|
||||
--- @param groups string[] List of fields in a |vim.Diagnostic| structure to associate with captures from {pat}.
|
||||
--- @param severity_map table? A table mapping the severity field from {groups} with an item from |vim.diagnostic.severity|.
|
||||
--- @param defaults table? Table of default values for any fields not listed in {groups}.
|
||||
--- @return vim.Diagnostic?
|
||||
function M.match(str, pat, groups, severity_map, defaults)
|
||||
vim.validate('str', str, 'string')
|
||||
vim.validate('pat', pat, 'string')
|
||||
vim.validate('groups', groups, 'table')
|
||||
vim.validate('severity_map', severity_map, 'table', true)
|
||||
vim.validate('defaults', defaults, 'table', true)
|
||||
|
||||
--- @type table<string,vim.diagnostic.Severity>
|
||||
severity_map = severity_map or severity
|
||||
|
||||
local matches = { str:match(pat) } --- @type any[]
|
||||
if vim.tbl_isempty(matches) then
|
||||
return
|
||||
end
|
||||
|
||||
local diagnostic = {} --- @type table<string,any>
|
||||
for i, match in ipairs(matches) do
|
||||
local field = groups[i]
|
||||
if field == 'severity' then
|
||||
diagnostic[field] = severity_map[match]
|
||||
elseif field == 'lnum' or field == 'end_lnum' or field == 'col' or field == 'end_col' then
|
||||
diagnostic[field] = vim._assert_integer(match) - 1
|
||||
elseif field then
|
||||
diagnostic[field] = match
|
||||
end
|
||||
end
|
||||
|
||||
diagnostic = vim.tbl_extend('keep', diagnostic, defaults or {}) --- @type vim.Diagnostic
|
||||
diagnostic.severity = diagnostic.severity or severity.ERROR
|
||||
diagnostic.col = diagnostic.col or 0
|
||||
diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum
|
||||
diagnostic.end_col = diagnostic.end_col or diagnostic.col
|
||||
return diagnostic
|
||||
end
|
||||
|
||||
return M
|
||||
175
runtime/lua/vim/diagnostic/_shared.lua
Normal file
175
runtime/lua/vim/diagnostic/_shared.lua
Normal file
@@ -0,0 +1,175 @@
|
||||
local api = vim.api
|
||||
|
||||
local store = require('vim.diagnostic._store')
|
||||
|
||||
--- @class (private) vim.diagnostic._shared
|
||||
local M = {}
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @return integer
|
||||
function M.count_sources(bufnr)
|
||||
local count = 0
|
||||
local seen = {} --- @type table<string, true>
|
||||
for _, diagnostic in ipairs(store.get_diagnostics(bufnr, nil, false)) do
|
||||
local source = diagnostic.source
|
||||
if source and not seen[source] then
|
||||
seen[source] = true
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
--- @param diagnostics vim.Diagnostic[]
|
||||
--- @return vim.Diagnostic[]
|
||||
function M.prefix_source(diagnostics)
|
||||
--- @param diagnostic vim.Diagnostic
|
||||
return vim.tbl_map(function(diagnostic)
|
||||
if not diagnostic.source then
|
||||
return diagnostic
|
||||
end
|
||||
|
||||
local copied = vim.deepcopy(diagnostic, true)
|
||||
copied.message = string.format('%s: %s', diagnostic.source, diagnostic.message)
|
||||
return copied
|
||||
end, diagnostics)
|
||||
end
|
||||
|
||||
--- Get a position based on an extmark referenced by `_extmark_id` field
|
||||
--- @param diagnostic vim.Diagnostic
|
||||
--- @return integer lnum
|
||||
--- @return integer col
|
||||
--- @return integer end_lnum
|
||||
--- @return integer end_col
|
||||
--- @return boolean valid
|
||||
function M.get_logical_pos(diagnostic)
|
||||
if not diagnostic._extmark_id then
|
||||
return diagnostic.lnum, diagnostic.col, diagnostic.end_lnum, diagnostic.end_col, true
|
||||
end
|
||||
|
||||
local ns = vim.diagnostic.get_namespace(diagnostic.namespace)
|
||||
local extmark = api.nvim_buf_get_extmark_by_id(
|
||||
diagnostic.bufnr,
|
||||
ns.user_data.location_ns,
|
||||
diagnostic._extmark_id,
|
||||
{ details = true }
|
||||
)
|
||||
if next(extmark) == nil then
|
||||
return diagnostic.lnum, diagnostic.col, diagnostic.end_lnum, diagnostic.end_col, true
|
||||
end
|
||||
|
||||
return extmark[1], extmark[2], extmark[3].end_row, extmark[3].end_col, not extmark[3].invalid
|
||||
end
|
||||
|
||||
--- @param diagnostics vim.Diagnostic[]?
|
||||
--- @param use_logical_pos boolean
|
||||
--- @return table<integer, vim.Diagnostic[]>
|
||||
function M.diagnostic_lines(diagnostics, use_logical_pos)
|
||||
if not diagnostics then
|
||||
return {}
|
||||
end
|
||||
|
||||
local diagnostics_by_line = {} --- @type table<integer, vim.Diagnostic[]>
|
||||
for _, diagnostic in ipairs(diagnostics) do
|
||||
local lnum --- @type integer
|
||||
local valid --- @type boolean
|
||||
|
||||
if use_logical_pos then
|
||||
lnum, _, _, _, valid = M.get_logical_pos(diagnostic)
|
||||
else
|
||||
lnum, valid = diagnostic.lnum, true
|
||||
end
|
||||
|
||||
if valid then
|
||||
local line_diagnostics = diagnostics_by_line[lnum]
|
||||
if not line_diagnostics then
|
||||
line_diagnostics = {}
|
||||
diagnostics_by_line[lnum] = line_diagnostics
|
||||
end
|
||||
line_diagnostics[#line_diagnostics + 1] = diagnostic
|
||||
end
|
||||
end
|
||||
return diagnostics_by_line
|
||||
end
|
||||
|
||||
--- @param diagnostics table<integer, vim.Diagnostic[]>
|
||||
--- @return vim.Diagnostic[]
|
||||
function M.diagnostics_at_cursor(diagnostics)
|
||||
local lnum = api.nvim_win_get_cursor(0)[1] - 1
|
||||
|
||||
if diagnostics[lnum] ~= nil then
|
||||
return diagnostics[lnum]
|
||||
end
|
||||
|
||||
local cursor_diagnostics = {} --- @type vim.Diagnostic[]
|
||||
for _, line_diags in pairs(diagnostics) do
|
||||
for _, diagnostic in ipairs(line_diags) do
|
||||
if diagnostic.end_lnum and lnum >= diagnostic.lnum and lnum <= diagnostic.end_lnum then
|
||||
cursor_diagnostics[#cursor_diagnostics + 1] = diagnostic
|
||||
end
|
||||
end
|
||||
end
|
||||
return cursor_diagnostics
|
||||
end
|
||||
|
||||
--- @param a vim.Diagnostic
|
||||
--- @param b vim.Diagnostic
|
||||
--- @param primary_key string
|
||||
--- @param reverse boolean
|
||||
--- @param col_fn? fun(diagnostic: vim.Diagnostic): integer
|
||||
--- @return boolean
|
||||
function M.diagnostic_cmp(a, b, primary_key, reverse, col_fn)
|
||||
local a_val, b_val --- @type integer, integer
|
||||
if col_fn then
|
||||
a_val, b_val = col_fn(a), col_fn(b)
|
||||
else
|
||||
a_val = a[primary_key] --[[@as integer]]
|
||||
b_val = b[primary_key] --[[@as integer]]
|
||||
end
|
||||
|
||||
local cmp = function(x, y)
|
||||
if reverse then
|
||||
return x > y
|
||||
end
|
||||
return x < y
|
||||
end
|
||||
|
||||
if a_val ~= b_val then
|
||||
return cmp(a_val, b_val)
|
||||
end
|
||||
if a.lnum ~= b.lnum then
|
||||
return cmp(a.lnum, b.lnum)
|
||||
end
|
||||
if a.col ~= b.col then
|
||||
return cmp(a.col, b.col)
|
||||
end
|
||||
if a.end_lnum ~= b.end_lnum then
|
||||
return cmp(a.end_lnum, b.end_lnum)
|
||||
end
|
||||
if a.end_col ~= b.end_col then
|
||||
return cmp(a.end_col, b.end_col)
|
||||
end
|
||||
|
||||
return cmp(a._extmark_id or 0, b._extmark_id or 0)
|
||||
end
|
||||
|
||||
--- @param format fun(diagnostic: vim.Diagnostic): string?
|
||||
--- @param diagnostics vim.Diagnostic[]
|
||||
--- @return vim.Diagnostic[]
|
||||
function M.reformat_diagnostics(format, diagnostics)
|
||||
vim.validate('format', format, 'function')
|
||||
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
|
||||
|
||||
local formatted = {} --- @type vim.Diagnostic[]
|
||||
for _, diagnostic in ipairs(diagnostics) do
|
||||
local message = format(diagnostic)
|
||||
if message ~= nil then
|
||||
local formatted_diagnostic = vim.deepcopy(diagnostic, true)
|
||||
formatted_diagnostic.message = message
|
||||
formatted[#formatted + 1] = formatted_diagnostic
|
||||
end
|
||||
end
|
||||
return formatted
|
||||
end
|
||||
|
||||
return M
|
||||
314
runtime/lua/vim/diagnostic/_store.lua
Normal file
314
runtime/lua/vim/diagnostic/_store.lua
Normal file
@@ -0,0 +1,314 @@
|
||||
local api = vim.api
|
||||
|
||||
local severity_module = require('vim.diagnostic._severity')
|
||||
|
||||
--- @class (private) vim.diagnostic._store
|
||||
local M = {}
|
||||
|
||||
-- bufnr -> ns -> Diagnostic[]
|
||||
local diagnostic_cache = {} --- @type table<integer,table<integer,vim.Diagnostic[]?>>
|
||||
|
||||
local group = api.nvim_create_augroup('nvim.diagnostic.buf_wipeout', {})
|
||||
setmetatable(diagnostic_cache, {
|
||||
--- @param t table<integer,vim.Diagnostic[]>
|
||||
--- @param bufnr integer
|
||||
__index = function(t, bufnr)
|
||||
assert(bufnr > 0, 'Invalid buffer number')
|
||||
api.nvim_create_autocmd('BufWipeout', {
|
||||
group = group,
|
||||
buffer = bufnr,
|
||||
callback = function()
|
||||
rawset(t, bufnr, nil)
|
||||
end,
|
||||
})
|
||||
t[bufnr] = {}
|
||||
return t[bufnr]
|
||||
end,
|
||||
})
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @param namespace integer
|
||||
--- @param d vim.Diagnostic.Set
|
||||
local function norm_diag(bufnr, namespace, d)
|
||||
vim.validate('diagnostic.lnum', d.lnum, 'number')
|
||||
local d1 = d --[[@as vim.Diagnostic]]
|
||||
d1.severity = d.severity and severity_module.to_severity(d.severity)
|
||||
or vim.diagnostic.severity.ERROR
|
||||
d1.end_lnum = d.end_lnum or d.lnum
|
||||
d1.col = d.col or 0
|
||||
d1.end_col = d.end_col or d.col or 0
|
||||
d1.namespace = namespace
|
||||
d1.bufnr = bufnr
|
||||
end
|
||||
|
||||
--- Execute a given function now if the given buffer is already loaded or once it is loaded later.
|
||||
---
|
||||
--- @param bufnr integer Buffer number
|
||||
--- @param fn fun()
|
||||
--- @return integer?
|
||||
local function once_buf_loaded(bufnr, fn)
|
||||
if api.nvim_buf_is_loaded(bufnr) then
|
||||
fn()
|
||||
else
|
||||
return api.nvim_create_autocmd('BufRead', {
|
||||
buffer = bufnr,
|
||||
once = true,
|
||||
callback = function()
|
||||
fn()
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
--- @param bufnr integer?
|
||||
--- @param opts vim.diagnostic.GetOpts?
|
||||
--- @param clamp boolean
|
||||
--- @return vim.Diagnostic[]
|
||||
function M.get_diagnostics(bufnr, opts, clamp)
|
||||
opts = opts or {}
|
||||
|
||||
local namespace = opts.namespace
|
||||
|
||||
if type(namespace) == 'number' then
|
||||
namespace = { namespace }
|
||||
end
|
||||
|
||||
--- @cast namespace integer[]
|
||||
|
||||
--- @type vim.Diagnostic[]
|
||||
local diagnostics = {}
|
||||
|
||||
-- Memoized results of buf_line_count per bufnr
|
||||
--- @type table<integer,integer>
|
||||
local buf_line_count = setmetatable({}, {
|
||||
--- @param t table<integer,integer>
|
||||
--- @param k integer
|
||||
--- @return integer
|
||||
__index = function(t, k)
|
||||
t[k] = api.nvim_buf_line_count(k)
|
||||
return rawget(t, k)
|
||||
end,
|
||||
})
|
||||
|
||||
local match_severity = opts.severity and severity_module.severity_predicate(opts.severity)
|
||||
or function(_)
|
||||
return true
|
||||
end
|
||||
|
||||
--- @param b integer
|
||||
--- @param d vim.Diagnostic
|
||||
local match_enablement = function(d, b)
|
||||
if opts.enabled == nil then
|
||||
return true
|
||||
end
|
||||
|
||||
local enabled = vim.diagnostic.is_enabled({ bufnr = b, ns_id = d.namespace })
|
||||
|
||||
return (enabled and opts.enabled) or (not enabled and not opts.enabled)
|
||||
end
|
||||
|
||||
--- @param b integer
|
||||
--- @param d vim.Diagnostic
|
||||
local function add(b, d)
|
||||
if
|
||||
match_severity(d)
|
||||
and match_enablement(d, b)
|
||||
and (not opts.lnum or (opts.lnum >= d.lnum and opts.lnum <= (d.end_lnum or d.lnum)))
|
||||
then
|
||||
if clamp and api.nvim_buf_is_loaded(b) then
|
||||
local line_count = buf_line_count[b] - 1
|
||||
if
|
||||
d.lnum > line_count
|
||||
or d.end_lnum > line_count
|
||||
or d.lnum < 0
|
||||
or d.end_lnum < 0
|
||||
or d.col < 0
|
||||
or d.end_col < 0
|
||||
then
|
||||
d = vim.deepcopy(d, true)
|
||||
d.lnum = math.max(math.min(d.lnum, line_count), 0)
|
||||
d.end_lnum = math.max(math.min(d.end_lnum, line_count), 0)
|
||||
d.col = math.max(d.col, 0)
|
||||
d.end_col = math.max(d.end_col, 0)
|
||||
end
|
||||
end
|
||||
table.insert(diagnostics, d)
|
||||
end
|
||||
end
|
||||
|
||||
--- @param buf integer
|
||||
--- @param diags vim.Diagnostic[]
|
||||
local function add_all_diags(buf, diags)
|
||||
for _, diagnostic0 in pairs(diags) do
|
||||
add(buf, diagnostic0)
|
||||
end
|
||||
end
|
||||
|
||||
if not namespace and not bufnr then
|
||||
for buf, ns_diags in pairs(diagnostic_cache) do
|
||||
for _, diags in pairs(ns_diags) do
|
||||
add_all_diags(buf, diags)
|
||||
end
|
||||
end
|
||||
elseif not namespace then
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
for iter_namespace in pairs(diagnostic_cache[bufnr]) do
|
||||
add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace])
|
||||
end
|
||||
elseif bufnr == nil then
|
||||
for b, t in pairs(diagnostic_cache) do
|
||||
for _, iter_namespace in ipairs(namespace) do
|
||||
add_all_diags(b, t[iter_namespace] or {})
|
||||
end
|
||||
end
|
||||
else
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
for _, iter_namespace in ipairs(namespace) do
|
||||
add_all_diags(bufnr, diagnostic_cache[bufnr][iter_namespace] or {})
|
||||
end
|
||||
end
|
||||
|
||||
return diagnostics
|
||||
end
|
||||
|
||||
--- @return integer[]
|
||||
function M.get_bufnrs()
|
||||
return vim.tbl_keys(diagnostic_cache)
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
--- @return integer[]
|
||||
function M.get_buf_namespaces(bufnr)
|
||||
return vim.tbl_keys(diagnostic_cache[vim._resolve_bufnr(bufnr)])
|
||||
end
|
||||
|
||||
--- @param namespace integer
|
||||
--- @param bufnr integer
|
||||
function M.clear(namespace, bufnr)
|
||||
diagnostic_cache[vim._resolve_bufnr(bufnr)][namespace] = nil
|
||||
end
|
||||
|
||||
--- @param bufnr integer
|
||||
function M.drop_buf(bufnr)
|
||||
diagnostic_cache[vim._resolve_bufnr(bufnr)] = nil
|
||||
end
|
||||
|
||||
--- Set diagnostics for the given namespace and buffer.
|
||||
---
|
||||
--- @param namespace integer The diagnostic namespace
|
||||
--- @param bufnr integer Buffer number
|
||||
--- @param diagnostics vim.Diagnostic.Set[]
|
||||
function M.set(namespace, bufnr, diagnostics)
|
||||
vim.validate('namespace', namespace, 'number')
|
||||
vim.validate('bufnr', bufnr, 'number')
|
||||
vim.validate('diagnostics', diagnostics, vim.islist, 'a list of diagnostics')
|
||||
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
|
||||
for _, diagnostic0 in ipairs(diagnostics) do
|
||||
norm_diag(bufnr, namespace, diagnostic0)
|
||||
end
|
||||
|
||||
--- @cast diagnostics vim.Diagnostic[]
|
||||
|
||||
if vim.tbl_isempty(diagnostics) then
|
||||
diagnostic_cache[bufnr][namespace] = nil
|
||||
else
|
||||
diagnostic_cache[bufnr][namespace] = diagnostics
|
||||
end
|
||||
|
||||
-- Compute positions, set them as extmarks, and store in diagnostic._extmark_id
|
||||
-- (used by get_logical_pos to adjust positions).
|
||||
once_buf_loaded(bufnr, function()
|
||||
local ns = vim.diagnostic.get_namespace(namespace)
|
||||
|
||||
if not ns.user_data.location_ns then
|
||||
ns.user_data.location_ns =
|
||||
api.nvim_create_namespace(string.format('nvim.%s.diagnostic', ns.name))
|
||||
end
|
||||
|
||||
api.nvim_buf_clear_namespace(bufnr, ns.user_data.location_ns, 0, -1)
|
||||
|
||||
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, true)
|
||||
-- set extmarks at diagnostic locations to preserve logical positions despite text changes
|
||||
for _, diagnostic0 in ipairs(diagnostics) do
|
||||
local last_row = #lines - 1
|
||||
local row = math.max(0, math.min(diagnostic0.lnum, last_row))
|
||||
local row_len = #lines[row + 1]
|
||||
local col = math.max(0, math.min(diagnostic0.col, row_len - 1))
|
||||
|
||||
local end_row = math.max(0, math.min(diagnostic0.end_lnum or row, last_row))
|
||||
local end_row_len = #lines[end_row + 1]
|
||||
local end_col = math.max(0, math.min(diagnostic0.end_col or col, end_row_len))
|
||||
|
||||
if end_row == row then
|
||||
-- avoid starting an extmark beyond end of the line
|
||||
if end_col == col then
|
||||
end_col = math.min(end_col + 1, end_row_len)
|
||||
end
|
||||
else
|
||||
-- avoid ending an extmark before start of the line
|
||||
if end_col == 0 then
|
||||
end_row = end_row - 1
|
||||
|
||||
local end_line = lines[end_row + 1]
|
||||
|
||||
if not end_line then
|
||||
error(
|
||||
'Failed to adjust diagnostic position to the end of a previous line. #lines in a buffer: '
|
||||
.. #lines
|
||||
.. ', lnum: '
|
||||
.. diagnostic0.lnum
|
||||
.. ', col: '
|
||||
.. diagnostic0.col
|
||||
.. ', end_lnum: '
|
||||
.. diagnostic0.end_lnum
|
||||
.. ', end_col: '
|
||||
.. diagnostic0.end_col
|
||||
)
|
||||
end
|
||||
|
||||
end_col = #end_line
|
||||
end
|
||||
end
|
||||
|
||||
diagnostic0._extmark_id =
|
||||
api.nvim_buf_set_extmark(bufnr, ns.user_data.location_ns, row, col, {
|
||||
end_row = end_row,
|
||||
end_col = end_col,
|
||||
invalidate = true,
|
||||
})
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- @param bufnr integer? Buffer number to get diagnostics from. Use 0 for
|
||||
--- current buffer or nil for all buffers.
|
||||
--- @param opts? vim.diagnostic.GetOpts
|
||||
--- @return vim.Diagnostic[] : Fields `bufnr`, `end_lnum`, `end_col`, and `severity`
|
||||
--- are guaranteed to be present.
|
||||
function M.get(bufnr, opts)
|
||||
vim.validate('bufnr', bufnr, 'number', true)
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
|
||||
return vim.deepcopy(M.get_diagnostics(bufnr, opts, false), true)
|
||||
end
|
||||
|
||||
--- @param bufnr? integer Buffer number to get diagnostics from. Use 0 for
|
||||
--- current buffer or nil for all buffers.
|
||||
--- @param opts? vim.diagnostic.GetOpts
|
||||
--- @return table<integer, integer> : Table with actually present severity values as keys
|
||||
--- (see |diagnostic-severity|) and integer counts as values.
|
||||
function M.count(bufnr, opts)
|
||||
vim.validate('bufnr', bufnr, 'number', true)
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
|
||||
local diagnostics = M.get_diagnostics(bufnr, opts, false)
|
||||
local count = {} --- @type table<integer,integer>
|
||||
for _, d in ipairs(diagnostics) do
|
||||
count[d.severity] = (count[d.severity] or 0) + 1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user