diff options
Diffstat (limited to 'runtime/lua/vim/diagnostic.lua')
-rw-r--r-- | runtime/lua/vim/diagnostic.lua | 543 |
1 files changed, 355 insertions, 188 deletions
diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index c977fe0109..4cf22282a2 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -19,6 +19,7 @@ local global_diagnostic_options = { signs = true, underline = true, virtual_text = true, + float = true, update_in_insert = false, severity_sort = false, } @@ -27,7 +28,10 @@ local global_diagnostic_options = { ---@private local function to_severity(severity) - return type(severity) == 'string' and M.severity[string.upper(severity)] or severity + if type(severity) == 'string' then + return assert(M.severity[string.upper(severity)], string.format("Invalid severity: %s", severity)) + end + return severity end ---@private @@ -48,25 +52,46 @@ local function filter_by_severity(severity, diagnostics) end ---@private -local function resolve_optional_value(option, namespace, bufnr) - local enabled_val = {} +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'" } } + + if source == "if_many" then + local sources = {} + for _, d in pairs(diagnostics) do + if d.source then + sources[d.source] = true + end + end + if #vim.tbl_keys(sources) <= 1 then + return diagnostics + end + end - if not option then - return false - elseif option == true then - return enabled_val - elseif type(option) == 'function' then - local val = option(namespace, bufnr) - if val == true then - return enabled_val - else - return val + return vim.tbl_map(function(d) + if not d.source then + return d end - elseif type(option) == 'table' then - return option - else - error("Unexpected option type: " .. vim.inspect(option)) + + local t = vim.deepcopy(d) + t.message = string.format("%s: %s", d.source, d.message) + return t + end, diagnostics) +end + +---@private +local function reformat_diagnostics(format, diagnostics) + vim.validate { + format = {format, 'f'}, + diagnostics = {diagnostics, 't'}, + } + + local formatted = vim.deepcopy(diagnostics) + for _, diagnostic in ipairs(formatted) do + diagnostic.message = format(diagnostic) end + return formatted end local all_namespaces = {} @@ -82,9 +107,7 @@ local function get_namespace(ns) end end - if not name then - return vim.notify("namespace does not exist or is anonymous", vim.log.levels.ERROR) - end + assert(name, "namespace does not exist or is anonymous") all_namespaces[ns] = { name = name, @@ -96,12 +119,47 @@ local function get_namespace(ns) end ---@private +local function enabled_value(option, namespace) + local ns = namespace and 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] + end + + return {} +end + +---@private +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) + if val == true then + return enabled_value(option, namespace) + else + return val + end + elseif type(value) == 'table' then + return value + else + error("Unexpected option type: " .. vim.inspect(value)) + end +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 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(resolved[k], namespace, bufnr) + resolved[k] = resolve_optional_value(k, resolved[k], namespace, bufnr) end end return resolved @@ -206,7 +264,7 @@ end ---@private local function diagnostic_lines(diagnostics) if not diagnostics then - return + return {} end local diagnostics_by_line = {} @@ -222,26 +280,14 @@ local function diagnostic_lines(diagnostics) end ---@private -local function set_diagnostic_cache(namespace, diagnostics, bufnr) - local buf_line_count = vim.api.nvim_buf_line_count(bufnr) +local function set_diagnostic_cache(namespace, bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do - if diagnostic.severity == nil then - diagnostic.severity = M.severity.ERROR - end - + diagnostic.severity = diagnostic.severity and to_severity(diagnostic.severity) or M.severity.ERROR + diagnostic.end_lnum = diagnostic.end_lnum or diagnostic.lnum + diagnostic.end_col = diagnostic.end_col or diagnostic.col diagnostic.namespace = namespace diagnostic.bufnr = bufnr - - if buf_line_count > 0 then - diagnostic.lnum = math.max(math.min( - diagnostic.lnum, buf_line_count - 1 - ), 0) - diagnostic.end_lnum = math.max(math.min( - diagnostic.end_lnum, buf_line_count - 1 - ), 0) - end end - diagnostic_cache[bufnr][namespace] = diagnostics end @@ -310,19 +356,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 @@ -332,56 +374,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 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 - - 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) @@ -404,12 +404,28 @@ 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 + return + end + + for _, diagnostic in ipairs(diagnostics) do + diagnostic.lnum = math.max(math.min(diagnostic.lnum, buf_line_count - 1), 0) + diagnostic.end_lnum = math.max(math.min(diagnostic.end_lnum, buf_line_count - 1), 0) + end +end + +---@private local function next_diagnostic(position, search_forward, bufnr, opts, namespace) position[1] = position[1] - 1 - bufnr = bufnr or vim.api.nvim_get_current_buf() + bufnr = get_bufnr(bufnr) local wrap = vim.F.if_nil(opts.wrap, true) local line_count = vim.api.nvim_buf_line_count(bufnr) - opts.namespace = namespace + local diagnostics = M.get(bufnr, vim.tbl_extend("keep", opts, {namespace = namespace})) + clamp_line_numbers(bufnr, diagnostics) + local line_diagnostics = diagnostic_lines(diagnostics) for i = 0, line_count do local offset = i * (search_forward and 1 or -1) local lnum = position[1] + offset @@ -419,9 +435,7 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace) end lnum = (lnum + line_count) % line_count end - opts.lnum = lnum - local line_diagnostics = M.get(bufnr, opts) - if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then + if line_diagnostics[lnum] and not vim.tbl_isempty(line_diagnostics[lnum]) then local sort_diagnostics, is_next if search_forward then sort_diagnostics = function(a, b) return a.col < b.col end @@ -430,15 +444,15 @@ local function next_diagnostic(position, search_forward, bufnr, opts, namespace) sort_diagnostics = function(a, b) return a.col > b.col end is_next = function(diagnostic) return diagnostic.col < position[2] end end - table.sort(line_diagnostics, sort_diagnostics) + table.sort(line_diagnostics[lnum], sort_diagnostics) if i == 0 then - for _, v in pairs(line_diagnostics) do + for _, v in pairs(line_diagnostics[lnum]) do if is_next(v) then return v end end else - return line_diagnostics[1] + return line_diagnostics[lnum][1] end end end @@ -448,7 +462,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 @@ -456,17 +470,22 @@ local function diagnostic_move_pos(opts, pos) return end + -- Save position in the window's jumplist + vim.api.nvim_win_call(win_id, function() vim.cmd("normal! m'") end) + 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 - -- }}} -- Public API {{{ @@ -474,10 +493,27 @@ end --- Configure diagnostic options globally or for a specific diagnostic --- namespace. --- +--- Configuration can be specified globally, per-namespace, or ephemerally +--- (i.e. only for a single call to |vim.diagnostic.set()| or +--- |vim.diagnostic.show()|). Ephemeral configuration has highest priority, +--- followed by namespace configuration, and finally global configuration. +--- +--- For example, if a user enables virtual text globally with +--- <pre> +--- vim.diagnostic.config({virtual_text = true}) +--- </pre> +--- +--- and a diagnostic producer sets diagnostics with +--- <pre> +--- vim.diagnostic.set(ns, 0, diagnostics, {virtual_text = false}) +--- </pre> +--- +--- then virtual text will not be enabled for those diagnostics. +--- ---@note Each of the configuration options below accepts one of the following: --- - `false`: Disable this feature --- - `true`: Enable this feature, use default settings. ---- - `table`: Enable this feature with overrides. +--- - `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 Configuration table with the following keys: @@ -487,9 +523,32 @@ end --- - virtual_text: (default true) Use virtual text for diagnostics. Options: --- * severity: Only show virtual text for diagnostics matching the given --- severity |diagnostic-severity| +--- * source: (string) Include the diagnostic source in virtual +--- text. 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. Example: +--- <pre> +--- function(diagnostic) +--- if diagnostic.severity == vim.diagnostic.severity.ERROR then +--- return string.format("E: %s", diagnostic.message) +--- end +--- return diagnostic.message +--- end +--- </pre> --- - signs: (default true) Use signs for diagnostics. Options: --- * severity: Only show signs for diagnostics matching the given severity --- |diagnostic-severity| +--- * 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. --- - 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 @@ -551,32 +610,36 @@ function M.set(namespace, bufnr, diagnostics, opts) } if vim.tbl_isempty(diagnostics) then - return M.reset(namespace, bufnr) - end - - if not diagnostic_cleanup[bufnr][namespace] then - diagnostic_cleanup[bufnr][namespace] = true - - -- Clean up our data when the buffer unloads. - vim.api.nvim_buf_attach(bufnr, false, { - on_detach = function(_, b) - clear_diagnostic_cache(b, namespace) - diagnostic_cleanup[b][namespace] = nil - end - }) + clear_diagnostic_cache(namespace, bufnr) + else + if not diagnostic_cleanup[bufnr][namespace] then + diagnostic_cleanup[bufnr][namespace] = true + + -- Clean up our data when the buffer unloads. + vim.api.nvim_buf_attach(bufnr, false, { + on_detach = function(_, b) + clear_diagnostic_cache(b, namespace) + diagnostic_cleanup[b][namespace] = nil + end + }) + end + set_diagnostic_cache(namespace, bufnr, diagnostics) end - set_diagnostic_cache(namespace, diagnostics, bufnr) - if vim.api.nvim_buf_is_loaded(bufnr) then M.show(namespace, bufnr, diagnostics, opts) - elseif opts then - M.config(opts, namespace) end vim.api.nvim_command("doautocmd <nomodeline> User DiagnosticsChanged") end +--- Get current diagnostic namespaces. +--- +---@return table A list of active diagnostic namespaces |vim.diagnostic|. +function M.get_namespaces() + return vim.deepcopy(all_namespaces) +end + --- Get current diagnostics. --- ---@param bufnr number|nil Buffer number to get diagnostics from. Use 0 for @@ -708,10 +771,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( @@ -740,16 +802,35 @@ function M._set_signs(namespace, bufnr, diagnostics, opts) } bufnr = get_bufnr(bufnr) - opts = get_resolved_options({ signs = opts }, namespace, bufnr).signs + opts = get_resolved_options({ signs = opts }, namespace, bufnr) - if opts and opts.severity then - diagnostics = filter_by_severity(opts.severity, diagnostics) + if opts.signs and opts.signs.severity then + diagnostics = filter_by_severity(opts.signs.severity, diagnostics) end local ns = get_namespace(namespace) 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) + end + else + get_priority = function(severity) + return priority + (vim.diagnostic.severity.HINT - severity) + end + end + else + get_priority = function() + return priority + end + end + for _, diagnostic in ipairs(diagnostics) do vim.fn.sign_place( 0, @@ -757,7 +838,7 @@ function M._set_signs(namespace, bufnr, diagnostics, opts) sign_highlight_map[diagnostic.severity], bufnr, { - priority = opts and opts.priority, + priority = get_priority(diagnostic.severity), lnum = diagnostic.lnum + 1 } ) @@ -814,6 +895,8 @@ end ---@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 { @@ -826,12 +909,20 @@ function M._set_virtual_text(namespace, bufnr, diagnostics, opts) bufnr = get_bufnr(bufnr) opts = get_resolved_options({ virtual_text = opts }, namespace, bufnr).virtual_text + if opts and opts.format then + diagnostics = reformat_diagnostics(opts.format, diagnostics) + end + + if opts and opts.source then + diagnostics = prefix_source(opts.source, diagnostics) + end + 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) end - local virt_texts = M.get_virt_text_chunks(line_diagnostics, opts) + 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, { @@ -844,13 +935,11 @@ end --- Get virtual text chunks to display using |nvim_buf_set_extmark()|. --- ----@param line_diags table The diagnostics associated with the line. ----@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. ----@return array of ({text}, {hl_group}) tuples. This can be passed directly to ---- the {virt_text} option of |nvim_buf_set_extmark()|. -function M.get_virt_text_chunks(line_diags, opts) +--- 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 +function M._get_virt_text_chunks(line_diags, opts) if #line_diags == 0 then return nil end @@ -983,6 +1072,8 @@ function M.show(namespace, bufnr, diagnostics, opts) end end + clamp_line_numbers(bufnr, diagnostics) + if opts.underline then M._set_underline(namespace, bufnr, diagnostics, opts.underline) end @@ -998,61 +1089,136 @@ function M.show(namespace, bufnr, diagnostics, opts) 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 ----@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) +--- - 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. +--- 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 - 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) - end - local position_diagnostics = vim.tbl_filter(match_position_predicate, M.get(bufnr, opts)) - table.sort(position_diagnostics, function(a, b) return a.severity < b.severity end) - return show_diagnostics(opts, position_diagnostics) -end + 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 ---- 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 }, - } + local diagnostics = M.get(bufnr, opts) + clamp_line_numbers(bufnr, diagnostics) + + 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" - opts.lnum = lnum or (vim.api.nvim_win_get_cursor(0)[1] - 1) - bufnr = get_bufnr(bufnr) - local line_diagnostics = M.get(bufnr, opts) - 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 + + 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 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 + + for i, diagnostic in ipairs(diagnostics) do + local prefix = string.format("%d. ", i) + 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. @@ -1151,6 +1317,7 @@ end --- <pre> --- WARNING filename:27:3: Variable 'foo' does not exist --- </pre> +--- --- This can be parsed into a diagnostic |diagnostic-structure| --- with: --- <pre> |