mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 03:18:16 +00:00

Add a new function to redraw diagnostics from the current diagnostic cache, without receiving a "publishDiagnostics" message from the server. This is already being done in two places in the Lua stdlib, so this function unifies that functionality in addition to providing it to third party plugins. An example use case for this could be a command or key-binding for toggling diagnostics virtual text. The virtual text configuration option can be toggled using `vim.lsp.with` followed by `vim.lsp.diagnostic.redraw()` to immediately redraw the diagnostics with the updated setting.
1403 lines
45 KiB
Lua
1403 lines
45 KiB
Lua
local api = vim.api
|
|
local validate = vim.validate
|
|
|
|
local highlight = vim.highlight
|
|
local log = require('vim.lsp.log')
|
|
local protocol = require('vim.lsp.protocol')
|
|
local util = require('vim.lsp.util')
|
|
|
|
local if_nil = vim.F.if_nil
|
|
|
|
--@class DiagnosticSeverity
|
|
local DiagnosticSeverity = protocol.DiagnosticSeverity
|
|
|
|
local to_severity = function(severity)
|
|
if not severity then return nil end
|
|
return type(severity) == 'string' and DiagnosticSeverity[severity] or severity
|
|
end
|
|
|
|
local filter_to_severity_limit = function(severity, diagnostics)
|
|
local filter_level = to_severity(severity)
|
|
if not filter_level then
|
|
return diagnostics
|
|
end
|
|
|
|
return vim.tbl_filter(function(t) return t.severity == filter_level end, diagnostics)
|
|
end
|
|
|
|
local filter_by_severity_limit = function(severity_limit, diagnostics)
|
|
local filter_level = to_severity(severity_limit)
|
|
if not filter_level then
|
|
return diagnostics
|
|
end
|
|
|
|
return vim.tbl_filter(function(t) return t.severity <= filter_level end, diagnostics)
|
|
end
|
|
|
|
local to_position = function(position, bufnr)
|
|
vim.validate { position = {position, 't'} }
|
|
|
|
return {
|
|
position.line,
|
|
util._get_line_byte_from_position(bufnr, position)
|
|
}
|
|
end
|
|
|
|
|
|
---@brief lsp-diagnostic
|
|
---
|
|
--@class Diagnostic
|
|
--@field range Range
|
|
--@field message string
|
|
--@field severity DiagnosticSeverity|nil
|
|
--@field code number | string
|
|
--@field source string
|
|
--@field tags DiagnosticTag[]
|
|
--@field relatedInformation DiagnosticRelatedInformation[]
|
|
|
|
local M = {}
|
|
|
|
-- Diagnostic Highlights {{{
|
|
|
|
-- TODO(tjdevries): Determine how to generate documentation for these
|
|
-- and how to configure them to be easy for users.
|
|
--
|
|
-- For now, just use the following script. It should work pretty good.
|
|
--[[
|
|
local levels = {"Error", "Warning", "Information", "Hint" }
|
|
|
|
local all_info = {
|
|
{ "Default", "Used as the base highlight group, other highlight groups link to", },
|
|
{ "VirtualText", 'Used for "%s" diagnostic virtual text.\n See |vim.lsp.diagnostic.set_virtual_text()|', },
|
|
{ "Underline", 'Used to underline "%s" diagnostics.\n See |vim.lsp.diagnostic.set_underline()|', },
|
|
{ "Floating", 'Used to color "%s" diagnostic messages in diagnostics float.\n See |vim.lsp.diagnostic.show_line_diagnostics()|', },
|
|
{ "Sign", 'Used for "%s" signs in sing column.\n See |vim.lsp.diagnostic.set_signs()|', },
|
|
}
|
|
|
|
local results = {}
|
|
for _, info in ipairs(all_info) do
|
|
for _, level in ipairs(levels) do
|
|
local name = info[1]
|
|
local description = info[2]
|
|
local fullname = string.format("Lsp%s%s", name, level)
|
|
table.insert(results, string.format(
|
|
"%78s", string.format("*hl-%s*", fullname))
|
|
)
|
|
|
|
table.insert(results, fullname)
|
|
table.insert(results, string.format(" %s", description))
|
|
table.insert(results, "")
|
|
end
|
|
end
|
|
|
|
-- print(table.concat(results, '\n'))
|
|
vim.fn.setreg("*", table.concat(results, '\n'))
|
|
--]]
|
|
|
|
local diagnostic_severities = {
|
|
[DiagnosticSeverity.Error] = { guifg = "Red" };
|
|
[DiagnosticSeverity.Warning] = { guifg = "Orange" };
|
|
[DiagnosticSeverity.Information] = { guifg = "LightBlue" };
|
|
[DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
|
|
}
|
|
|
|
-- Make a map from DiagnosticSeverity -> Highlight Name
|
|
local make_highlight_map = function(base_name)
|
|
local result = {}
|
|
for k, _ in pairs(diagnostic_severities) do
|
|
result[k] = "LspDiagnostics" .. base_name .. DiagnosticSeverity[k]
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
local default_highlight_map = make_highlight_map("Default")
|
|
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")
|
|
local sign_highlight_map = make_highlight_map("Sign")
|
|
|
|
-- }}}
|
|
-- Diagnostic Namespaces {{{
|
|
local DEFAULT_CLIENT_ID = -1
|
|
local get_client_id = function(client_id)
|
|
if client_id == nil then
|
|
client_id = DEFAULT_CLIENT_ID
|
|
end
|
|
|
|
return client_id
|
|
end
|
|
|
|
local get_bufnr = function(bufnr)
|
|
if not bufnr then
|
|
return api.nvim_get_current_buf()
|
|
elseif bufnr == 0 then
|
|
return api.nvim_get_current_buf()
|
|
end
|
|
|
|
return bufnr
|
|
end
|
|
|
|
|
|
--- Create a namespace table, used to track a client's buffer local items
|
|
local _make_namespace_table = function(namespace, api_namespace)
|
|
vim.validate { namespace = { namespace, 's' } }
|
|
|
|
return setmetatable({
|
|
[DEFAULT_CLIENT_ID] = api.nvim_create_namespace(namespace)
|
|
}, {
|
|
__index = function(t, client_id)
|
|
client_id = get_client_id(client_id)
|
|
|
|
if rawget(t, client_id) == nil then
|
|
local value = string.format("%s:%s", namespace, client_id)
|
|
|
|
if api_namespace then
|
|
value = api.nvim_create_namespace(value)
|
|
end
|
|
|
|
rawset(t, client_id, value)
|
|
end
|
|
|
|
return rawget(t, client_id)
|
|
end
|
|
})
|
|
end
|
|
|
|
local _diagnostic_namespaces = _make_namespace_table("vim_lsp_diagnostics", true)
|
|
local _sign_namespaces = _make_namespace_table("vim_lsp_signs", false)
|
|
|
|
--@private
|
|
function M._get_diagnostic_namespace(client_id)
|
|
return _diagnostic_namespaces[client_id]
|
|
end
|
|
|
|
--@private
|
|
function M._get_sign_namespace(client_id)
|
|
return _sign_namespaces[client_id]
|
|
end
|
|
-- }}}
|
|
-- Diagnostic Buffer & Client metatables {{{
|
|
local bufnr_and_client_cacher_mt = {
|
|
__index = function(t, bufnr)
|
|
if bufnr == 0 or bufnr == nil then
|
|
bufnr = vim.api.nvim_get_current_buf()
|
|
end
|
|
|
|
if rawget(t, bufnr) == nil then
|
|
rawset(t, bufnr, {})
|
|
end
|
|
|
|
return rawget(t, bufnr)
|
|
end,
|
|
|
|
__newindex = function(t, bufnr, v)
|
|
if bufnr == 0 or bufnr == nil then
|
|
bufnr = vim.api.nvim_get_current_buf()
|
|
end
|
|
|
|
rawset(t, bufnr, v)
|
|
end,
|
|
}
|
|
-- }}}
|
|
-- Diagnostic Saving & Caching {{{
|
|
local _diagnostic_cleanup = setmetatable({}, bufnr_and_client_cacher_mt)
|
|
local diagnostic_cache = setmetatable({}, bufnr_and_client_cacher_mt)
|
|
local diagnostic_cache_extmarks = setmetatable({}, bufnr_and_client_cacher_mt)
|
|
local diagnostic_cache_lines = setmetatable({}, bufnr_and_client_cacher_mt)
|
|
local diagnostic_cache_counts = setmetatable({}, bufnr_and_client_cacher_mt)
|
|
local diagnostic_attached_buffers = {}
|
|
|
|
-- Disabled buffers and clients
|
|
local diagnostic_disabled = setmetatable({}, bufnr_and_client_cacher_mt)
|
|
|
|
local _bufs_waiting_to_update = setmetatable({}, bufnr_and_client_cacher_mt)
|
|
|
|
--- Store Diagnostic[] by line
|
|
---
|
|
---@param diagnostics Diagnostic[]
|
|
---@return table<number, Diagnostic[]>
|
|
local _diagnostic_lines = function(diagnostics)
|
|
if not diagnostics then return end
|
|
|
|
local diagnostics_by_line = {}
|
|
for _, diagnostic in ipairs(diagnostics) do
|
|
local start = diagnostic.range.start
|
|
local line_diagnostics = diagnostics_by_line[start.line]
|
|
if not line_diagnostics then
|
|
line_diagnostics = {}
|
|
diagnostics_by_line[start.line] = line_diagnostics
|
|
end
|
|
table.insert(line_diagnostics, diagnostic)
|
|
end
|
|
return diagnostics_by_line
|
|
end
|
|
|
|
--- Get the count of M by Severity
|
|
---
|
|
---@param diagnostics Diagnostic[]
|
|
---@return table<DiagnosticSeverity, number>
|
|
local _diagnostic_counts = function(diagnostics)
|
|
if not diagnostics then return end
|
|
|
|
local counts = {}
|
|
for _, diagnostic in pairs(diagnostics) do
|
|
if diagnostic.severity then
|
|
local val = counts[diagnostic.severity]
|
|
if val == nil then
|
|
val = 0
|
|
end
|
|
|
|
counts[diagnostic.severity] = val + 1
|
|
end
|
|
end
|
|
|
|
return counts
|
|
end
|
|
|
|
--@private
|
|
--- Set the different diagnostic cache after `textDocument/publishDiagnostics`
|
|
---@param diagnostics Diagnostic[]
|
|
---@param bufnr number
|
|
---@param client_id number
|
|
---@return nil
|
|
local function set_diagnostic_cache(diagnostics, bufnr, client_id)
|
|
client_id = get_client_id(client_id)
|
|
|
|
-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic
|
|
--
|
|
-- The diagnostic's severity. Can be omitted. If omitted it is up to the
|
|
-- client to interpret diagnostics as error, warning, info or hint.
|
|
-- TODO: Replace this with server-specific heuristics to infer severity.
|
|
local buf_line_count = vim.api.nvim_buf_line_count(bufnr)
|
|
for _, diagnostic in ipairs(diagnostics) do
|
|
if diagnostic.severity == nil then
|
|
diagnostic.severity = DiagnosticSeverity.Error
|
|
end
|
|
-- Account for servers that place diagnostics on terminating newline
|
|
if buf_line_count > 0 then
|
|
diagnostic.range.start.line = math.max(math.min(
|
|
diagnostic.range.start.line, buf_line_count - 1
|
|
), 0)
|
|
diagnostic.range["end"].line = math.max(math.min(
|
|
diagnostic.range["end"].line, buf_line_count - 1
|
|
), 0)
|
|
end
|
|
end
|
|
|
|
diagnostic_cache[bufnr][client_id] = diagnostics
|
|
diagnostic_cache_lines[bufnr][client_id] = _diagnostic_lines(diagnostics)
|
|
diagnostic_cache_counts[bufnr][client_id] = _diagnostic_counts(diagnostics)
|
|
end
|
|
|
|
|
|
--@private
|
|
--- Clear the cached diagnostics
|
|
---@param bufnr number
|
|
---@param client_id number
|
|
local function clear_diagnostic_cache(bufnr, client_id)
|
|
client_id = get_client_id(client_id)
|
|
|
|
diagnostic_cache[bufnr][client_id] = nil
|
|
diagnostic_cache_lines[bufnr][client_id] = nil
|
|
diagnostic_cache_counts[bufnr][client_id] = nil
|
|
end
|
|
|
|
--- Save diagnostics to the current buffer.
|
|
---
|
|
--- Handles saving diagnostics from multiple clients in the same buffer.
|
|
---@param diagnostics Diagnostic[]
|
|
---@param bufnr number
|
|
---@param client_id number
|
|
function M.save(diagnostics, bufnr, client_id)
|
|
validate {
|
|
diagnostics = {diagnostics, 't'},
|
|
bufnr = {bufnr, 'n'},
|
|
client_id = {client_id, 'n', true},
|
|
}
|
|
|
|
if not diagnostics then return end
|
|
|
|
bufnr = get_bufnr(bufnr)
|
|
client_id = get_client_id(client_id)
|
|
|
|
if not _diagnostic_cleanup[bufnr][client_id] then
|
|
_diagnostic_cleanup[bufnr][client_id] = true
|
|
|
|
-- Clean up our data when the buffer unloads.
|
|
api.nvim_buf_attach(bufnr, false, {
|
|
on_detach = function(_, b)
|
|
clear_diagnostic_cache(b, client_id)
|
|
_diagnostic_cleanup[b][client_id] = nil
|
|
end
|
|
})
|
|
end
|
|
|
|
set_diagnostic_cache(diagnostics, bufnr, client_id)
|
|
end
|
|
-- }}}
|
|
-- Diagnostic Retrieval {{{
|
|
|
|
|
|
--- Get all diagnostics for clients
|
|
---
|
|
---@param client_id number Restrict included diagnostics to the client
|
|
--- If nil, diagnostics of all clients are included.
|
|
---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[])
|
|
function M.get_all(client_id)
|
|
local diagnostics_by_bufnr = {}
|
|
for bufnr, buf_diagnostics in pairs(diagnostic_cache) do
|
|
diagnostics_by_bufnr[bufnr] = {}
|
|
for cid, client_diagnostics in pairs(buf_diagnostics) do
|
|
if client_id == nil or cid == client_id then
|
|
vim.list_extend(diagnostics_by_bufnr[bufnr], client_diagnostics)
|
|
end
|
|
end
|
|
end
|
|
return diagnostics_by_bufnr
|
|
end
|
|
|
|
--- Return associated diagnostics for bufnr
|
|
---
|
|
---@param bufnr number
|
|
---@param client_id number|nil If nil, then return all of the diagnostics.
|
|
--- Else, return just the diagnostics associated with the client_id.
|
|
function M.get(bufnr, client_id)
|
|
if client_id == nil then
|
|
local all_diagnostics = {}
|
|
for iter_client_id, _ in pairs(diagnostic_cache[bufnr]) do
|
|
local iter_diagnostics = M.get(bufnr, iter_client_id)
|
|
|
|
for _, diagnostic in ipairs(iter_diagnostics) do
|
|
table.insert(all_diagnostics, diagnostic)
|
|
end
|
|
end
|
|
|
|
return all_diagnostics
|
|
end
|
|
|
|
return diagnostic_cache[bufnr][client_id] or {}
|
|
end
|
|
|
|
--- Get the diagnostics by line
|
|
---
|
|
---@param bufnr number The buffer number
|
|
---@param line_nr number The line number
|
|
---@param opts table|nil Configuration keys
|
|
--- - severity: (DiagnosticSeverity, default nil)
|
|
--- - Only return diagnostics with this severity. Overrides severity_limit
|
|
--- - severity_limit: (DiagnosticSeverity, default nil)
|
|
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
|
|
---@param client_id number the client id
|
|
---@return table Table with map of line number to list of diagnostics.
|
|
-- Structured: { [1] = {...}, [5] = {.... } }
|
|
function M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
|
|
opts = opts or {}
|
|
|
|
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
|
line_nr = line_nr or vim.api.nvim_win_get_cursor(0)[1] - 1
|
|
|
|
local client_get_diags = function(iter_client_id)
|
|
return (diagnostic_cache_lines[bufnr][iter_client_id] or {})[line_nr] or {}
|
|
end
|
|
|
|
local line_diagnostics
|
|
if client_id == nil then
|
|
line_diagnostics = {}
|
|
for iter_client_id, _ in pairs(diagnostic_cache_lines[bufnr]) do
|
|
for _, diagnostic in ipairs(client_get_diags(iter_client_id)) do
|
|
table.insert(line_diagnostics, diagnostic)
|
|
end
|
|
end
|
|
else
|
|
line_diagnostics = vim.deepcopy(client_get_diags(client_id))
|
|
end
|
|
|
|
if opts.severity then
|
|
line_diagnostics = filter_to_severity_limit(opts.severity, line_diagnostics)
|
|
elseif opts.severity_limit then
|
|
line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics)
|
|
end
|
|
|
|
table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end)
|
|
|
|
return line_diagnostics
|
|
end
|
|
|
|
--- Get the counts for a particular severity
|
|
---
|
|
--- Useful for showing diagnostic counts in statusline. eg:
|
|
---
|
|
--- <pre>
|
|
--- function! LspStatus() abort
|
|
--- let sl = ''
|
|
--- if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))')
|
|
--- let sl.='%#MyStatuslineLSP#E:'
|
|
--- let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Error]])")}'
|
|
--- let sl.='%#MyStatuslineLSP# W:'
|
|
--- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.diagnostic.get_count(0, [[Warning]])")}'
|
|
--- else
|
|
--- let sl.='%#MyStatuslineLSPErrors#off'
|
|
--- endif
|
|
--- return sl
|
|
--- endfunction
|
|
--- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
|
|
--- </pre>
|
|
---
|
|
---@param bufnr number The buffer number
|
|
---@param severity DiagnosticSeverity
|
|
---@param client_id number the client id
|
|
function M.get_count(bufnr, severity, client_id)
|
|
if client_id == nil then
|
|
local total = 0
|
|
for iter_client_id, _ in pairs(diagnostic_cache_counts[bufnr]) do
|
|
total = total + M.get_count(bufnr, severity, iter_client_id)
|
|
end
|
|
|
|
return total
|
|
end
|
|
|
|
return (diagnostic_cache_counts[bufnr][client_id] or {})[DiagnosticSeverity[severity]] or 0
|
|
end
|
|
|
|
|
|
-- }}}
|
|
-- Diagnostic Movements {{{
|
|
|
|
--- Helper function to iterate through all of the diagnostic lines
|
|
---@return table list of diagnostics
|
|
local _iter_diagnostic_lines = function(start, finish, step, bufnr, opts, client_id)
|
|
if bufnr == nil then
|
|
bufnr = vim.api.nvim_get_current_buf()
|
|
end
|
|
|
|
local wrap = if_nil(opts.wrap, true)
|
|
|
|
local search = function(search_start, search_finish, search_step)
|
|
for line_nr = search_start, search_finish, search_step do
|
|
local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
|
|
if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then
|
|
return line_diagnostics
|
|
end
|
|
end
|
|
end
|
|
|
|
local result = search(start, finish, step)
|
|
|
|
if wrap then
|
|
local wrap_start, wrap_finish
|
|
if step == 1 then
|
|
wrap_start, wrap_finish = 1, start
|
|
else
|
|
wrap_start, wrap_finish = vim.api.nvim_buf_line_count(bufnr), start
|
|
end
|
|
|
|
if not result then
|
|
result = search(wrap_start, wrap_finish, step)
|
|
end
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
--@private
|
|
--- Helper function to ierate through diagnostic lines and return a position
|
|
---
|
|
---@return table {row, col}
|
|
local function _iter_diagnostic_lines_pos(opts, line_diagnostics)
|
|
opts = opts or {}
|
|
|
|
local win_id = opts.win_id or vim.api.nvim_get_current_win()
|
|
local bufnr = vim.api.nvim_win_get_buf(win_id)
|
|
|
|
if line_diagnostics == nil or vim.tbl_isempty(line_diagnostics) then
|
|
return false
|
|
end
|
|
|
|
local iter_diagnostic = line_diagnostics[1]
|
|
return to_position(iter_diagnostic.range.start, bufnr)
|
|
end
|
|
|
|
--@private
|
|
-- Move to the diagnostic position
|
|
local function _iter_diagnostic_move_pos(name, opts, pos)
|
|
opts = opts or {}
|
|
|
|
local enable_popup = if_nil(opts.enable_popup, true)
|
|
local win_id = opts.win_id or vim.api.nvim_get_current_win()
|
|
|
|
if not pos then
|
|
print(string.format("%s: No more valid diagnostics to move to.", name))
|
|
return
|
|
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.
|
|
vim.schedule(function()
|
|
M.show_line_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id))
|
|
end)
|
|
end
|
|
end
|
|
|
|
--- Get the previous diagnostic closest to the cursor_position
|
|
---
|
|
---@param opts table See |vim.lsp.diagnostic.goto_next()|
|
|
---@return table Previous diagnostic
|
|
function M.get_prev(opts)
|
|
opts = opts or {}
|
|
|
|
local win_id = opts.win_id or vim.api.nvim_get_current_win()
|
|
local bufnr = vim.api.nvim_win_get_buf(win_id)
|
|
local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
|
|
|
|
return _iter_diagnostic_lines(cursor_position[1] - 2, 0, -1, bufnr, opts, opts.client_id)
|
|
end
|
|
|
|
--- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
|
|
---@param opts table See |vim.lsp.diagnostic.goto_next()|
|
|
---@return table Previous diagnostic position
|
|
function M.get_prev_pos(opts)
|
|
return _iter_diagnostic_lines_pos(
|
|
opts,
|
|
M.get_prev(opts)
|
|
)
|
|
end
|
|
|
|
--- Move to the previous diagnostic
|
|
---@param opts table See |vim.lsp.diagnostic.goto_next()|
|
|
function M.goto_prev(opts)
|
|
return _iter_diagnostic_move_pos(
|
|
"DiagnosticPrevious",
|
|
opts,
|
|
M.get_prev_pos(opts)
|
|
)
|
|
end
|
|
|
|
--- Get the next diagnostic closest to the cursor_position
|
|
---@param opts table See |vim.lsp.diagnostic.goto_next()|
|
|
---@return table Next diagnostic
|
|
function M.get_next(opts)
|
|
opts = opts or {}
|
|
|
|
local win_id = opts.win_id or vim.api.nvim_get_current_win()
|
|
local bufnr = vim.api.nvim_win_get_buf(win_id)
|
|
local cursor_position = opts.cursor_position or vim.api.nvim_win_get_cursor(win_id)
|
|
|
|
return _iter_diagnostic_lines(cursor_position[1], vim.api.nvim_buf_line_count(bufnr), 1, bufnr, opts, opts.client_id)
|
|
end
|
|
|
|
--- Return the pos, {row, col}, for the next diagnostic in the current buffer.
|
|
---@param opts table See |vim.lsp.diagnostic.goto_next()|
|
|
---@return table Next diagnostic position
|
|
function M.get_next_pos(opts)
|
|
return _iter_diagnostic_lines_pos(
|
|
opts,
|
|
M.get_next(opts)
|
|
)
|
|
end
|
|
|
|
--- Move to the next diagnostic
|
|
---@param opts table|nil Configuration table. Keys:
|
|
--- - {client_id}: (number)
|
|
--- - If nil, will consider all clients attached to buffer.
|
|
--- - {cursor_position}: (Position, default current position)
|
|
--- - See |nvim_win_get_cursor()|
|
|
--- - {wrap}: (boolean, default true)
|
|
--- - Whether to loop around file or not. Similar to 'wrapscan'
|
|
--- - {severity}: (DiagnosticSeverity)
|
|
--- - Exclusive severity to consider. Overrides {severity_limit}
|
|
--- - {severity_limit}: (DiagnosticSeverity)
|
|
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
|
|
--- - {enable_popup}: (boolean, default true)
|
|
--- - Call |vim.lsp.diagnostic.show_line_diagnostics()| on jump
|
|
--- - {popup_opts}: (table)
|
|
--- - Table to pass as {opts} parameter to |vim.lsp.diagnostic.show_line_diagnostics()|
|
|
--- - {win_id}: (number, default 0)
|
|
--- - Window ID
|
|
function M.goto_next(opts)
|
|
return _iter_diagnostic_move_pos(
|
|
"DiagnosticNext",
|
|
opts,
|
|
M.get_next_pos(opts)
|
|
)
|
|
end
|
|
-- }}}
|
|
-- Diagnostic Setters {{{
|
|
|
|
--- Set signs for given diagnostics
|
|
---
|
|
--- Sign characters can be customized with the following commands:
|
|
---
|
|
--- <pre>
|
|
--- sign define LspDiagnosticsSignError text=E texthl=LspDiagnosticsSignError linehl= numhl=
|
|
--- sign define LspDiagnosticsSignWarning text=W texthl=LspDiagnosticsSignWarning linehl= numhl=
|
|
--- sign define LspDiagnosticsSignInformation text=I texthl=LspDiagnosticsSignInformation linehl= numhl=
|
|
--- sign define LspDiagnosticsSignHint text=H texthl=LspDiagnosticsSignHint linehl= numhl=
|
|
--- </pre>
|
|
---@param diagnostics Diagnostic[]
|
|
---@param bufnr number The buffer number
|
|
---@param client_id number the client id
|
|
---@param sign_ns number|nil
|
|
---@param opts table Configuration for signs. Keys:
|
|
--- - priority: Set the priority of the signs.
|
|
--- - severity_limit (DiagnosticSeverity):
|
|
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
|
|
function M.set_signs(diagnostics, bufnr, client_id, sign_ns, opts)
|
|
opts = opts or {}
|
|
sign_ns = sign_ns or M._get_sign_namespace(client_id)
|
|
|
|
if not diagnostics then
|
|
diagnostics = diagnostic_cache[bufnr][client_id]
|
|
end
|
|
|
|
if not diagnostics then
|
|
return
|
|
end
|
|
|
|
bufnr = get_bufnr(bufnr)
|
|
diagnostics = filter_by_severity_limit(opts.severity_limit, diagnostics)
|
|
|
|
local ok = true
|
|
for _, diagnostic in ipairs(diagnostics) do
|
|
|
|
ok = ok and pcall(vim.fn.sign_place,
|
|
0,
|
|
sign_ns,
|
|
sign_highlight_map[diagnostic.severity],
|
|
bufnr,
|
|
{
|
|
priority = opts.priority,
|
|
lnum = diagnostic.range.start.line + 1
|
|
}
|
|
)
|
|
end
|
|
|
|
if not ok then
|
|
log.debug("Failed to place signs:", diagnostics)
|
|
end
|
|
end
|
|
|
|
--- Set underline for given diagnostics
|
|
---
|
|
--- Underline highlights can be customized by changing the following |:highlight| groups.
|
|
---
|
|
--- <pre>
|
|
--- LspDiagnosticsUnderlineError
|
|
--- LspDiagnosticsUnderlineWarning
|
|
--- LspDiagnosticsUnderlineInformation
|
|
--- LspDiagnosticsUnderlineHint
|
|
--- </pre>
|
|
---
|
|
---@param diagnostics Diagnostic[]
|
|
---@param bufnr number: The buffer number
|
|
---@param client_id number: The client id
|
|
---@param diagnostic_ns number|nil: The namespace
|
|
---@param opts table: Configuration table:
|
|
--- - severity_limit (DiagnosticSeverity):
|
|
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
|
|
function M.set_underline(diagnostics, bufnr, client_id, diagnostic_ns, opts)
|
|
opts = opts or {}
|
|
|
|
diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
|
|
diagnostics = filter_by_severity_limit(opts.severity_limit, diagnostics)
|
|
|
|
for _, diagnostic in ipairs(diagnostics) do
|
|
local start = diagnostic.range["start"]
|
|
local finish = diagnostic.range["end"]
|
|
local higroup = underline_highlight_map[diagnostic.severity]
|
|
|
|
if higroup == nil then
|
|
-- Default to error if we don't have a highlight associated
|
|
higroup = underline_highlight_map[DiagnosticSeverity.Error]
|
|
end
|
|
|
|
highlight.range(
|
|
bufnr,
|
|
diagnostic_ns,
|
|
higroup,
|
|
to_position(start, bufnr),
|
|
to_position(finish, bufnr)
|
|
)
|
|
end
|
|
end
|
|
|
|
-- Virtual Text {{{
|
|
--- Set virtual text given diagnostics
|
|
---
|
|
--- Virtual text highlights can be customized by changing the following |:highlight| groups.
|
|
---
|
|
--- <pre>
|
|
--- LspDiagnosticsVirtualTextError
|
|
--- LspDiagnosticsVirtualTextWarning
|
|
--- LspDiagnosticsVirtualTextInformation
|
|
--- LspDiagnosticsVirtualTextHint
|
|
--- </pre>
|
|
---
|
|
---@param diagnostics Diagnostic[]
|
|
---@param bufnr number
|
|
---@param client_id number
|
|
---@param diagnostic_ns number
|
|
---@param opts table Options on how to display virtual text. Keys:
|
|
--- - prefix (string): Prefix to display before virtual text on line
|
|
--- - spacing (number): Number of spaces to insert before virtual text
|
|
--- - severity_limit (DiagnosticSeverity):
|
|
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
|
|
function M.set_virtual_text(diagnostics, bufnr, client_id, diagnostic_ns, opts)
|
|
opts = opts or {}
|
|
|
|
client_id = get_client_id(client_id)
|
|
diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
|
|
|
|
local buffer_line_diagnostics
|
|
if diagnostics then
|
|
buffer_line_diagnostics = _diagnostic_lines(diagnostics)
|
|
else
|
|
buffer_line_diagnostics = diagnostic_cache_lines[bufnr][client_id]
|
|
end
|
|
|
|
if not buffer_line_diagnostics then
|
|
return nil
|
|
end
|
|
|
|
for line, line_diagnostics in pairs(buffer_line_diagnostics) do
|
|
line_diagnostics = filter_by_severity_limit(opts.severity_limit, line_diagnostics)
|
|
local virt_texts = M.get_virtual_text_chunks_for_line(bufnr, line, line_diagnostics, opts)
|
|
|
|
if virt_texts then
|
|
api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {})
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Default function to get text chunks to display using `nvim_buf_set_virtual_text`.
|
|
---@param bufnr number The buffer to display the virtual text in
|
|
---@param line number The line number to display the virtual text on
|
|
---@param line_diags Diagnostic[] The diagnostics associated with the line
|
|
---@param opts table See {opts} from |vim.lsp.diagnostic.set_virtual_text()|
|
|
---@return table chunks, as defined by |nvim_buf_set_virtual_text()|
|
|
function M.get_virtual_text_chunks_for_line(bufnr, line, line_diags, opts)
|
|
assert(bufnr or line)
|
|
|
|
if #line_diags == 0 then
|
|
return nil
|
|
end
|
|
|
|
opts = opts or {}
|
|
local prefix = opts.prefix or "■"
|
|
local spacing = opts.spacing or 4
|
|
|
|
-- Create a little more space between virtual text and contents
|
|
local virt_texts = {{string.rep(" ", spacing)}}
|
|
|
|
for i = 1, #line_diags - 1 do
|
|
table.insert(virt_texts, {prefix, virtual_text_highlight_map[line_diags[i].severity]})
|
|
end
|
|
local last = line_diags[#line_diags]
|
|
|
|
-- TODO(tjdevries): Allow different servers to be shown first somehow?
|
|
-- TODO(tjdevries): Display server name associated with these?
|
|
if last.message then
|
|
table.insert(
|
|
virt_texts,
|
|
{
|
|
string.format("%s %s", prefix, last.message:gsub("\r", ""):gsub("\n", " ")),
|
|
virtual_text_highlight_map[last.severity]
|
|
}
|
|
)
|
|
|
|
return virt_texts
|
|
end
|
|
end
|
|
-- }}}
|
|
-- }}}
|
|
-- Diagnostic Clear {{{
|
|
--- Clears the currently displayed diagnostics
|
|
---@param bufnr number The buffer number
|
|
---@param client_id number the client id
|
|
---@param diagnostic_ns number|nil Associated diagnostic namespace
|
|
---@param sign_ns number|nil Associated sign namespace
|
|
function M.clear(bufnr, client_id, diagnostic_ns, sign_ns)
|
|
bufnr = get_bufnr(bufnr)
|
|
if client_id == nil then
|
|
return vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _)
|
|
return M.clear(bufnr, iter_client_id)
|
|
end)
|
|
end
|
|
|
|
diagnostic_ns = diagnostic_ns or M._get_diagnostic_namespace(client_id)
|
|
sign_ns = sign_ns or M._get_sign_namespace(client_id)
|
|
diagnostic_cache_extmarks[bufnr][client_id] = {}
|
|
|
|
assert(bufnr, "bufnr is required")
|
|
assert(diagnostic_ns, "Need diagnostic_ns, got nil")
|
|
assert(sign_ns, string.format("Need sign_ns, got nil %s", sign_ns))
|
|
|
|
-- clear sign group
|
|
vim.fn.sign_unplace(sign_ns, {buffer=bufnr})
|
|
|
|
-- clear virtual text namespace
|
|
api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1)
|
|
end
|
|
-- }}}
|
|
-- Diagnostic Insert Leave Handler {{{
|
|
|
|
--- Callback scheduled for after leaving insert mode
|
|
---
|
|
--- Used to handle
|
|
--@private
|
|
function M._execute_scheduled_display(bufnr, client_id)
|
|
local args = _bufs_waiting_to_update[bufnr][client_id]
|
|
if not args then
|
|
return
|
|
end
|
|
|
|
-- Clear the args so we don't display unnecessarily.
|
|
_bufs_waiting_to_update[bufnr][client_id] = nil
|
|
|
|
M.display(nil, bufnr, client_id, args)
|
|
end
|
|
|
|
local registered = {}
|
|
|
|
local make_augroup_key = function(bufnr, client_id)
|
|
return string.format("LspDiagnosticInsertLeave:%s:%s", bufnr, client_id)
|
|
end
|
|
|
|
--- Table of autocmd events to fire the update for displaying new diagnostic information
|
|
M.insert_leave_auto_cmds = { "InsertLeave", "CursorHoldI" }
|
|
|
|
--- Used to schedule diagnostic updates upon leaving insert mode.
|
|
---
|
|
--- For parameter description, see |M.display()|
|
|
function M._schedule_display(bufnr, client_id, args)
|
|
_bufs_waiting_to_update[bufnr][client_id] = args
|
|
|
|
local key = make_augroup_key(bufnr, client_id)
|
|
if not registered[key] then
|
|
vim.cmd(string.format("augroup %s", key))
|
|
vim.cmd(" au!")
|
|
vim.cmd(
|
|
string.format(
|
|
[[autocmd %s <buffer=%s> :lua vim.lsp.diagnostic._execute_scheduled_display(%s, %s)]],
|
|
table.concat(M.insert_leave_auto_cmds, ","),
|
|
bufnr,
|
|
bufnr,
|
|
client_id
|
|
)
|
|
)
|
|
vim.cmd("augroup END")
|
|
|
|
registered[key] = true
|
|
end
|
|
end
|
|
|
|
|
|
--- Used in tandem with
|
|
---
|
|
--- For parameter description, see |M.display()|
|
|
function M._clear_scheduled_display(bufnr, client_id)
|
|
local key = make_augroup_key(bufnr, client_id)
|
|
|
|
if registered[key] then
|
|
vim.cmd(string.format("augroup %s", key))
|
|
vim.cmd(" au!")
|
|
vim.cmd("augroup END")
|
|
|
|
registered[key] = nil
|
|
end
|
|
end
|
|
-- }}}
|
|
|
|
-- Diagnostic Private Highlight Utilies {{{
|
|
--- Get the severity highlight name
|
|
--@private
|
|
function M._get_severity_highlight_name(severity)
|
|
return virtual_text_highlight_map[severity]
|
|
end
|
|
|
|
--- Get floating severity highlight name
|
|
--@private
|
|
function M._get_floating_severity_highlight_name(severity)
|
|
return floating_highlight_map[severity]
|
|
end
|
|
|
|
--- This should be called to update the highlights for the LSP client.
|
|
function M._define_default_signs_and_highlights()
|
|
--@private
|
|
local function define_default_sign(name, properties)
|
|
if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then
|
|
vim.fn.sign_define(name, properties)
|
|
end
|
|
end
|
|
|
|
-- Initialize default diagnostic highlights
|
|
for severity, hi_info in pairs(diagnostic_severities) do
|
|
local default_highlight_name = default_highlight_map[severity]
|
|
highlight.create(default_highlight_name, hi_info, true)
|
|
|
|
-- Default link all corresponding highlights to the default highlight
|
|
highlight.link(virtual_text_highlight_map[severity], default_highlight_name, false)
|
|
highlight.link(floating_highlight_map[severity], default_highlight_name, false)
|
|
highlight.link(sign_highlight_map[severity], default_highlight_name, false)
|
|
end
|
|
|
|
-- Create all signs
|
|
for severity, sign_hl_name in pairs(sign_highlight_map) do
|
|
local severity_name = DiagnosticSeverity[severity]
|
|
|
|
define_default_sign(sign_hl_name, {
|
|
text = (severity_name or 'U'):sub(1, 1),
|
|
texthl = sign_hl_name,
|
|
linehl = '',
|
|
numhl = '',
|
|
})
|
|
end
|
|
|
|
-- Initialize Underline highlights
|
|
for severity, underline_highlight_name in pairs(underline_highlight_map) do
|
|
highlight.create(underline_highlight_name, {
|
|
cterm = 'underline',
|
|
gui = 'underline',
|
|
guisp = diagnostic_severities[severity].guifg
|
|
}, true)
|
|
end
|
|
end
|
|
-- }}}
|
|
-- Diagnostic Display {{{
|
|
|
|
--- |lsp-handler| for the method "textDocument/publishDiagnostics"
|
|
---
|
|
---@note Each of the configuration options accepts:
|
|
--- - `false`: Disable this feature
|
|
--- - `true`: Enable this feature, use default settings.
|
|
--- - `table`: Enable this feature, use overrides.
|
|
--- - `function`: Function with signature (bufnr, client_id) that returns any of the above.
|
|
--- <pre>
|
|
--- vim.lsp.handlers["textDocument/publishDiagnostics"] = vim.lsp.with(
|
|
--- vim.lsp.diagnostic.on_publish_diagnostics, {
|
|
--- -- Enable underline, use default values
|
|
--- underline = true,
|
|
--- -- Enable virtual text, override spacing to 4
|
|
--- virtual_text = {
|
|
--- spacing = 4,
|
|
--- },
|
|
--- -- Use a function to dynamically turn signs off
|
|
--- -- and on, using buffer local variables
|
|
--- signs = function(bufnr, client_id)
|
|
--- return vim.bo[bufnr].show_signs == false
|
|
--- end,
|
|
--- -- Disable a feature
|
|
--- update_in_insert = false,
|
|
--- }
|
|
--- )
|
|
--- </pre>
|
|
---
|
|
---@param config table Configuration table.
|
|
--- - underline: (default=true)
|
|
--- - Apply underlines to diagnostics.
|
|
--- - See |vim.lsp.diagnostic.set_underline()|
|
|
--- - virtual_text: (default=true)
|
|
--- - Apply virtual text to line endings.
|
|
--- - See |vim.lsp.diagnostic.set_virtual_text()|
|
|
--- - signs: (default=true)
|
|
--- - Apply signs for diagnostics.
|
|
--- - See |vim.lsp.diagnostic.set_signs()|
|
|
--- - update_in_insert: (default=false)
|
|
--- - Update diagnostics in InsertMode or wait until InsertLeave
|
|
--- - severity_sort: (default=false)
|
|
--- - Sort diagnostics (and thus signs and virtual text)
|
|
function M.on_publish_diagnostics(_, _, params, client_id, _, config)
|
|
local uri = params.uri
|
|
local bufnr = vim.uri_to_bufnr(uri)
|
|
|
|
if not bufnr then
|
|
return
|
|
end
|
|
|
|
local diagnostics = params.diagnostics
|
|
|
|
if config and if_nil(config.severity_sort, false) then
|
|
table.sort(diagnostics, function(a, b) return a.severity > b.severity end)
|
|
end
|
|
|
|
-- Always save the diagnostics, even if the buf is not loaded.
|
|
-- Language servers may report compile or build errors via diagnostics
|
|
-- Users should be able to find these, even if they're in files which
|
|
-- are not loaded.
|
|
M.save(diagnostics, bufnr, client_id)
|
|
|
|
-- Unloaded buffers should not handle diagnostics.
|
|
-- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen.
|
|
-- This should trigger another publish of the diagnostics.
|
|
--
|
|
-- In particular, this stops a ton of spam when first starting a server for current
|
|
-- unloaded buffers.
|
|
if not api.nvim_buf_is_loaded(bufnr) then
|
|
return
|
|
end
|
|
|
|
M.display(diagnostics, bufnr, client_id, config)
|
|
end
|
|
|
|
-- restores the extmarks set by M.display
|
|
--- @param last number last line that was changed
|
|
-- @private
|
|
local function restore_extmarks(bufnr, last)
|
|
for client_id, extmarks in pairs(diagnostic_cache_extmarks[bufnr]) do
|
|
local ns = M._get_diagnostic_namespace(client_id)
|
|
local extmarks_current = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
|
|
local found = {}
|
|
for _, extmark in ipairs(extmarks_current) do
|
|
-- nvim_buf_set_lines will move any extmark to the line after the last
|
|
-- nvim_buf_set_text will move any extmark to the last line
|
|
if extmark[2] ~= last + 1 then
|
|
found[extmark[1]] = true
|
|
end
|
|
end
|
|
for _, extmark in ipairs(extmarks) do
|
|
if not found[extmark[1]] then
|
|
local opts = extmark[4]
|
|
opts.id = extmark[1]
|
|
-- HACK: end_row should be end_line
|
|
if opts.end_row then
|
|
opts.end_line = opts.end_row
|
|
opts.end_row = nil
|
|
end
|
|
pcall(api.nvim_buf_set_extmark, bufnr, ns, extmark[2], extmark[3], opts)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- caches the extmarks set by M.display
|
|
-- @private
|
|
local function save_extmarks(bufnr, client_id)
|
|
bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
|
|
if not diagnostic_attached_buffers[bufnr] then
|
|
api.nvim_buf_attach(bufnr, false, {
|
|
on_lines = function(_, _, _, _, _, last)
|
|
restore_extmarks(bufnr, last - 1)
|
|
end,
|
|
on_detach = function()
|
|
diagnostic_cache_extmarks[bufnr] = nil
|
|
end})
|
|
diagnostic_attached_buffers[bufnr] = true
|
|
end
|
|
local ns = M._get_diagnostic_namespace(client_id)
|
|
diagnostic_cache_extmarks[bufnr][client_id] = api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {details = true})
|
|
end
|
|
|
|
--@private
|
|
--- Display diagnostics for the buffer, given a configuration.
|
|
function M.display(diagnostics, bufnr, client_id, config)
|
|
if diagnostic_disabled[bufnr][client_id] then
|
|
return
|
|
end
|
|
|
|
config = vim.lsp._with_extend('vim.lsp.diagnostic.on_publish_diagnostics', {
|
|
signs = true,
|
|
underline = true,
|
|
virtual_text = true,
|
|
update_in_insert = false,
|
|
severity_sort = false,
|
|
}, config)
|
|
|
|
-- TODO(tjdevries): Consider how we can make this a "standardized" kind of thing for |lsp-handlers|.
|
|
-- It seems like we would probably want to do this more often as we expose more of them.
|
|
-- It provides a very nice functional interface for people to override configuration.
|
|
local resolve_optional_value = function(option)
|
|
local enabled_val = {}
|
|
|
|
if not option then
|
|
return false
|
|
elseif option == true then
|
|
return enabled_val
|
|
elseif type(option) == 'function' then
|
|
local val = option(bufnr, client_id)
|
|
if val == true then
|
|
return enabled_val
|
|
else
|
|
return val
|
|
end
|
|
elseif type(option) == 'table' then
|
|
return option
|
|
else
|
|
error("Unexpected option type: " .. vim.inspect(option))
|
|
end
|
|
end
|
|
|
|
if resolve_optional_value(config.update_in_insert) then
|
|
M._clear_scheduled_display(bufnr, client_id)
|
|
else
|
|
local mode = vim.api.nvim_get_mode()
|
|
|
|
if string.sub(mode.mode, 1, 1) == 'i' then
|
|
M._schedule_display(bufnr, client_id, config)
|
|
return
|
|
end
|
|
end
|
|
|
|
M.clear(bufnr, client_id)
|
|
|
|
diagnostics = diagnostics or M.get(bufnr, client_id)
|
|
|
|
vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged")
|
|
|
|
if not diagnostics or vim.tbl_isempty(diagnostics) then
|
|
return
|
|
end
|
|
|
|
local underline_opts = resolve_optional_value(config.underline)
|
|
if underline_opts then
|
|
M.set_underline(diagnostics, bufnr, client_id, nil, underline_opts)
|
|
end
|
|
|
|
local virtual_text_opts = resolve_optional_value(config.virtual_text)
|
|
if virtual_text_opts then
|
|
M.set_virtual_text(diagnostics, bufnr, client_id, nil, virtual_text_opts)
|
|
end
|
|
|
|
local signs_opts = resolve_optional_value(config.signs)
|
|
if signs_opts then
|
|
M.set_signs(diagnostics, bufnr, client_id, nil, signs_opts)
|
|
end
|
|
|
|
-- cache extmarks
|
|
save_extmarks(bufnr, client_id)
|
|
end
|
|
|
|
--- Redraw diagnostics for the given buffer and client
|
|
---
|
|
--- This calls the "textDocument/publishDiagnostics" handler manually using
|
|
--- the cached diagnostics already received from the server. This can be useful
|
|
--- for redrawing diagnostics after making changes in diagnostics
|
|
--- configuration. |lsp-handler-configuration|
|
|
---
|
|
--- @param bufnr (optional, number): Buffer handle, defaults to current
|
|
--- @param client_id (optional, number): Redraw diagnostics for the given
|
|
--- client. The default is to redraw diagnostics for all attached
|
|
--- clients.
|
|
function M.redraw(bufnr, client_id)
|
|
bufnr = get_bufnr(bufnr)
|
|
if not client_id then
|
|
return vim.lsp.for_each_buffer_client(bufnr, function(client)
|
|
M.redraw(bufnr, client.id)
|
|
end)
|
|
end
|
|
|
|
-- We need to invoke the publishDiagnostics handler directly instead of just
|
|
-- calling M.display so that we can preserve any custom configuration options
|
|
-- the user may have set with vim.lsp.with.
|
|
vim.lsp.handlers["textDocument/publishDiagnostics"](
|
|
nil,
|
|
"textDocument/publishDiagnostics",
|
|
{
|
|
uri = vim.uri_from_bufnr(bufnr),
|
|
diagnostics = M.get(bufnr, client_id),
|
|
},
|
|
client_id,
|
|
bufnr
|
|
)
|
|
end
|
|
|
|
-- }}}
|
|
-- Diagnostic User Functions {{{
|
|
|
|
--- Open a floating window with the diagnostics from {line_nr}
|
|
---
|
|
--- The floating window can be customized with the following highlight groups:
|
|
--- <pre>
|
|
--- LspDiagnosticsFloatingError
|
|
--- LspDiagnosticsFloatingWarning
|
|
--- LspDiagnosticsFloatingInformation
|
|
--- LspDiagnosticsFloatingHint
|
|
--- </pre>
|
|
---@param opts table Configuration table
|
|
--- - show_header (boolean, default true): Show "Diagnostics:" header.
|
|
--- - Plus all the opts for |vim.lsp.diagnostic.get_line_diagnostics()|
|
|
--- and |vim.lsp.util.open_floating_preview()| can be used here.
|
|
---@param bufnr number The buffer number
|
|
---@param line_nr number The line number
|
|
---@param client_id number|nil the client id
|
|
---@return table {popup_bufnr, win_id}
|
|
function M.show_line_diagnostics(opts, bufnr, line_nr, client_id)
|
|
opts = opts or {}
|
|
|
|
local show_header = if_nil(opts.show_header, true)
|
|
|
|
bufnr = bufnr or 0
|
|
line_nr = line_nr or (vim.api.nvim_win_get_cursor(0)[1] - 1)
|
|
|
|
local lines = {}
|
|
local highlights = {}
|
|
if show_header then
|
|
table.insert(lines, "Diagnostics:")
|
|
table.insert(highlights, {0, "Bold"})
|
|
end
|
|
|
|
local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
|
|
if vim.tbl_isempty(line_diagnostics) then return end
|
|
|
|
for i, diagnostic in ipairs(line_diagnostics) do
|
|
local prefix = string.format("%d. ", i)
|
|
local hiname = M._get_floating_severity_highlight_name(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
|
|
|
|
opts.focus_id = "line_diagnostics"
|
|
local popup_bufnr, winnr = util.open_floating_preview(lines, 'plaintext', opts)
|
|
for i, hi in ipairs(highlights) do
|
|
local prefixlen, hiname = unpack(hi)
|
|
-- Start highlight after the prefix
|
|
api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1)
|
|
end
|
|
|
|
return popup_bufnr, winnr
|
|
end
|
|
|
|
|
|
--- Clear diagnotics and diagnostic cache
|
|
---
|
|
--- Handles saving diagnostics from multiple clients in the same buffer.
|
|
---@param client_id number
|
|
---@param buffer_client_map table map of buffers to active clients
|
|
function M.reset(client_id, buffer_client_map)
|
|
buffer_client_map = vim.deepcopy(buffer_client_map)
|
|
vim.schedule(function()
|
|
for bufnr, client_ids in pairs(buffer_client_map) do
|
|
if client_ids[client_id] then
|
|
clear_diagnostic_cache(bufnr, client_id)
|
|
M.clear(bufnr, client_id)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
--- Gets diagnostics, converts them to quickfix/location list items, and applies the item_handler callback to the items.
|
|
---@param item_handler function Callback to apply to the diagnostic items
|
|
---@param command string|nil Command to execute after applying the item_handler
|
|
---@param opts table|nil Configuration table. Keys:
|
|
--- - {client_id}: (number)
|
|
--- - If nil, will consider all clients attached to buffer.
|
|
--- - {severity}: (DiagnosticSeverity)
|
|
--- - Exclusive severity to consider. Overrides {severity_limit}
|
|
--- - {severity_limit}: (DiagnosticSeverity)
|
|
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
|
|
--- - {workspace}: (boolean, default false)
|
|
--- - Set the list with workspace diagnostics
|
|
local function apply_to_diagnostic_items(item_handler, command, opts)
|
|
opts = opts or {}
|
|
local current_bufnr = api.nvim_get_current_buf()
|
|
local diags = opts.workspace and M.get_all(opts.client_id) or {
|
|
[current_bufnr] = M.get(current_bufnr, opts.client_id)
|
|
}
|
|
local predicate = function(d)
|
|
local severity = to_severity(opts.severity)
|
|
if severity then
|
|
return d.severity == severity
|
|
end
|
|
local severity_limit = to_severity(opts.severity_limit)
|
|
if severity_limit then
|
|
return d.severity <= severity_limit
|
|
end
|
|
return true
|
|
end
|
|
local items = util.diagnostics_to_items(diags, predicate)
|
|
item_handler(items)
|
|
if command then
|
|
vim.cmd(command)
|
|
end
|
|
end
|
|
|
|
--- Sets the quickfix list
|
|
---@param opts table|nil Configuration table. Keys:
|
|
--- - {open}: (boolean, default true)
|
|
--- - Open quickfix list after set
|
|
--- - {client_id}: (number)
|
|
--- - If nil, will consider all clients attached to buffer.
|
|
--- - {severity}: (DiagnosticSeverity)
|
|
--- - Exclusive severity to consider. Overrides {severity_limit}
|
|
--- - {severity_limit}: (DiagnosticSeverity)
|
|
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
|
|
--- - {workspace}: (boolean, default true)
|
|
--- - Set the list with workspace diagnostics
|
|
function M.set_qflist(opts)
|
|
opts = opts or {}
|
|
opts.workspace = if_nil(opts.workspace, true)
|
|
local open_qflist = if_nil(opts.open, true)
|
|
local command = open_qflist and [[copen]] or nil
|
|
apply_to_diagnostic_items(util.set_qflist, command, opts)
|
|
end
|
|
|
|
--- Sets the location list
|
|
---@param opts table|nil Configuration table. Keys:
|
|
--- - {open}: (boolean, default true)
|
|
--- - Open loclist after set
|
|
--- - {client_id}: (number)
|
|
--- - If nil, will consider all clients attached to buffer.
|
|
--- - {severity}: (DiagnosticSeverity)
|
|
--- - Exclusive severity to consider. Overrides {severity_limit}
|
|
--- - {severity_limit}: (DiagnosticSeverity)
|
|
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
|
|
--- - {workspace}: (boolean, default false)
|
|
--- - Set the list with workspace diagnostics
|
|
function M.set_loclist(opts)
|
|
opts = opts or {}
|
|
local open_loclist = if_nil(opts.open, true)
|
|
local command = open_loclist and [[lopen]] or nil
|
|
apply_to_diagnostic_items(util.set_loclist, command, opts)
|
|
end
|
|
|
|
--- Disable diagnostics for the given buffer and client
|
|
--- @param bufnr (optional, number): Buffer handle, defaults to current
|
|
--- @param client_id (optional, number): Disable diagnostics for the given
|
|
--- client. The default is to disable diagnostics for all attached
|
|
--- clients.
|
|
-- Note that when diagnostics are disabled for a buffer, the server will still
|
|
-- send diagnostic information and the client will still process it. The
|
|
-- diagnostics are simply not displayed to the user.
|
|
function M.disable(bufnr, client_id)
|
|
if not client_id then
|
|
return vim.lsp.for_each_buffer_client(bufnr, function(client)
|
|
M.disable(bufnr, client.id)
|
|
end)
|
|
end
|
|
|
|
diagnostic_disabled[bufnr][client_id] = true
|
|
M.clear(bufnr, client_id)
|
|
end
|
|
|
|
--- Enable diagnostics for the given buffer and client
|
|
--- @param bufnr (optional, number): Buffer handle, defaults to current
|
|
--- @param client_id (optional, number): Enable diagnostics for the given
|
|
--- client. The default is to enable diagnostics for all attached
|
|
--- clients.
|
|
function M.enable(bufnr, client_id)
|
|
if not client_id then
|
|
return vim.lsp.for_each_buffer_client(bufnr, function(client)
|
|
M.enable(bufnr, client.id)
|
|
end)
|
|
end
|
|
|
|
if not diagnostic_disabled[bufnr][client_id] then
|
|
return
|
|
end
|
|
|
|
diagnostic_disabled[bufnr][client_id] = nil
|
|
|
|
M.redraw(bufnr, client_id)
|
|
end
|
|
-- }}}
|
|
|
|
return M
|