aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/diagnostic.lua
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/diagnostic.lua')
-rw-r--r--runtime/lua/vim/diagnostic.lua543
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>