diff options
Diffstat (limited to 'runtime/lua/vim/diagnostic.lua')
-rw-r--r-- | runtime/lua/vim/diagnostic.lua | 717 |
1 files changed, 391 insertions, 326 deletions
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 326932d982..b30a678eeb 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -1,3 +1,5 @@ +local if_nil = vim.F.if_nil + local M = {} M.severity = { @@ -19,10 +21,21 @@ local global_diagnostic_options = { signs = true, underline = true, virtual_text = true, + float = true, update_in_insert = false, severity_sort = false, } +M.handlers = setmetatable({}, { + __newindex = function(t, name, handler) + vim.validate { handler = {handler, "t" } } + rawset(t, name, handler) + if not global_diagnostic_options[name] then + global_diagnostic_options[name] = true + end + end, +}) + -- Local functions {{{ ---@private @@ -54,7 +67,7 @@ end local function prefix_source(source, diagnostics) vim.validate { source = {source, function(v) return v == "always" or v == "if_many" - end, "Invalid value for option 'source'" } } + end, "'always' or 'if_many'" } } if source == "if_many" then local sources = {} @@ -96,31 +109,9 @@ end local all_namespaces = {} ---@private -local function get_namespace(ns) - if not all_namespaces[ns] then - local name - for k, v in pairs(vim.api.nvim_get_namespaces()) do - if ns == v then - name = k - break - end - end - - assert(name, "namespace does not exist or is anonymous") - - all_namespaces[ns] = { - name = name, - sign_group = string.format("vim.diagnostic.%s", name), - opts = {} - } - end - return all_namespaces[ns] -end - ----@private local function enabled_value(option, namespace) - local ns = get_namespace(namespace) - if type(ns.opts[option]) == "table" then + local ns = namespace and M.get_namespace(namespace) or {} + if ns.opts and type(ns.opts[option]) == "table" then return ns.opts[option] end @@ -153,8 +144,9 @@ end ---@private local function get_resolved_options(opts, namespace, bufnr) - local ns = get_namespace(namespace) - local resolved = vim.tbl_extend('keep', opts or {}, ns.opts, global_diagnostic_options) + 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) for k in pairs(global_diagnostic_options) do if resolved[k] ~= nil then resolved[k] = resolve_optional_value(k, resolved[k], namespace, bufnr) @@ -323,7 +315,7 @@ end ---@private local function save_extmarks(namespace, bufnr) - bufnr = bufnr == 0 and vim.api.nvim_get_current_buf() or bufnr + bufnr = get_bufnr(bufnr) if not diagnostic_attached_buffers[bufnr] then vim.api.nvim_buf_attach(bufnr, false, { on_lines = function(_, _, _, _, _, last) @@ -341,7 +333,7 @@ local registered_autocmds = {} ---@private local function make_augroup_key(namespace, bufnr) - local ns = get_namespace(namespace) + local ns = M.get_namespace(namespace) return string.format("DiagnosticInsertLeave:%s:%s", bufnr, ns.name) end @@ -354,19 +346,15 @@ local function schedule_display(namespace, bufnr, args) local key = make_augroup_key(namespace, bufnr) if not registered_autocmds[key] then - vim.cmd(string.format("augroup %s", key)) - vim.cmd(" au!") - vim.cmd( - string.format( - [[autocmd %s <buffer=%s> lua vim.diagnostic._execute_scheduled_display(%s, %s)]], - table.concat(insert_leave_auto_cmds, ","), - bufnr, - namespace, - bufnr - ) - ) - vim.cmd("augroup END") - + vim.cmd(string.format([[augroup %s + au! + autocmd %s <buffer=%s> lua vim.diagnostic._execute_scheduled_display(%s, %s) + augroup END]], + key, + table.concat(insert_leave_auto_cmds, ","), + bufnr, + namespace, + bufnr)) registered_autocmds[key] = true end end @@ -376,77 +364,14 @@ local function clear_scheduled_display(namespace, bufnr) local key = make_augroup_key(namespace, bufnr) if registered_autocmds[key] then - vim.cmd(string.format("augroup %s", key)) - vim.cmd(" au!") - vim.cmd("augroup END") - + vim.cmd(string.format([[augroup %s + au! + augroup END]], key)) registered_autocmds[key] = nil end end ---@private ---- Open a floating window with the provided diagnostics ----@param opts table Configuration table ---- - show_header (boolean, default true): Show "Diagnostics:" header ---- - all opts for |vim.util.open_floating_preview()| can be used here ----@param diagnostics table: The diagnostics to display ----@return table {popup_bufnr, win_id} -local function show_diagnostics(opts, diagnostics) - if not diagnostics or vim.tbl_isempty(diagnostics) then - return - end - local lines = {} - local highlights = {} - local show_header = vim.F.if_nil(opts.show_header, true) - if show_header then - table.insert(lines, "Diagnostics:") - table.insert(highlights, {0, "Bold"}) - end - - if opts.format then - diagnostics = reformat_diagnostics(opts.format, diagnostics) - end - - if opts.source then - diagnostics = prefix_source(opts.source, diagnostics) - end - - -- Use global setting for severity_sort since 'show_diagnostics' is namespace - -- independent - local 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) return a.severity > b.severity end) - else - table.sort(diagnostics, function(a, b) return a.severity < b.severity end) - end - end - - for i, diagnostic in ipairs(diagnostics) do - local prefix = string.format("%d. ", i) - local hiname = floating_highlight_map[diagnostic.severity] - assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity)) - - local message_lines = vim.split(diagnostic.message, '\n', true) - table.insert(lines, prefix..message_lines[1]) - table.insert(highlights, {#prefix, hiname}) - for j = 2, #message_lines do - table.insert(lines, string.rep(' ', #prefix) .. message_lines[j]) - table.insert(highlights, {0, hiname}) - end - end - - local popup_bufnr, winnr = require('vim.lsp.util').open_floating_preview(lines, 'plaintext', opts) - for i, hi in ipairs(highlights) do - local prefixlen, hiname = unpack(hi) - -- Start highlight after the prefix - vim.api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1) - end - - return popup_bufnr, winnr -end - ----@private local function set_list(loclist, opts) opts = opts or {} local open = vim.F.if_nil(opts.open, true) @@ -469,6 +394,7 @@ local function set_list(loclist, opts) end ---@private +--- To (slightly) improve performance, modifies diagnostics in place. local function clamp_line_numbers(bufnr, diagnostics) local buf_line_count = vim.api.nvim_buf_line_count(bufnr) if buf_line_count == 0 then @@ -526,7 +452,7 @@ end local function diagnostic_move_pos(opts, pos) opts = opts or {} - local enable_popup = vim.F.if_nil(opts.enable_popup, true) + local float = vim.F.if_nil(opts.float, true) local win_id = opts.win_id or vim.api.nvim_get_current_win() if not pos then @@ -539,10 +465,13 @@ local function diagnostic_move_pos(opts, pos) vim.api.nvim_win_set_cursor(win_id, {pos[1] + 1, pos[2]}) - if enable_popup then - -- This is a bit weird... I'm surprised that we need to wait til the next tick to do this. + if float then + local float_opts = type(float) == "table" and float or {} vim.schedule(function() - M.show_position_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id)) + M.open_float( + vim.api.nvim_win_get_buf(win_id), + vim.tbl_extend("keep", float_opts, {scope="cursor"}) + ) end) end end @@ -561,12 +490,12 @@ end --- --- For example, if a user enables virtual text globally with --- <pre> ---- vim.diagnostic.config({virt_text = true}) +--- vim.diagnostic.config({virtual_text = true}) --- </pre> --- --- and a diagnostic producer sets diagnostics with --- <pre> ---- vim.diagnostic.set(ns, 0, diagnostics, {virt_text = false}) +--- vim.diagnostic.set(ns, 0, diagnostics, {virtual_text = false}) --- </pre> --- --- then virtual text will not be enabled for those diagnostics. @@ -603,6 +532,19 @@ 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. +--- - float: Options for floating windows: +--- * severity: See |diagnostic-severity|. +--- * show_header: (boolean, default true) Show "Diagnostics:" header +--- * source: (string) Include the diagnostic source in +--- the message. One of "always" or "if_many". +--- * format: (function) A function that takes a diagnostic as input and returns a +--- string. The return value is the text used to display the diagnostic. +--- * prefix: (function or string) Prefix each diagnostic in the floating window. If +--- a function, it must have the signature (diagnostic, i, total) -> string, +--- where {i} is the index of the diagnostic being evaluated and {total} is +--- the total number of diagnostics displayed in the window. The returned +--- string is prepended to each diagnostic in the window. Otherwise, +--- if {prefix} is a string, it is prepended to each diagnostic. --- - update_in_insert: (default false) Update diagnostics in Insert mode (if false, --- diagnostics are updated on InsertLeave) --- - severity_sort: (default false) Sort diagnostics by severity. This affects the order in @@ -620,7 +562,7 @@ function M.config(opts, namespace) local t if namespace then - local ns = get_namespace(namespace) + local ns = M.get_namespace(namespace) t = ns.opts else t = global_diagnostic_options @@ -672,7 +614,7 @@ function M.set(namespace, bufnr, diagnostics, opts) -- Clean up our data when the buffer unloads. vim.api.nvim_buf_attach(bufnr, false, { on_detach = function(_, b) - clear_diagnostic_cache(b, namespace) + clear_diagnostic_cache(namespace, b) diagnostic_cleanup[b][namespace] = nil end }) @@ -687,6 +629,32 @@ function M.set(namespace, bufnr, diagnostics, opts) vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged") end +--- Get namespace metadata. +--- +---@param ns number Diagnostic namespace +---@return table Namespace metadata +function M.get_namespace(namespace) + vim.validate { namespace = { namespace, 'n' } } + if not all_namespaces[namespace] then + local name + for k, v in pairs(vim.api.nvim_get_namespaces()) do + if namespace == v then + name = k + break + end + end + + assert(name, "namespace does not exist or is anonymous") + + all_namespaces[namespace] = { + name = name, + opts = {}, + user_data = {}, + } + end + return all_namespaces[namespace] +end + --- Get current diagnostic namespaces. --- ---@return table A list of active diagnostic namespaces |vim.diagnostic|. @@ -825,10 +793,9 @@ end --- |nvim_win_get_cursor()|. Defaults to the current cursor position. --- - wrap: (boolean, default true) Whether to loop around file or not. Similar to 'wrapscan'. --- - severity: See |diagnostic-severity|. ---- - enable_popup: (boolean, default true) Call |vim.diagnostic.show_line_diagnostics()| ---- on jump. ---- - popup_opts: (table) Table to pass as {opts} parameter to ---- |vim.diagnostic.show_line_diagnostics()| +--- - float: (boolean or table, default true) If "true", call |vim.diagnostic.open_float()| +--- after moving. If a table, pass the table as the {opts} parameter to +--- |vim.diagnostic.open_float()|. --- - win_id: (number, default 0) Window ID function M.goto_next(opts) return diagnostic_move_pos( @@ -837,156 +804,171 @@ function M.goto_next(opts) ) end --- Diagnostic Setters {{{ - ---- Set signs for given diagnostics. ---- ----@param namespace number The diagnostic namespace ----@param bufnr number Buffer number ----@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the ---- current diagnostics in the given buffer are used. ----@param opts table Configuration table with the following keys: ---- - priority: Set the priority of the signs |sign-priority|. ----@private -function M._set_signs(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, - opts = {opts, 't', true}, - } +M.handlers.signs = { + show = function(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } - bufnr = get_bufnr(bufnr) - opts = get_resolved_options({ signs = opts }, namespace, bufnr) + bufnr = get_bufnr(bufnr) - if opts.signs and opts.signs.severity then - diagnostics = filter_by_severity(opts.signs.severity, diagnostics) - end - - local ns = get_namespace(namespace) + if opts.signs and opts.signs.severity then + diagnostics = filter_by_severity(opts.signs.severity, diagnostics) + end - define_default_signs() + 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 - if opts.severity_sort then - if type(opts.severity_sort) == "table" and opts.severity_sort.reverse then - get_priority = function(severity) - return priority + (severity - vim.diagnostic.severity.ERROR) + -- 10 is the default sign priority when none is explicitly specified + local priority = opts.signs and opts.signs.priority or 10 + local get_priority + if opts.severity_sort then + if type(opts.severity_sort) == "table" and opts.severity_sort.reverse then + get_priority = function(severity) + return priority + (severity - vim.diagnostic.severity.ERROR) + end + else + get_priority = function(severity) + return priority + (vim.diagnostic.severity.HINT - severity) + end end else - get_priority = function(severity) - return priority + (vim.diagnostic.severity.HINT - severity) + get_priority = function() + return priority end end - else - get_priority = function() - return priority - end - end - - for _, diagnostic in ipairs(diagnostics) do - vim.fn.sign_place( - 0, - ns.sign_group, - sign_highlight_map[diagnostic.severity], - bufnr, - { - priority = get_priority(diagnostic.severity), - lnum = diagnostic.lnum + 1 - } - ) - end -end ---- Set underline for given diagnostics. ---- ----@param namespace number The diagnostic namespace ----@param bufnr number Buffer number ----@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the ---- current diagnostics in the given buffer are used. ----@param opts table Configuration table. Currently unused. ----@private -function M._set_underline(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, - opts = {opts, 't', true}, - } + 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) + end - bufnr = get_bufnr(bufnr) - opts = get_resolved_options({ underline = opts }, namespace, bufnr).underline + 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 + } + ) + end + end, + hide = function(namespace, bufnr) + local ns = M.get_namespace(namespace) + if ns.user_data.sign_group then + vim.fn.sign_unplace(ns.user_data.sign_group, {buffer=bufnr}) + end + end, +} - if opts and opts.severity then - diagnostics = filter_by_severity(opts.severity, diagnostics) - end +M.handlers.underline = { + show = function(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } - for _, diagnostic in ipairs(diagnostics) do - local higroup = underline_highlight_map[diagnostic.severity] + bufnr = get_bufnr(bufnr) - if higroup == nil then - -- Default to error if we don't have a highlight associated - higroup = underline_highlight_map.Error + if opts.underline and opts.underline.severity then + diagnostics = filter_by_severity(opts.underline.severity, diagnostics) end - vim.highlight.range( - bufnr, - namespace, - higroup, - { diagnostic.lnum, diagnostic.col }, - { diagnostic.end_lnum, diagnostic.end_col } - ) - end -end + local ns = M.get_namespace(namespace) + if not ns.user_data.underline_ns then + ns.user_data.underline_ns = vim.api.nvim_create_namespace("") + end ---- Set virtual text for given diagnostics. ---- ----@param namespace number The diagnostic namespace ----@param bufnr number Buffer number ----@param diagnostics table A list of diagnostic items |diagnostic-structure|. When omitted the ---- current diagnostics in the given buffer are used. ----@param opts table|nil Configuration table with the following keys: ---- - prefix: (string) Prefix to display before virtual text on line. ---- - spacing: (number) Number of spaces to insert before virtual text. ---- - source: (string) Include the diagnostic source in virtual text. One of "always" or ---- "if_many". ----@private -function M._set_virtual_text(namespace, bufnr, diagnostics, opts) - vim.validate { - namespace = {namespace, 'n'}, - bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, - opts = {opts, 't', true}, - } + local underline_ns = ns.user_data.underline_ns + for _, diagnostic in ipairs(diagnostics) do + local higroup = underline_highlight_map[diagnostic.severity] - bufnr = get_bufnr(bufnr) - opts = get_resolved_options({ virtual_text = opts }, namespace, bufnr).virtual_text + if higroup == nil then + -- Default to error if we don't have a highlight associated + higroup = underline_highlight_map.Error + end - if opts and opts.format then - diagnostics = reformat_diagnostics(opts.format, diagnostics) + vim.highlight.range( + bufnr, + underline_ns, + higroup, + { diagnostic.lnum, diagnostic.col }, + { diagnostic.end_lnum, diagnostic.end_col } + ) + end + save_extmarks(underline_ns, bufnr) + end, + hide = function(namespace, bufnr) + local ns = M.get_namespace(namespace) + if ns.user_data.underline_ns then + diagnostic_cache_extmarks[bufnr][ns.user_data.underline_ns] = {} + vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.underline_ns, 0, -1) + end end +} - if opts and opts.source then - diagnostics = prefix_source(opts.source, diagnostics) - end +M.handlers.virtual_text = { + show = function(namespace, bufnr, diagnostics, opts) + vim.validate { + namespace = {namespace, 'n'}, + bufnr = {bufnr, 'n'}, + diagnostics = {diagnostics, 't'}, + opts = {opts, 't', true}, + } - local buffer_line_diagnostics = diagnostic_lines(diagnostics) - for line, line_diagnostics in pairs(buffer_line_diagnostics) do - if opts and opts.severity then - line_diagnostics = filter_by_severity(opts.severity, line_diagnostics) + bufnr = get_bufnr(bufnr) + + local severity + if opts.virtual_text then + if opts.virtual_text.format then + diagnostics = reformat_diagnostics(opts.virtual_text.format, diagnostics) + end + if opts.virtual_text.source then + diagnostics = prefix_source(opts.virtual_text.source, diagnostics) + end + if opts.virtual_text.severity then + severity = opts.virtual_text.severity + end end - local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts) - if virt_texts then - vim.api.nvim_buf_set_extmark(bufnr, namespace, line, 0, { - hl_mode = "combine", - virt_text = virt_texts, - }) + local ns = M.get_namespace(namespace) + if not ns.user_data.virt_text_ns then + ns.user_data.virt_text_ns = vim.api.nvim_create_namespace("") end - end -end + + local virt_text_ns = ns.user_data.virt_text_ns + local buffer_line_diagnostics = diagnostic_lines(diagnostics) + for line, line_diagnostics in pairs(buffer_line_diagnostics) do + if severity then + line_diagnostics = filter_by_severity(severity, line_diagnostics) + end + local virt_texts = M._get_virt_text_chunks(line_diagnostics, opts.virtual_text) + + if virt_texts then + vim.api.nvim_buf_set_extmark(bufnr, virt_text_ns, line, 0, { + hl_mode = "combine", + virt_text = virt_texts, + }) + end + end + save_extmarks(virt_text_ns, bufnr) + end, + hide = function(namespace, bufnr) + local ns = M.get_namespace(namespace) + if ns.user_data.virt_text_ns then + diagnostic_cache_extmarks[bufnr][ns.user_data.virt_text_ns] = {} + vim.api.nvim_buf_clear_namespace(bufnr, ns.user_data.virt_text_ns, 0, -1) + end + end, +} --- Get virtual text chunks to display using |nvim_buf_set_extmark()|. --- @@ -1055,46 +1037,54 @@ end --- To hide diagnostics and prevent them from re-displaying, use --- |vim.diagnostic.disable()|. --- ----@param namespace number The diagnostic namespace +---@param namespace number|nil Diagnostic namespace. When omitted, hide +--- diagnostics from all namespaces. ---@param bufnr number|nil Buffer number. Defaults to the current buffer. function M.hide(namespace, bufnr) vim.validate { - namespace = { namespace, 'n' }, + namespace = { namespace, 'n', true }, bufnr = { bufnr, 'n', true }, } bufnr = get_bufnr(bufnr) - diagnostic_cache_extmarks[bufnr][namespace] = {} - - local ns = get_namespace(namespace) - - -- clear sign group - vim.fn.sign_unplace(ns.sign_group, {buffer=bufnr}) - - -- clear virtual text namespace - vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1) + local namespaces = namespace and {namespace} or vim.tbl_keys(diagnostic_cache[bufnr]) + for _, iter_namespace in ipairs(namespaces) do + for _, handler in pairs(M.handlers) do + if handler.hide then + handler.hide(iter_namespace, bufnr) + end + end + end end - --- Display diagnostics for the given namespace and buffer. --- ----@param namespace number Diagnostic namespace +---@param namespace number|nil Diagnostic namespace. When omitted, show +--- diagnostics from all namespaces. ---@param bufnr number|nil Buffer number. Defaults to the current buffer. ---@param diagnostics table|nil 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. +--- diagnostics. May not be used when {namespace} is nil. ---@param opts table|nil Display options. See |vim.diagnostic.config()|. function M.show(namespace, bufnr, diagnostics, opts) vim.validate { - namespace = { namespace, 'n' }, + namespace = { namespace, 'n', true }, bufnr = { bufnr, 'n', true }, diagnostics = { diagnostics, 't', true }, opts = { opts, 't', true }, } bufnr = get_bufnr(bufnr) + if not namespace then + assert(not diagnostics, "Cannot show diagnostics without a namespace") + for iter_namespace in pairs(diagnostic_cache[bufnr]) do + M.show(iter_namespace, bufnr, nil, opts) + end + return + end + if is_disabled(namespace, bufnr) then return end @@ -1129,83 +1119,154 @@ function M.show(namespace, bufnr, diagnostics, opts) clamp_line_numbers(bufnr, diagnostics) - if opts.underline then - M._set_underline(namespace, bufnr, diagnostics, opts.underline) - end - - if opts.virtual_text then - M._set_virtual_text(namespace, bufnr, diagnostics, opts.virtual_text) - end - - if opts.signs then - M._set_signs(namespace, bufnr, diagnostics, opts.signs) + for handler_name, handler in pairs(M.handlers) do + if handler.show and opts[handler_name] then + handler.show(namespace, bufnr, diagnostics, opts) + end end - - save_extmarks(namespace, bufnr) end ---- Open a floating window with the diagnostics at the given position. +--- Show diagnostics in a floating window. --- +---@param bufnr number|nil Buffer number. Defaults to the current buffer. ---@param opts table|nil Configuration table with the same keys as --- |vim.lsp.util.open_floating_preview()| in addition to the following: --- - namespace: (number) Limit diagnostics to the given namespace ---- - severity: See |diagnostic-severity|. ---- - show_header: (boolean, default true) Show "Diagnostics:" header ---- - source: (string) Include the diagnostic source in ---- the message. One of "always" or "if_many". +--- - scope: (string, default "buffer") Show diagnostics from the whole buffer ("buffer"), +--- the current cursor line ("line"), or the current cursor position ("cursor"). +--- - pos: (number or table) If {scope} is "line" or "cursor", use this position rather +--- than the cursor position. If a number, interpreted as a line number; +--- otherwise, a (row, col) tuple. +--- - severity_sort: (default false) Sort diagnostics by severity. Overrides the setting +--- from |vim.diagnostic.config()|. +--- - severity: See |diagnostic-severity|. Overrides the setting from +--- |vim.diagnostic.config()|. +--- - show_header: (boolean, default true) Show "Diagnostics:" header. Overrides the +--- setting from |vim.diagnostic.config()|. +--- - source: (string) Include the diagnostic source in the message. One of "always" or +--- "if_many". Overrides the setting from |vim.diagnostic.config()|. --- - format: (function) A function that takes a diagnostic as input and returns a --- string. The return value is the text used to display the diagnostic. ----@param bufnr number|nil Buffer number. Defaults to the current buffer. ----@param position table|nil The (0,0)-indexed position. Defaults to the current cursor position. ----@return tuple ({popup_bufnr}, {win_id}) -function M.show_position_diagnostics(opts, bufnr, position) +--- Overrides the setting from |vim.diagnostic.config()|. +--- - prefix: (function or string) Prefix each diagnostic in the floating window. +--- Overrides the setting from |vim.diagnostic.config()|. +---@return tuple ({float_bufnr}, {win_id}) +function M.open_float(bufnr, opts) vim.validate { - opts = { opts, 't', true }, bufnr = { bufnr, 'n', true }, - position = { position, 't', true }, + opts = { opts, 't', true }, } opts = opts or {} - - opts.focus_id = "position_diagnostics" bufnr = get_bufnr(bufnr) - if not position then - local curr_position = vim.api.nvim_win_get_cursor(0) - curr_position[1] = curr_position[1] - 1 - position = curr_position + local scope = opts.scope or "buffer" + local lnum, col + if scope == "line" or scope == "cursor" then + if not opts.pos then + local pos = vim.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) + else + error("Invalid value for option 'pos'") + end + elseif scope ~= "buffer" then + error("Invalid value for option 'scope'") end - local match_position_predicate = function(diag) - return position[1] == diag.lnum and - position[2] >= diag.col and - (position[2] <= diag.end_col or position[1] < diag.end_lnum) + + do + -- 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 which also includes "keyword" parameters. So we create a dedicated + -- options table that inherits missing keys from the global configuration before resolving. + local t = global_diagnostic_options.float + local float_opts = vim.tbl_extend("keep", opts, type(t) == "table" and t or {}) + opts = get_resolved_options({ float = float_opts }, nil, bufnr).float end + local diagnostics = M.get(bufnr, opts) clamp_line_numbers(bufnr, diagnostics) - local position_diagnostics = vim.tbl_filter(match_position_predicate, diagnostics) - return show_diagnostics(opts, position_diagnostics) -end ---- Open a floating window with the diagnostics from the given line. ---- ----@param opts table Configuration table. See |vim.diagnostic.show_position_diagnostics()|. ----@param bufnr number|nil Buffer number. Defaults to the current buffer. ----@param lnum number|nil Line number. Defaults to line number of cursor. ----@return tuple ({popup_bufnr}, {win_id}) -function M.show_line_diagnostics(opts, bufnr, lnum) - vim.validate { - opts = { opts, 't', true }, - bufnr = { bufnr, 'n', true }, - lnum = { lnum, 'n', true }, - } + if scope == "line" then + 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 = #vim.api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1] + diagnostics = vim.tbl_filter(function(d) + return d.lnum == lnum + and math.min(d.col, line_length - 1) <= col + and (d.end_col >= col or d.end_lnum > lnum) + end, diagnostics) + end - opts = opts or {} - opts.focus_id = "line_diagnostics" - bufnr = get_bufnr(bufnr) - local diagnostics = M.get(bufnr, opts) - clamp_line_numbers(bufnr, diagnostics) - lnum = lnum or (vim.api.nvim_win_get_cursor(0)[1] - 1) - local line_diagnostics = diagnostic_lines(diagnostics)[lnum] - return show_diagnostics(opts, line_diagnostics) + if vim.tbl_isempty(diagnostics) then + return + end + + local severity_sort = vim.F.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) return a.severity > b.severity end) + else + table.sort(diagnostics, function(a, b) return a.severity < b.severity end) + end + end + + local lines = {} + local highlights = {} + local show_header = vim.F.if_nil(opts.show_header, true) + if show_header then + table.insert(lines, "Diagnostics:") + table.insert(highlights, {0, "Bold"}) + end + + if opts.format then + diagnostics = reformat_diagnostics(opts.format, diagnostics) + end + + if opts.source then + diagnostics = prefix_source(opts.source, diagnostics) + end + + local prefix_opt = if_nil(opts.prefix, (scope == "cursor" and #diagnostics <= 1) and "" or function(_, i) + return string.format("%d. ", i) + end) + if prefix_opt then + vim.validate { prefix = { prefix_opt, function(v) + return type(v) == "string" or type(v) == "function" + end, "'string' or 'function'" } } + end + + for i, diagnostic in ipairs(diagnostics) do + local prefix = type(prefix_opt) == "string" and prefix_opt or prefix_opt(diagnostic, i, #diagnostics) + local hiname = floating_highlight_map[diagnostic.severity] + local message_lines = vim.split(diagnostic.message, '\n') + table.insert(lines, prefix..message_lines[1]) + table.insert(highlights, {#prefix, hiname}) + for j = 2, #message_lines do + table.insert(lines, string.rep(' ', #prefix) .. message_lines[j]) + table.insert(highlights, {0, hiname}) + 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 + local float_bufnr, winnr = require('vim.lsp.util').open_floating_preview(lines, 'plaintext', opts) + for i, hi in ipairs(highlights) do + local prefixlen, hiname = unpack(hi) + -- Start highlight after the prefix + vim.api.nvim_buf_add_highlight(float_bufnr, -1, hiname, i-1, prefixlen, -1) + end + + return float_bufnr, winnr end --- Remove all diagnostics from the given namespace. @@ -1215,19 +1276,23 @@ end --- simply remove diagnostic decorations in a way that they can be --- re-displayed, use |vim.diagnostic.hide()|. --- ----@param namespace number +---@param namespace number|nil Diagnostic namespace. When omitted, remove +--- diagnostics from all namespaces. ---@param bufnr number|nil Remove diagnostics for the given buffer. When omitted, --- diagnostics are removed for all buffers. function M.reset(namespace, bufnr) - if bufnr == nil then - for iter_bufnr, namespaces in pairs(diagnostic_cache) do - if namespaces[namespace] then - M.reset(namespace, iter_bufnr) - end + vim.validate { + namespace = {namespace, 'n', true}, + bufnr = {bufnr, 'n', true}, + } + + local buffers = bufnr and {bufnr} or vim.tbl_keys(diagnostic_cache) + for _, iter_bufnr in ipairs(buffers) do + local namespaces = namespace and {namespace} or vim.tbl_keys(diagnostic_cache[iter_bufnr]) + for _, iter_namespace in ipairs(namespaces) do + clear_diagnostic_cache(iter_namespace, iter_bufnr) + M.hide(iter_namespace, iter_bufnr) end - else - clear_diagnostic_cache(namespace, bufnr) - M.hide(namespace, bufnr) end vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged") |