From 8122470f8310ae34bcd5e436e8474f9255eb16f2 Mon Sep 17 00:00:00 2001 From: Raphael Date: Wed, 13 Dec 2023 22:19:53 +0800 Subject: refactor(diagnostic): set sign by using extmark (#26193) after sign implementation refactor by using extmark, we can use `nvim_buf_set_extmark` to set diagnostic sign instead use `sign_define` --- runtime/lua/vim/diagnostic.lua | 61 +++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 34 deletions(-) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 99448982b4..729156584f 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -221,30 +221,6 @@ local underline_highlight_map = make_highlight_map('Underline') local floating_highlight_map = make_highlight_map('Floating') local sign_highlight_map = make_highlight_map('Sign') ----@private -local define_default_signs = (function() - local signs_defined = false - return function() - if signs_defined then - return - end - - for severity, sign_hl_name in pairs(sign_highlight_map) do - if vim.tbl_isempty(vim.fn.sign_getdefined(sign_hl_name)) then - local severity_name = M.severity[severity] - vim.fn.sign_define(sign_hl_name, { - text = (severity_name or 'U'):sub(1, 1), - texthl = sign_hl_name, - linehl = '', - numhl = '', - }) - end - end - - signs_defined = true - end -end)() - local function get_bufnr(bufnr) if not bufnr or bufnr == 0 then return api.nvim_get_current_buf() @@ -618,6 +594,14 @@ end --- * priority: (number, default 10) Base priority to use for signs. When --- {severity_sort} is used, the priority of a sign is adjusted based on --- its severity. Otherwise, all signs use the same priority. +--- * text: (table) A table mapping |diagnostic-severity| to the sign text +--- to display in the sign column. The default is to use "E", "W", "I", and "H" +--- for errors, warnings, information, and hints, respectively. Example: +---
lua
+---                     vim.diagnostic.config({
+---                       sign = { text = { [vim.diagnostic.severity.ERROR] = 'E', ... } }
+---                     })
+---                   
--- - float: Options for floating windows. See |vim.diagnostic.open_float()|. --- - update_in_insert: (default false) Update diagnostics in Insert mode (if false, --- diagnostics are updated on InsertLeave) @@ -868,8 +852,6 @@ M.handlers.signs = { diagnostics = filter_by_severity(opts.signs.severity, diagnostics) end - define_default_signs() - -- 10 is the default sign priority when none is explicitly specified local priority = opts.signs and opts.signs.priority or 10 local get_priority @@ -890,22 +872,33 @@ M.handlers.signs = { end local ns = M.get_namespace(namespace) - if not ns.user_data.sign_group then - ns.user_data.sign_group = string.format('vim.diagnostic.%s', ns.name) + if not ns.user_data.sign_ns then + ns.user_data.sign_ns = api.nvim_create_namespace('') + end + + local text = {} + for k in pairs(M.severity) do + if opts.signs.text and opts.signs.text[k] then + text[k] = opts.signs.text[k] + elseif type(k) == 'string' and not text[k] then + text[k] = string.sub(k, 1, 1):upper() + end end - local sign_group = ns.user_data.sign_group for _, diagnostic in ipairs(diagnostics) do - vim.fn.sign_place(0, sign_group, sign_highlight_map[diagnostic.severity], bufnr, { - priority = get_priority(diagnostic.severity), - lnum = diagnostic.lnum + 1, - }) + if api.nvim_buf_is_loaded(diagnostic.bufnr) then + api.nvim_buf_set_extmark(bufnr, ns.user_data.sign_ns, diagnostic.lnum, 0, { + sign_text = text[diagnostic.severity] or text[M.severity[diagnostic.severity]] or 'U', + sign_hl_group = sign_highlight_map[diagnostic.severity], + priority = get_priority(diagnostic.severity), + }) + end end end, hide = function(namespace, bufnr) local ns = M.get_namespace(namespace) if ns.user_data.sign_group and api.nvim_buf_is_valid(bufnr) then - vim.fn.sign_unplace(ns.user_data.sign_group, { buffer = bufnr }) + api.nvim_buf_clear_namespace(bufnr, ns.user_data.sign_ns, 0, -1) end end, } -- cgit From 29d5ff6ac4eade7996d14eec60a31ec6328a165d Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Wed, 13 Dec 2023 09:26:39 -0600 Subject: fix(diagnostic): check for sign namespace instead of sign group --- runtime/lua/vim/diagnostic.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 729156584f..d6db530bb0 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -897,7 +897,7 @@ M.handlers.signs = { end, hide = function(namespace, bufnr) local ns = M.get_namespace(namespace) - if ns.user_data.sign_group and api.nvim_buf_is_valid(bufnr) then + 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, -- cgit From 39112c72dd3722cd4a0770fc23c9d7269a9c2283 Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Wed, 13 Dec 2023 09:43:27 -0600 Subject: docs(diagnostic): fix typo in example --- runtime/lua/vim/diagnostic.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index d6db530bb0..1d76d1d7a0 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -598,9 +598,9 @@ end --- to display in the sign column. The default is to use "E", "W", "I", and "H" --- for errors, warnings, information, and hints, respectively. Example: ---
lua
----                     vim.diagnostic.config({
----                       sign = { text = { [vim.diagnostic.severity.ERROR] = 'E', ... } }
----                     })
+---                       vim.diagnostic.config({
+---                         signs = { text = { [vim.diagnostic.severity.ERROR] = 'E', ... } }
+---                       })
 ---                   
--- - float: Options for floating windows. See |vim.diagnostic.open_float()|. --- - update_in_insert: (default false) Update diagnostics in Insert mode (if false, -- cgit From a3b39784744f330b922117655811542202fcd85b Mon Sep 17 00:00:00 2001 From: Gregory Anders Date: Wed, 13 Dec 2023 09:54:04 -0600 Subject: feat(diagnostics): support numhl and linehl for diagnostic signs --- runtime/lua/vim/diagnostic.lua | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 1d76d1d7a0..0eb1ac7f15 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -602,6 +602,10 @@ end --- signs = { text = { [vim.diagnostic.severity.ERROR] = 'E', ... } } --- }) --- +--- * numhl: (table) A table mapping |diagnostic-severity| to the highlight +--- group used for the line number where the sign is placed. +--- * linehl: (table) A table mapping |diagnostic-severity| to the highlight group +--- used for the whole line the sign is placed in. --- - float: Options for floating windows. See |vim.diagnostic.open_float()|. --- - update_in_insert: (default false) Update diagnostics in Insert mode (if false, --- diagnostics are updated on InsertLeave) @@ -885,11 +889,16 @@ M.handlers.signs = { end end + local numhl = opts.signs.numhl or {} + local linehl = opts.signs.linehl or {} + for _, diagnostic in ipairs(diagnostics) do if api.nvim_buf_is_loaded(diagnostic.bufnr) then api.nvim_buf_set_extmark(bufnr, ns.user_data.sign_ns, diagnostic.lnum, 0, { sign_text = text[diagnostic.severity] or text[M.severity[diagnostic.severity]] or 'U', sign_hl_group = sign_highlight_map[diagnostic.severity], + number_hl_group = numhl[diagnostic.severity], + line_hl_group = linehl[diagnostic.severity], priority = get_priority(diagnostic.severity), }) end -- cgit From ef38fdfdc6c84abd8ce7be02eaf8edc91ebc7917 Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Thu, 14 Dec 2023 09:19:33 -0600 Subject: refactor(diagnostic): use named namespaces (#26568) Anonymous namespaces are more difficult to extend or hook into since they do not appear in the output of nvim_get_namespaces(). Use named namespaces instead. --- runtime/lua/vim/diagnostic.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 0eb1ac7f15..2171ae14e6 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -877,7 +877,8 @@ M.handlers.signs = { local ns = M.get_namespace(namespace) if not ns.user_data.sign_ns then - ns.user_data.sign_ns = api.nvim_create_namespace('') + ns.user_data.sign_ns = + api.nvim_create_namespace(string.format('%s/diagnostic/signs', ns.name)) end local text = {} @@ -938,7 +939,8 @@ M.handlers.underline = { local ns = M.get_namespace(namespace) if not ns.user_data.underline_ns then - ns.user_data.underline_ns = api.nvim_create_namespace('') + ns.user_data.underline_ns = + api.nvim_create_namespace(string.format('%s/diagnostic/underline', ns.name)) end local underline_ns = ns.user_data.underline_ns @@ -1020,7 +1022,8 @@ M.handlers.virtual_text = { local ns = M.get_namespace(namespace) if not ns.user_data.virt_text_ns then - ns.user_data.virt_text_ns = api.nvim_create_namespace('') + ns.user_data.virt_text_ns = + api.nvim_create_namespace(string.format('%s/diagnostic/virtual_text', ns.name)) end local virt_text_ns = ns.user_data.virt_text_ns -- cgit From e164f4c2715c97b48607bb6339eac3aff7106c47 Mon Sep 17 00:00:00 2001 From: Yi Ming Date: Mon, 18 Dec 2023 08:11:47 +0800 Subject: docs(diagnostic): add return value of `vim.diagnostic.config()` (#26615) --- runtime/lua/vim/diagnostic.lua | 2 ++ 1 file changed, 2 insertions(+) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 2171ae14e6..e4f694b8a2 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -617,6 +617,8 @@ end --- ---@param namespace integer|nil Update the options for the given namespace. When omitted, update the --- global diagnostic options. +--- +---@return table|nil table of current diagnostic config if `opts` is omitted. function M.config(opts, namespace) vim.validate({ opts = { opts, 't', true }, -- cgit From 3a4aa3fc58f87a295a075fe457bc78805eef7c4d Mon Sep 17 00:00:00 2001 From: Gregory Anders <8965202+gpanders@users.noreply.github.com> Date: Mon, 18 Dec 2023 11:04:44 -0600 Subject: refactor: soft-deprecate diagnostic signs configured with :sign-define (#26618) Diagnostic signs should now be configured with vim.diagnostic.config(), but "legacy" sign definitions should go through the standard deprecation process to minimize the impact from breaking changes. --- runtime/lua/vim/diagnostic.lua | 49 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index e4f694b8a2..6d2c212dfc 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -588,7 +588,7 @@ end --- return diagnostic.message --- end --- ---- - signs: (default true) Use signs for diagnostics. Options: +--- - signs: (default true) Use signs for diagnostics |diagnostic-signs|. Options: --- * severity: Only show signs for diagnostics matching the given --- severity |diagnostic-severity| --- * priority: (number, default 10) Base priority to use for signs. When @@ -883,7 +883,52 @@ M.handlers.signs = { api.nvim_create_namespace(string.format('%s/diagnostic/signs', ns.name)) end - local text = {} + --- Handle legacy diagnostic sign definitions + --- These were deprecated in 0.10 and will be removed in 0.12 + if opts.signs and not opts.signs.text and not opts.signs.numhl and not opts.signs.texthl then + for _, v in ipairs({ 'Error', 'Warn', 'Info', 'Hint' }) do + local name = string.format('DiagnosticSign%s', v) + local sign = vim.fn.sign_getdefined(name)[1] + if sign then + local severity = M.severity[v:upper()] + if vim.fn.has('nvim-0.11') == 1 then + vim.deprecate( + 'Defining diagnostic signs with :sign-define or sign_define()', + 'vim.diagnostic.config()', + '0.12', + nil, + false + ) + end + + if not opts.signs.text then + opts.signs.text = {} + end + + if not opts.signs.numhl then + opts.signs.numhl = {} + end + + if not opts.signs.linehl then + opts.signs.linehl = {} + end + + if opts.signs.text[severity] == nil then + opts.signs.text[severity] = sign.text or '' + end + + if opts.signs.numhl[severity] == nil then + opts.signs.numhl[severity] = sign.numhl + end + + if opts.signs.linehl[severity] == nil then + opts.signs.linehl[severity] = sign.linehl + end + end + end + end + + local text = {} ---@type table for k in pairs(M.severity) do if opts.signs.text and opts.signs.text[k] then text[k] = opts.signs.text[k] -- cgit From 5f9d4d8afeb5dc3d5df4965c24cbb4c6e01694f7 Mon Sep 17 00:00:00 2001 From: dundargoc Date: Mon, 25 Dec 2023 21:28:28 +0100 Subject: refactor: use vim.deprecate on all deprecated functions --- runtime/lua/vim/diagnostic.lua | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 6d2c212dfc..a82a61eeb7 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -891,15 +891,13 @@ M.handlers.signs = { local sign = vim.fn.sign_getdefined(name)[1] if sign then local severity = M.severity[v:upper()] - if vim.fn.has('nvim-0.11') == 1 then - vim.deprecate( - 'Defining diagnostic signs with :sign-define or sign_define()', - 'vim.diagnostic.config()', - '0.12', - nil, - false - ) - end + vim.deprecate( + 'Defining diagnostic signs with :sign-define or sign_define()', + 'vim.diagnostic.config()', + '0.12', + nil, + false + ) if not opts.signs.text then opts.signs.text = {} -- cgit From d51b6157473c4830313b566116aa3ad38dc97412 Mon Sep 17 00:00:00 2001 From: dundargoc Date: Wed, 13 Dec 2023 14:04:24 +0100 Subject: refactor: fix luals warnings --- runtime/lua/vim/diagnostic.lua | 1 + 1 file changed, 1 insertion(+) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index a82a61eeb7..ad40723737 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -108,6 +108,7 @@ local function filter_by_severity(severity, diagnostics) severities[to_severity(s)] = true end + --- @param t table return vim.tbl_filter(function(t) return severities[t.severity] end, diagnostics) -- cgit From 4ee656e4f35766bef4e27c5afbfa8e3d8d74a76c Mon Sep 17 00:00:00 2001 From: Evgeni Chasnovski Date: Mon, 1 Jan 2024 23:03:50 +0200 Subject: feature(diagnostic): add `vim.diagnostic.count()` (#26807) feat(diagnostic): add `vim.diagnostic.count()` Problem: Getting diagnostic count based on the output of `vim.diagnostic.get()` might become costly as number of diagnostic entries grows. This is because it returns a copy of diagnostic cache entries (so as to not allow users to change them in place). Getting information about diagnostic count is frequently used in statusline, so it is important to be as fast as reasonbly possible. Solution: Add `vim.diagnostic.count()` which computes severity counts without making copies. --- runtime/lua/vim/diagnostic.lua | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index ad40723737..a447463dff 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -363,7 +363,6 @@ local function get_diagnostics(bufnr, opts, clamp) local function add(b, d) if not opts.lnum or d.lnum == opts.lnum then - d = vim.deepcopy(d) if clamp and api.nvim_buf_is_loaded(b) then local line_count = buf_line_count[b] - 1 if @@ -374,6 +373,7 @@ local function get_diagnostics(bufnr, opts, clamp) or d.col < 0 or d.end_col < 0 then + d = vim.deepcopy(d) 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) @@ -756,7 +756,31 @@ function M.get(bufnr, opts) opts = { opts, 't', true }, }) - return get_diagnostics(bufnr, opts, false) + return vim.deepcopy(get_diagnostics(bufnr, opts, false)) +end + +--- Get current diagnostics count. +--- +---@param bufnr integer|nil Buffer number to get diagnostics from. Use 0 for +--- current buffer or nil for all buffers. +---@param opts table|nil A table with the following keys: +--- - namespace: (number) Limit diagnostics to the given namespace. +--- - lnum: (number) Limit diagnostics to the given line number. +--- - severity: See |diagnostic-severity|. +---@return table A 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, 'n', true }, + opts = { opts, 't', true }, + }) + + local diagnostics = get_diagnostics(bufnr, opts, false) + local count = {} + for i = 1, #diagnostics do + local severity = diagnostics[i].severity + count[severity] = (count[severity] or 0) + 1 + end + return count end --- Get the previous diagnostic closest to the cursor position. -- cgit From 3734519e3b4ba1bf19ca772104170b0ef776be46 Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 2 Jan 2024 15:47:55 +0000 Subject: feat(lua): add noref to deepcopy Problem: Currently `deepcopy` hashes every single tables it copies so it can be reused. For tables of mostly unique items that are non recursive, this hashing is unnecessarily expensive Solution: Port the `noref` argument from Vimscripts `deepcopy()`. The below benchmark demonstrates the results for two extreme cases of tables of different sizes. One table that uses the same table lots of times and one with all unique tables. | test | `noref=false` (ms) | `noref=true` (ms) | | -------------------- | ------------------ | ----------------- | | unique tables (50) | 6.59 | 2.62 | | shared tables (50) | 3.24 | 6.40 | | unique tables (2000) | 23381.48 | 2884.53 | | shared tables (2000) | 3505.54 | 14038.80 | The results are basically the inverse of each other where `noref` is much more performance on tables with unique fields, and `not noref` is more performant on tables that reuse fields. --- runtime/lua/vim/diagnostic.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index a447463dff..897837a5ce 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -134,7 +134,7 @@ local function prefix_source(diagnostics) return d end - local t = vim.deepcopy(d) + local t = vim.deepcopy(d, true) t.message = string.format('%s: %s', d.source, d.message) return t end, diagnostics) @@ -146,7 +146,7 @@ local function reformat_diagnostics(format, diagnostics) diagnostics = { diagnostics, 't' }, }) - local formatted = vim.deepcopy(diagnostics) + local formatted = vim.deepcopy(diagnostics, true) for _, diagnostic in ipairs(formatted) do diagnostic.message = format(diagnostic) end @@ -373,7 +373,7 @@ local function get_diagnostics(bufnr, opts, clamp) or d.col < 0 or d.end_col < 0 then - d = vim.deepcopy(d) + 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) @@ -636,7 +636,7 @@ function M.config(opts, namespace) if not opts then -- Return current config - return vim.deepcopy(t) + return vim.deepcopy(t, true) end for k, v in pairs(opts) do @@ -723,7 +723,7 @@ end --- ---@return table A list of active diagnostic namespaces |vim.diagnostic|. function M.get_namespaces() - return vim.deepcopy(all_namespaces) + return vim.deepcopy(all_namespaces, true) end ---@class Diagnostic @@ -756,7 +756,7 @@ function M.get(bufnr, opts) opts = { opts, 't', true }, }) - return vim.deepcopy(get_diagnostics(bufnr, opts, false)) + return vim.deepcopy(get_diagnostics(bufnr, opts, false), true) end --- Get current diagnostics count. -- cgit From 50284d07b6f020c819aeb07bfb30d88453e63b6d Mon Sep 17 00:00:00 2001 From: Lewis Russell Date: Tue, 9 Jan 2024 12:47:57 +0000 Subject: fix(diagnostic): typing --- runtime/lua/vim/diagnostic.lua | 521 +++++++++++++++++++++++++++++------------ 1 file changed, 365 insertions(+), 156 deletions(-) (limited to 'runtime/lua/vim/diagnostic.lua') diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 897837a5ce..c7a1e8ebd3 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -2,15 +2,87 @@ local api, if_nil = vim.api, vim.F.if_nil local M = {} ----@enum DiagnosticSeverity +--- @class vim.Diagnostic +--- @field bufnr? integer +--- @field lnum integer 0-indexed +--- @field end_lnum? integer 0-indexed +--- @field col integer 0-indexed +--- @field end_col? integer 0-indexed +--- @field severity? vim.diagnostic.Severity +--- @field message string +--- @field source? string +--- @field code? string +--- @field _tags? { deprecated: boolean, unnecessary: boolean} +--- @field user_data? any arbitrary data plugins can add +--- @field namespace? integer + +--- @class vim.diagnostic.Opts +--- @field float? boolean|vim.diagnostic.Opts.Float +--- @field update_in_insert? boolean +--- @field underline? boolean|vim.diagnostic.Opts.Underline +--- @field virtual_text? boolean|vim.diagnostic.Opts.VirtualText +--- @field signs? boolean|vim.diagnostic.Opts.Signs +--- @field severity_sort? boolean|{reverse?:boolean} + +--- @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 signs vim.diagnostic.Opts.Signs +--- @field severity_sort {reverse?:boolean} + +--- @class vim.diagnostic.Opts.Float +--- @field bufnr? integer +--- @field namespace? integer +--- @field scope? 'line'|'buffer'|'cursor'|'c'|'l'|'b' +--- @field pos? integer|{[1]:integer,[2]:integer} +--- @field severity_sort? boolean|{reverse?:boolean} +--- @field severity? vim.diagnostic.Severity +--- @field header? string|{[1]:string,[2]:any} +--- @field source? boolean|string +--- @field format? fun(diagnostic:vim.Diagnostic): string +--- @field prefix? string|table|(fun(diagnostic:vim.Diagnostic,i:integer,total:integer): string, string) +--- @field suffix? string|table|(fun(diagnostic:vim.Diagnostic,i:integer,total:integer): string, string) +--- @field focus_id? string + +--- @class vim.diagnostic.Opts.Underline +--- @field severity? vim.diagnostic.Severity + +--- @class vim.diagnostic.Opts.VirtualText +--- @field severity? vim.diagnostic.Severity +--- @field source? boolean|string +--- @field prefix? string|function +--- @field suffix? string|function +--- @field spacing? integer +--- @field format? function +--- @field hl_mode? 'replace'|'combine'|'blend' +--- @field virt_text? {[1]:string,[2]:any}[] +--- @field virt_text_pos? 'eol'|'overlay'|'right_align'|'inline' +--- @field virt_text_win_col? integer +--- @field virt_text_hide? boolean + +--- @class vim.diagnostic.Opts.Signs +--- @field severity? vim.diagnostic.Severity +--- @field priority? integer +--- @field text? table +--- @field numhl? table +--- @field linehl? table +--- @field texthl? table + +--- @enum vim.diagnostic.Severity M.severity = { ERROR = 1, WARN = 2, INFO = 3, HINT = 4, + [1] = 'ERROR', + [2] = 'WARN', + [3] = 'INFO', + [4] = 'HINT', } -vim.tbl_add_reverse_lookup(M.severity) +--- @alias vim.diagnostic.SeverityInt 1|2|3|4 -- Mappings from qflist/loclist error types to severities M.severity.E = M.severity.ERROR @@ -18,6 +90,7 @@ M.severity.W = M.severity.WARN M.severity.I = M.severity.INFO M.severity.N = M.severity.HINT +--- @type vim.diagnostic.Opts local global_diagnostic_options = { signs = true, underline = true, @@ -27,6 +100,11 @@ local global_diagnostic_options = { severity_sort = false, } +--- @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) + +--- @type table M.handlers = setmetatable({}, { __newindex = function(t, name, handler) vim.validate({ handler = { handler, 't' } }) @@ -39,6 +117,9 @@ M.handlers = setmetatable({}, { -- Metatable that automatically creates an empty table when assigning to a missing key local bufnr_and_namespace_cacher_mt = { + --- @param t table + --- @param bufnr integer + --- @return table __index = function(t, bufnr) assert(bufnr > 0, 'Invalid buffer number') t[bufnr] = {} @@ -46,10 +127,13 @@ local bufnr_and_namespace_cacher_mt = { end, } -local diagnostic_cache +-- bufnr -> ns -> Diagnostic[] +local diagnostic_cache = {} --- @type table> do local group = api.nvim_create_augroup('DiagnosticBufWipeout', {}) - diagnostic_cache = setmetatable({}, { + setmetatable(diagnostic_cache, { + --- @param t table + --- @param bufnr integer __index = function(t, bufnr) assert(bufnr > 0, 'Invalid buffer number') api.nvim_create_autocmd('BufWipeout', { @@ -65,13 +149,35 @@ do }) end +--- @class vim.diagnostic._extmark +--- @field [1] integer id +--- @field [2] integer start +--- @field [3] integer end +--- @field [4] table details + +--- @type table> local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_namespace_cacher_mt) + +--- @type table local diagnostic_attached_buffers = {} + +--- @type table> local diagnostic_disabled = {} + +--- @type table> local bufs_waiting_to_update = setmetatable({}, bufnr_and_namespace_cacher_mt) +--- @class vim.diagnostic.NS +--- @field name string +--- @field opts vim.diagnostic.Opts +--- @field user_data table +--- @field disabled? boolean + +--- @type table local all_namespaces = {} +---@param severity string|vim.diagnostic.Severity +---@return vim.diagnostic.Severity? local function to_severity(severity) if type(severity) == 'string' then return assert( @@ -82,45 +188,56 @@ local function to_severity(severity) return severity end +--- @param severity vim.diagnostic.Severity|vim.diagnostic.Severity[]|{min:vim.diagnostic.Severity,max:vim.diagnostic.Severity} +--- @param diagnostics vim.Diagnostic[] +--- @return vim.Diagnostic[] local function filter_by_severity(severity, diagnostics) if not severity then return diagnostics end if type(severity) ~= 'table' then - severity = to_severity(severity) + severity = assert(to_severity(severity)) + --- @param t vim.Diagnostic return vim.tbl_filter(function(t) return t.severity == severity end, diagnostics) end if severity.min or severity.max then + --- @cast severity {min:vim.diagnostic.Severity,max:vim.diagnostic.Severity} local min_severity = to_severity(severity.min) or M.severity.HINT local max_severity = to_severity(severity.max) or M.severity.ERROR + --- @param t vim.Diagnostic return vim.tbl_filter(function(t) return t.severity <= min_severity and t.severity >= max_severity end, diagnostics) end - local severities = {} + --- @cast severity vim.diagnostic.Severity[] + + local severities = {} --- @type table for _, s in ipairs(severity) do - severities[to_severity(s)] = true + severities[assert(to_severity(s))] = true end - --- @param t table + --- @param t vim.Diagnostic return vim.tbl_filter(function(t) return severities[t.severity] end, diagnostics) end +--- @param bufnr integer +--- @return integer local function count_sources(bufnr) - local seen = {} + local seen = {} --- @type table local count = 0 for _, namespace_diagnostics in pairs(diagnostic_cache[bufnr]) do for _, diagnostic in ipairs(namespace_diagnostics) do - if diagnostic.source and not seen[diagnostic.source] then - seen[diagnostic.source] = true + local source = diagnostic.source + if source and not seen[source] then + seen[source] = true count = count + 1 end end @@ -128,7 +245,10 @@ local function count_sources(bufnr) return count end +--- @param diagnostics vim.Diagnostic[] +--- @return vim.Diagnostic[] local function prefix_source(diagnostics) + --- @param d vim.Diagnostic return vim.tbl_map(function(d) if not d.source then return d @@ -140,6 +260,8 @@ local function prefix_source(diagnostics) end, diagnostics) end +--- @param diagnostics vim.Diagnostic[] +--- @return vim.Diagnostic[] local function reformat_diagnostics(format, diagnostics) vim.validate({ format = { format, 'f' }, @@ -153,26 +275,35 @@ local function reformat_diagnostics(format, diagnostics) return formatted end +--- @param option string +--- @param namespace integer? +--- @return table local function enabled_value(option, namespace) local ns = namespace and M.get_namespace(namespace) or {} if ns.opts and type(ns.opts[option]) == 'table' then return ns.opts[option] end - if type(global_diagnostic_options[option]) == 'table' then - return global_diagnostic_options[option] + 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) + local val = value(namespace, bufnr) --- @type any if val == true then return enabled_value(option, namespace) else @@ -180,15 +311,18 @@ local function resolve_optional_value(option, value, namespace, bufnr) end elseif type(value) == 'table' then return value - else - error('Unexpected option type: ' .. vim.inspect(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 local function get_resolved_options(opts, namespace, bufnr) local ns = namespace and M.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) + local resolved = vim.tbl_extend('keep', opts or {}, ns.opts or {}, global_diagnostic_options) --- @type table for k in pairs(global_diagnostic_options) do if resolved[k] ~= nil then resolved[k] = resolve_optional_value(k, resolved[k], namespace, bufnr) @@ -205,9 +339,11 @@ local diagnostic_severities = { [M.severity.HINT] = { ctermfg = 7, guifg = 'LightGrey' }, } --- Make a map from DiagnosticSeverity -> Highlight Name +--- Make a map from vim.diagnostic.Severity -> Highlight Name +--- @param base_name string +--- @return table local function make_highlight_map(base_name) - local result = {} + local result = {} --- @type table for k in pairs(diagnostic_severities) do local name = M.severity[k] name = name:sub(1, 1) .. name:sub(2):lower() @@ -217,6 +353,8 @@ local function make_highlight_map(base_name) return result end +-- TODO(lewis6991): these highlight maps can only be indexed with an integer, however there usage +-- implies they can be indexed with any vim.diagnostic.Severity local virtual_text_highlight_map = make_highlight_map('VirtualText') local underline_highlight_map = make_highlight_map('Underline') local floating_highlight_map = make_highlight_map('Floating') @@ -229,12 +367,14 @@ local function get_bufnr(bufnr) return bufnr end +--- @param diagnostics vim.Diagnostic[] +--- @return table local function diagnostic_lines(diagnostics) if not diagnostics then return {} end - local diagnostics_by_line = {} + local diagnostics_by_line = {} --- @type table for _, diagnostic in ipairs(diagnostics) do local line_diagnostics = diagnostics_by_line[diagnostic.lnum] if not line_diagnostics then @@ -246,6 +386,9 @@ local function diagnostic_lines(diagnostics) return diagnostics_by_line end +--- @param namespace integer +--- @param bufnr integer +--- @param diagnostics vim.Diagnostic[] local function set_diagnostic_cache(namespace, bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do assert(diagnostic.lnum, 'Diagnostic line number is required') @@ -260,10 +403,12 @@ local function set_diagnostic_cache(namespace, bufnr, diagnostics) diagnostic_cache[bufnr][namespace] = diagnostics end +--- @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 = {} + local found = {} --- @type table 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 @@ -281,6 +426,8 @@ local function restore_extmarks(bufnr, last) end end +--- @param namespace integer +--- @param bufnr? integer local function save_extmarks(namespace, bufnr) bufnr = get_bufnr(bufnr) if not diagnostic_attached_buffers[bufnr] then @@ -298,6 +445,7 @@ local function save_extmarks(namespace, bufnr) api.nvim_buf_get_extmarks(bufnr, namespace, 0, -1, { details = true }) end +--- @type table local registered_autocmds = {} local function make_augroup_key(namespace, bufnr) @@ -305,6 +453,8 @@ local function make_augroup_key(namespace, bufnr) return string.format('DiagnosticInsertLeave:%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 @@ -320,6 +470,9 @@ end --- 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 +--- @param args any[] local function schedule_display(namespace, bufnr, args) bufs_waiting_to_update[bufnr][namespace] = args @@ -338,6 +491,8 @@ local function schedule_display(namespace, bufnr, args) end end +--- @param namespace integer +--- @param bufnr integer local function clear_scheduled_display(namespace, bufnr) local key = make_augroup_key(namespace, bufnr) @@ -347,6 +502,10 @@ local function clear_scheduled_display(namespace, bufnr) end end +--- @param bufnr integer? +--- @param opts vim.diagnostic.GetOpts? +--- @param clamp boolean +--- @return vim.Diagnostic[] local function get_diagnostics(bufnr, opts, clamp) opts = opts or {} @@ -354,13 +513,19 @@ local function get_diagnostics(bufnr, opts, clamp) local diagnostics = {} -- Memoized results of buf_line_count per bufnr + --- @type table local buf_line_count = setmetatable({}, { + --- @param t table + --- @param k integer + --- @return integer __index = function(t, k) t[k] = api.nvim_buf_line_count(k) return rawget(t, k) end, }) + ---@param b integer + ---@param d vim.Diagnostic local function add(b, d) if not opts.lnum or d.lnum == opts.lnum then if clamp and api.nvim_buf_is_loaded(b) then @@ -375,7 +540,7 @@ local function get_diagnostics(bufnr, opts, clamp) 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.end_lnum = math.max(math.min(assert(d.end_lnum), line_count), 0) d.col = math.max(d.col, 0) d.end_col = math.max(d.end_col, 0) end @@ -384,6 +549,8 @@ local function get_diagnostics(bufnr, opts, clamp) end end + --- @param buf integer + --- @param diags vim.Diagnostic[] local function add_all_diags(buf, diags) for _, diagnostic in pairs(diags) do add(buf, diagnostic) @@ -417,18 +584,20 @@ local function get_diagnostics(bufnr, opts, clamp) return diagnostics end +--- @param loclist boolean +--- @param opts vim.diagnostic.setqflist.Opts|vim.diagnostic.setloclist.Opts? local function set_list(loclist, opts) opts = opts or {} - local open = vim.F.if_nil(opts.open, true) + local open = if_nil(opts.open, true) local title = opts.title or 'Diagnostics' local winnr = opts.winnr or 0 - local bufnr + local bufnr --- @type integer? if loclist then bufnr = api.nvim_win_get_buf(winnr) end -- Don't clamp line numbers since the quickfix list can already handle line -- numbers beyond the end of the buffer - local diagnostics = get_diagnostics(bufnr, opts, false) + local diagnostics = get_diagnostics(bufnr, opts --[[@as vim.diagnostic.GetOpts]], false) local items = M.toqflist(diagnostics) if loclist then vim.fn.setloclist(winnr, {}, ' ', { title = title, items = items }) @@ -440,10 +609,16 @@ local function set_list(loclist, opts) end end +--- @param position {[1]: integer, [2]: integer} +--- @param search_forward boolean +--- @param bufnr integer +--- @param opts vim.diagnostic.GotoOpts +--- @param namespace integer +--- @return vim.Diagnostic? local function next_diagnostic(position, search_forward, bufnr, opts, namespace) position[1] = position[1] - 1 bufnr = get_bufnr(bufnr) - local wrap = vim.F.if_nil(opts.wrap, true) + local wrap = if_nil(opts.wrap, true) local line_count = api.nvim_buf_line_count(bufnr) local diagnostics = get_diagnostics(bufnr, vim.tbl_extend('keep', opts, { namespace = namespace }), true) @@ -460,6 +635,7 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace) 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] + --- @type function, function local sort_diagnostics, is_next if search_forward then sort_diagnostics = function(a, b) @@ -478,7 +654,9 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace) end table.sort(line_diagnostics[lnum], sort_diagnostics) if i == 0 then - for _, v in pairs(line_diagnostics[lnum]) do + for _, v in + pairs(line_diagnostics[lnum] --[[@as table]]) + do if is_next(v) then return v end @@ -490,10 +668,12 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace) end end +--- @param opts vim.diagnostic.GotoOpts? +--- @param pos {[1]:integer,[2]:integer}|false local function diagnostic_move_pos(opts, pos) opts = opts or {} - local float = vim.F.if_nil(opts.float, true) + local float = if_nil(opts.float, true) local win_id = opts.win_id or api.nvim_get_current_win() if not pos then @@ -549,8 +729,8 @@ end --- - `table`: Enable this feature with overrides. Use an empty table to use default values. --- - `function`: Function with signature (namespace, bufnr) that returns any of the above. --- ----@param opts table|nil When omitted or "nil", retrieve the current configuration. Otherwise, a ---- configuration table with the following keys: +---@param opts vim.diagnostic.Opts? (table?) When omitted or "nil", retrieve the current +--- configuration. Otherwise, a configuration table with the following keys: --- - underline: (default true) Use underline for diagnostics. Options: --- * severity: Only underline diagnostics matching the given --- severity |diagnostic-severity| @@ -616,17 +796,17 @@ end --- Options: --- * reverse: (boolean) Reverse sort order --- ----@param namespace integer|nil Update the options for the given namespace. When omitted, update the ---- global diagnostic options. +---@param namespace integer? Update the options for the given namespace. When omitted, update the +--- global diagnostic options. --- ----@return table|nil table of current diagnostic config if `opts` is omitted. +---@return vim.diagnostic.Opts? (table?) table of current diagnostic config if `opts` is omitted. function M.config(opts, namespace) vim.validate({ opts = { opts, 't', true }, namespace = { namespace, 'n', true }, }) - local t + local t --- @type vim.diagnostic.Opts if namespace then local ns = M.get_namespace(namespace) t = ns.opts @@ -639,7 +819,9 @@ function M.config(opts, namespace) return vim.deepcopy(t, true) end - for k, v in pairs(opts) do + for k, v in + pairs(opts --[[@as table]]) + do t[k] = v end @@ -662,8 +844,8 @@ end --- ---@param namespace integer The diagnostic namespace ---@param bufnr integer Buffer number ----@param diagnostics table A list of diagnostic items |diagnostic-structure| ----@param opts table|nil Display options to pass to |vim.diagnostic.show()| +---@param diagnostics vim.Diagnostic[] A list of diagnostic items |diagnostic-structure| +---@param opts? vim.diagnostic.Opts (table) Display options to pass to |vim.diagnostic.show()| function M.set(namespace, bufnr, diagnostics, opts) vim.validate({ namespace = { namespace, 'n' }, @@ -689,6 +871,7 @@ function M.set(namespace, bufnr, diagnostics, opts) api.nvim_exec_autocmds('DiagnosticChanged', { modeline = false, buffer = bufnr, + -- TODO(lewis6991): should this be deepcopy()'d like they are in vim.diagnostic.get() data = { diagnostics = diagnostics }, }) end @@ -696,11 +879,11 @@ end --- Get namespace metadata. --- ---@param namespace integer Diagnostic namespace ----@return table Namespace metadata +---@return vim.diagnostic.NS (table) Namespace metadata function M.get_namespace(namespace) vim.validate({ namespace = { namespace, 'n' } }) if not all_namespaces[namespace] then - local name + local name --- @type string? for k, v in pairs(api.nvim_get_namespaces()) do if namespace == v then name = k @@ -721,35 +904,22 @@ end --- Get current diagnostic namespaces. --- ----@return table A list of active diagnostic namespaces |vim.diagnostic|. +---@return table A list of active diagnostic namespaces |vim.diagnostic|. function M.get_namespaces() return vim.deepcopy(all_namespaces, true) end ----@class Diagnostic ----@field bufnr? integer ----@field lnum integer 0-indexed ----@field end_lnum? integer 0-indexed ----@field col integer 0-indexed ----@field end_col? integer 0-indexed ----@field severity? DiagnosticSeverity ----@field message string ----@field source? string ----@field code? string ----@field _tags? { deprecated: boolean, unnecessary: boolean} ----@field user_data? any arbitrary data plugins can add - --- Get current diagnostics. --- --- Modifying diagnostics in the returned table has no effect. To set diagnostics in a buffer, use |vim.diagnostic.set()|. --- ----@param bufnr integer|nil Buffer number to get diagnostics from. Use 0 for +---@param bufnr integer? Buffer number to get diagnostics from. Use 0 for --- current buffer or nil for all buffers. ----@param opts table|nil A table with the following keys: +---@param opts? vim.diagnostic.GetOpts (table) A table with the following keys: --- - namespace: (number) Limit diagnostics to the given namespace. --- - lnum: (number) Limit diagnostics to the given line number. --- - severity: See |diagnostic-severity|. ----@return Diagnostic[] table A list of diagnostic items |diagnostic-structure|. Keys `bufnr`, `end_lnum`, `end_col`, and `severity` are guaranteed to be present. +---@return vim.Diagnostic[] table A list of diagnostic items |diagnostic-structure|. Keys `bufnr`, `end_lnum`, `end_col`, and `severity` are guaranteed to be present. function M.get(bufnr, opts) vim.validate({ bufnr = { bufnr, 'n', true }, @@ -761,12 +931,12 @@ end --- Get current diagnostics count. --- ----@param bufnr integer|nil Buffer number to get diagnostics from. Use 0 for ---- current buffer or nil for all buffers. ----@param opts table|nil A table with the following keys: ---- - namespace: (number) Limit diagnostics to the given namespace. ---- - lnum: (number) Limit diagnostics to the given line number. ---- - severity: See |diagnostic-severity|. +---@param bufnr? integer Buffer number to get diagnostics from. Use 0 for +--- current buffer or nil for all buffers. +---@param opts? table A table with the following keys: +--- - namespace: (number) Limit diagnostics to the given namespace. +--- - lnum: (number) Limit diagnostics to the given line number. +--- - severity: See |diagnostic-severity|. ---@return table A table with actually present severity values as keys (see |diagnostic-severity|) and integer counts as values. function M.count(bufnr, opts) vim.validate({ @@ -775,9 +945,9 @@ function M.count(bufnr, opts) }) local diagnostics = get_diagnostics(bufnr, opts, false) - local count = {} + local count = {} --- @type table for i = 1, #diagnostics do - local severity = diagnostics[i].severity + local severity = diagnostics[i].severity --[[@as integer]] count[severity] = (count[severity] or 0) + 1 end return count @@ -785,8 +955,8 @@ end --- Get the previous diagnostic closest to the cursor position. --- ----@param opts nil|table See |vim.diagnostic.goto_next()| ----@return Diagnostic|nil Previous diagnostic +---@param opts? vim.diagnostic.GotoOpts (table) See |vim.diagnostic.goto_next()| +---@return vim.Diagnostic? Previous diagnostic function M.get_prev(opts) opts = opts or {} @@ -799,9 +969,9 @@ end --- Return the position of the previous diagnostic in the current buffer. --- ----@param opts table|nil See |vim.diagnostic.goto_next()| ----@return table|false Previous diagnostic position as a (row, col) tuple or false if there is no ---- prior diagnostic +---@param opts? vim.diagnostic.GotoOpts (table) See |vim.diagnostic.goto_next()| +---@return table|false: Previous diagnostic position as a (row, col) tuple or false if there is no +--- prior diagnostic function M.get_prev_pos(opts) local prev = M.get_prev(opts) if not prev then @@ -812,15 +982,15 @@ function M.get_prev_pos(opts) end --- Move to the previous diagnostic in the current buffer. ----@param opts table|nil See |vim.diagnostic.goto_next()| +---@param opts? vim.diagnostic.GotoOpts (table) See |vim.diagnostic.goto_next()| function M.goto_prev(opts) return diagnostic_move_pos(opts, M.get_prev_pos(opts)) end --- Get the next diagnostic closest to the cursor position. --- ----@param opts table|nil See |vim.diagnostic.goto_next()| ----@return Diagnostic|nil Next diagnostic +---@param opts? vim.diagnostic.GotoOpts (table) See |vim.diagnostic.goto_next()| +---@return vim.Diagnostic? : Next diagnostic function M.get_next(opts) opts = opts or {} @@ -833,9 +1003,9 @@ end --- Return the position of the next diagnostic in the current buffer. --- ----@param opts table|nil See |vim.diagnostic.goto_next()| ----@return table|false Next diagnostic position as a (row, col) tuple or false if no next ---- diagnostic. +---@param opts? vim.diagnostic.GotoOpts (table) See |vim.diagnostic.goto_next()| +---@return table|false : Next diagnostic position as a (row, col) tuple or false if no next +--- diagnostic. function M.get_next_pos(opts) local next = M.get_next(opts) if not next then @@ -845,10 +1015,21 @@ function M.get_next_pos(opts) return { next.lnum, next.col } end +--- @class vim.diagnostic.GetOpts +--- @field namespace? integer +--- @field lnum? integer +--- @field severity? vim.diagnostic.Severity + +--- @class vim.diagnostic.GotoOpts : vim.diagnostic.GetOpts +--- @field cursor_position? {[1]:integer,[2]:integer} +--- @field wrap? integer +--- @field float? boolean|vim.diagnostic.Opts.Float +--- @field win_id? integer + --- Move to the next diagnostic. --- ----@param opts table|nil Configuration table with the following keys: ---- - namespace: (number) Only consider diagnostics from the given namespace. +---@param opts? vim.diagnostic.GotoOpts (table) Configuration table with the following keys: +--- - namespace: (integer) Only consider diagnostics from the given namespace. --- - cursor_position: (cursor position) Cursor position as a (row, col) tuple. --- See |nvim_win_get_cursor()|. Defaults to the current cursor position. --- - wrap: (boolean, default true) Whether to loop around file or not. Similar to 'wrapscan'. @@ -860,7 +1041,7 @@ end --- the "scope" option). --- - win_id: (number, default 0) Window ID function M.goto_next(opts) - return diagnostic_move_pos(opts, M.get_next_pos(opts)) + diagnostic_move_pos(opts, M.get_next_pos(opts)) end M.handlers.signs = { @@ -885,7 +1066,7 @@ M.handlers.signs = { -- 10 is the default sign priority when none is explicitly specified local priority = opts.signs and opts.signs.priority or 10 - local get_priority + local get_priority --- @type function if opts.severity_sort then if type(opts.severity_sort) == 'table' and opts.severity_sort.reverse then get_priority = function(severity) @@ -951,7 +1132,7 @@ M.handlers.signs = { end end - local text = {} ---@type table + local text = {} ---@type table for k in pairs(M.severity) do if opts.signs.text and opts.signs.text[k] then text[k] = opts.signs.text[k] @@ -975,6 +1156,9 @@ M.handlers.signs = { end end end, + + --- @param namespace integer + --- @param bufnr integer hide = function(namespace, bufnr) local ns = M.get_namespace(namespace) if ns.user_data.sign_ns and api.nvim_buf_is_valid(bufnr) then @@ -1015,10 +1199,12 @@ M.handlers.underline = { local underline_ns = ns.user_data.underline_ns for _, diagnostic in ipairs(diagnostics) do - local higroup = underline_highlight_map[diagnostic.severity] + --- @type string? + local higroup = underline_highlight_map[assert(diagnostic.severity)] if higroup == nil then -- Default to error if we don't have a highlight associated + -- TODO(lewis6991): this is always nil since underline_highlight_map only has integer keys higroup = underline_highlight_map.Error end @@ -1074,7 +1260,7 @@ M.handlers.virtual_text = { return end - local severity + local severity --- @type vim.diagnostic.Severity? if opts.virtual_text then if opts.virtual_text.format then diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics) @@ -1132,7 +1318,9 @@ M.handlers.virtual_text = { --- Exported for backward compatibility with --- vim.lsp.diagnostic.get_virtual_text_chunks_for_line(). When that function is eventually removed, --- this can be made local. ----@private +--- @private +--- @param line_diags table +--- @param opts vim.diagnostic.Opts.VirtualText function M._get_virt_text_chunks(line_diags, opts) if #line_diags == 0 then return nil @@ -1182,10 +1370,10 @@ end --- To hide diagnostics and prevent them from re-displaying, use --- |vim.diagnostic.disable()|. --- ----@param namespace integer|nil Diagnostic namespace. When omitted, hide ---- diagnostics from all namespaces. ----@param bufnr integer|nil Buffer number, or 0 for current buffer. When ---- omitted, hide diagnostics in all buffers. +---@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, 'n', true }, @@ -1207,11 +1395,11 @@ end --- Check whether diagnostics are disabled in a given buffer. --- ----@param bufnr integer|nil Buffer number, or 0 for current buffer. ----@param namespace integer|nil Diagnostic namespace. When omitted, checks if ---- all diagnostics are disabled in {bufnr}. ---- Otherwise, only checks if diagnostics from ---- {namespace} are disabled. +---@param bufnr integer? Buffer number, or 0 for current buffer. +---@param namespace integer? Diagnostic namespace. When omitted, checks if +--- all diagnostics are disabled in {bufnr}. +--- Otherwise, only checks if diagnostics from +--- {namespace} are disabled. ---@return boolean function M.is_disabled(bufnr, namespace) bufnr = get_bufnr(bufnr) @@ -1228,17 +1416,17 @@ end --- Display diagnostics for the given namespace and buffer. --- ----@param namespace integer|nil Diagnostic namespace. When omitted, show ---- diagnostics from all namespaces. ----@param bufnr integer|nil Buffer number, or 0 for current buffer. When omitted, show ---- diagnostics in all buffers. ----@param diagnostics table|nil The diagnostics to display. When omitted, use the +---@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 table|nil Display options. See |vim.diagnostic.config()|. +---@param opts? vim.diagnostic.Opts (table) Display options. See |vim.diagnostic.config()|. function M.show(namespace, bufnr, diagnostics, opts) vim.validate({ namespace = { namespace, 'n', true }, @@ -1277,24 +1465,24 @@ function M.show(namespace, bufnr, diagnostics, opts) diagnostics = diagnostics or get_diagnostics(bufnr, { namespace = namespace }, true) - if not diagnostics or vim.tbl_isempty(diagnostics) then + if vim.tbl_isempty(diagnostics) then return end - opts = get_resolved_options(opts, namespace, bufnr) + local opts_res = get_resolved_options(opts, namespace, bufnr) - if opts.update_in_insert then + if opts_res.update_in_insert then clear_scheduled_display(namespace, bufnr) else local mode = api.nvim_get_mode() if string.sub(mode.mode, 1, 1) == 'i' then - schedule_display(namespace, bufnr, opts) + schedule_display(namespace, bufnr, opts_res) return end end - if vim.F.if_nil(opts.severity_sort, false) then - if type(opts.severity_sort) == 'table' and opts.severity_sort.reverse then + if if_nil(opts_res.severity_sort, false) 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) @@ -1306,15 +1494,15 @@ function M.show(namespace, bufnr, diagnostics, opts) end for handler_name, handler in pairs(M.handlers) do - if handler.show and opts[handler_name] then - handler.show(namespace, bufnr, diagnostics, opts) + if handler.show and opts_res[handler_name] then + handler.show(namespace, bufnr, diagnostics, opts_res) end end end --- Show diagnostics in a floating window. --- ----@param opts table|nil Configuration table with the same keys +---@param opts vim.diagnostic.Opts.Float? (table?) Configuration table with the same keys --- as |vim.lsp.util.open_floating_preview()| in addition to the following: --- - bufnr: (number) Buffer number to show diagnostics from. --- Defaults to the current buffer. @@ -1354,13 +1542,13 @@ end --- Overrides the setting from |vim.diagnostic.config()|. --- - suffix: Same as {prefix}, but appends the text to the diagnostic instead of --- prepending it. Overrides the setting from |vim.diagnostic.config()|. ----@return integer|nil, integer|nil: ({float_bufnr}, {win_id}) +---@return integer?, integer?: ({float_bufnr}, {win_id}) function M.open_float(opts, ...) -- Support old (bufnr, opts) signature - local bufnr + local bufnr --- @type integer? if opts == nil or type(opts) == 'number' then bufnr = opts - opts = ... + opts = ... --- @type vim.diagnostic.Opts.Float else vim.validate({ opts = { opts, 't', true }, @@ -1382,16 +1570,17 @@ function M.open_float(opts, ...) end local scope = ({ l = 'line', c = 'cursor', b = 'buffer' })[opts.scope] or opts.scope or 'line' - local lnum, col + local lnum, col --- @type integer, integer + local opts_pos = opts.pos if scope == 'line' or scope == 'cursor' then - if not opts.pos 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 = unpack(opts.pos) + 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 @@ -1399,15 +1588,17 @@ function M.open_float(opts, ...) error("Invalid value for option 'scope'") end - local diagnostics = get_diagnostics(bufnr, opts, true) + local diagnostics = get_diagnostics(bufnr, opts --[[@as vim.diagnostic.GetOpts]], true) if scope == 'line' then + --- @param d vim.Diagnostic diagnostics = vim.tbl_filter(function(d) return d.lnum == lnum end, diagnostics) elseif scope == 'cursor' then -- LSP servers can send diagnostics with `end_col` past the length of the line local line_length = #api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1] + --- @param d vim.Diagnostic diagnostics = vim.tbl_filter(function(d) return d.lnum == lnum and math.min(d.col, line_length - 1) <= col @@ -1419,7 +1610,7 @@ function M.open_float(opts, ...) return end - local severity_sort = vim.F.if_nil(opts.severity_sort, global_diagnostic_options.severity_sort) + local severity_sort = if_nil(opts.severity_sort, global_diagnostic_options.severity_sort) if severity_sort then if type(severity_sort) == 'table' and severity_sort.reverse then table.sort(diagnostics, function(a, b) @@ -1432,8 +1623,8 @@ function M.open_float(opts, ...) end end - local lines = {} - local highlights = {} + local lines = {} --- @type string[] + local highlights = {} --- @type table[] local header = if_nil(opts.header, 'Diagnostics:') if header then vim.validate({ @@ -1468,7 +1659,7 @@ function M.open_float(opts, ...) return string.format('%d. ', i) end) - local prefix, prefix_hl_group + local prefix, prefix_hl_group --- @type string?, string? if prefix_opt then vim.validate({ prefix = { @@ -1488,7 +1679,7 @@ function M.open_float(opts, ...) return diagnostic.code and string.format(' [%s]', diagnostic.code) or '' end) - local suffix, suffix_hl_group + local suffix, suffix_hl_group --- @type string?, string? if suffix_opt then vim.validate({ suffix = { @@ -1505,15 +1696,18 @@ function M.open_float(opts, ...) end for i, diagnostic in ipairs(diagnostics) do - if prefix_opt and type(prefix_opt) == 'function' then - prefix, prefix_hl_group = prefix_opt(diagnostic, i, #diagnostics) - prefix, prefix_hl_group = prefix or '', prefix_hl_group or 'NormalFloat' + if type(prefix_opt) == 'function' then + --- @cast prefix_opt fun(...): string?, string? + local prefix0, prefix_hl_group0 = prefix_opt(diagnostic, i, #diagnostics) + prefix, prefix_hl_group = prefix0 or '', prefix_hl_group0 or 'NormalFloat' end - if suffix_opt and