aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp/diagnostic.lua
diff options
context:
space:
mode:
authorGregory Anders <greg@gpanders.com>2021-09-06 20:21:18 -0600
committerGregory Anders <greg@gpanders.com>2021-09-15 14:09:47 -0600
commita5bbb932f9094098bd656d3f6be3c58344576709 (patch)
tree80f35362c9e94853e9e8898416120a3ede623362 /runtime/lua/vim/lsp/diagnostic.lua
parent6188926e00081ae4b1a33d5fd12692a6749a2144 (diff)
downloadrneovim-a5bbb932f9094098bd656d3f6be3c58344576709.tar.gz
rneovim-a5bbb932f9094098bd656d3f6be3c58344576709.tar.bz2
rneovim-a5bbb932f9094098bd656d3f6be3c58344576709.zip
refactor: move vim.lsp.diagnostic to vim.diagnostic
This generalizes diagnostic handling outside of just the scope of LSP. LSP clients are now a specific case of a diagnostic producer, but the diagnostic subsystem is decoupled from the LSP subsystem (or will be, eventually). More discussion at [1]. [1]: https://github.com/neovim/neovim/pull/15585
Diffstat (limited to 'runtime/lua/vim/lsp/diagnostic.lua')
-rw-r--r--runtime/lua/vim/lsp/diagnostic.lua1469
1 files changed, 368 insertions, 1101 deletions
diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua
index ccd325b1ac..338491701f 100644
--- a/runtime/lua/vim/lsp/diagnostic.lua
+++ b/runtime/lua/vim/lsp/diagnostic.lua
@@ -1,48 +1,4 @@
-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
---
@@ -57,70 +13,9 @@ end
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)
+---@private
+local function get_client_id(client_id)
if client_id == nil then
client_id = DEFAULT_CLIENT_ID
end
@@ -128,179 +23,112 @@ local get_client_id = function(client_id)
return client_id
end
-local get_bufnr = function(bufnr)
+---@private
+local function get_bufnr(bufnr)
if not bufnr then
- return api.nvim_get_current_buf()
+ return vim.api.nvim_get_current_buf()
elseif bufnr == 0 then
- return api.nvim_get_current_buf()
+ return vim.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]
+local function severity_lsp_to_vim(severity)
+ if type(severity) == 'string' then
+ severity = vim.lsp.protocol.DiagnosticSeverity[severity]
+ end
+ return severity
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)
+local function severity_vim_to_lsp(severity)
+ if type(severity) == 'string' then
+ severity = vim.diagnostic.severity[severity]
end
- return diagnostics_by_line
+ return severity
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
+---@private
+local function line_byte_from_position(lines, lnum, col, offset_encoding)
+ if offset_encoding == "utf-8" then
+ return col
+ end
- counts[diagnostic.severity] = val + 1
- end
+ local line = lines[lnum + 1]
+ local ok, result = pcall(vim.str_byteindex, line, col, offset_encoding == "utf-16")
+ if ok then
+ return result
end
- return counts
+ return col
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
+local function get_buf_lines(bufnr)
+ if vim.api.nvim_buf_is_loaded(bufnr) then
+ return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
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)
+ local filename = vim.api.nvim_buf_get_name(bufnr)
+ local f = io.open(filename)
+ local lines = vim.split(f:read("*a"), "\n")
+ f:close()
+ return lines
end
+---@private
+local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
+ local buf_lines = get_buf_lines(bufnr)
+ local client = vim.lsp.get_client_by_id(client_id)
+ local offset_encoding = client and client.offset_encoding or "utf-16"
+ return vim.tbl_map(function(diagnostic)
+ local start = diagnostic.range.start
+ local _end = diagnostic.range["end"]
+ return {
+ lnum = start.line,
+ col = line_byte_from_position(buf_lines, start.line, start.character, offset_encoding),
+ end_lnum = _end.line,
+ end_col = line_byte_from_position(buf_lines, _end.line, _end.character, offset_encoding),
+ severity = severity_lsp_to_vim(diagnostic.severity),
+ message = diagnostic.message
+ }
+ end, 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)
+local function diagnostic_vim_to_lsp(diagnostics)
+ return vim.tbl_map(function(diagnostic)
+ return {
+ range = {
+ start = {
+ line = diagnostic.lnum,
+ character = diagnostic.col,
+ },
+ ["end"] = {
+ line = diagnostic.end_lnum,
+ character = diagnostic.end_col,
+ },
+ },
+ severity = severity_vim_to_lsp(diagnostic.severity),
+ message = diagnostic.message,
+ }
+ end, diagnostics)
+end
- diagnostic_cache[bufnr][client_id] = nil
- diagnostic_cache_lines[bufnr][client_id] = nil
- diagnostic_cache_counts[bufnr][client_id] = nil
+local _client_namespaces = {}
+
+--- Get the diagnostic namespace associated with an LSP client |vim.diagnostic|.
+---
+---@param client_id number The id of the LSP client
+function M.get_namespace(client_id)
+ vim.validate { client_id = { client_id, 'n' } }
+ if not _client_namespaces[client_id] then
+ local name = string.format("vim.lsp.client-%d", client_id)
+ _client_namespaces[client_id] = vim.api.nvim_create_namespace(name)
+ end
+ return _client_namespaces[client_id]
end
--- Save diagnostics to the current buffer.
@@ -309,86 +137,146 @@ end
---@param diagnostics Diagnostic[]
---@param bufnr number
---@param client_id number
+---@private
function M.save(diagnostics, bufnr, client_id)
- validate {
- diagnostics = {diagnostics, 't'},
- bufnr = {bufnr, 'n'},
- client_id = {client_id, 'n', true},
- }
+ local namespace = M.get_namespace(client_id)
+ vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
+end
+-- }}}
+
+--- |lsp-handler| for the method "textDocument/publishDiagnostics"
+---
+--- See |vim.diagnostic.config()| for configuration options. Handler-specific
+--- configuration can be set using |vim.lsp.with()|:
+--- <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 (see |vim.diagnostic.config()|).
+function M.on_publish_diagnostics(_, result, ctx, config)
+ local client_id = ctx.client_id
+ local uri = result.uri
+ local bufnr = vim.uri_to_bufnr(uri)
- if not diagnostics then return end
+ if not bufnr then
+ return
+ end
- bufnr = get_bufnr(bufnr)
client_id = get_client_id(client_id)
+ local namespace = M.get_namespace(client_id)
+ local diagnostics = result.diagnostics
- if not _diagnostic_cleanup[bufnr][client_id] then
- _diagnostic_cleanup[bufnr][client_id] = true
+ if config then
+ if vim.F.if_nil(config.severity_sort, false) then
+ table.sort(diagnostics, function(a, b) return a.severity > b.severity end)
+ end
- -- 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
+ for _, opt in pairs(config) do
+ if type(opt) == 'table' then
+ if not opt.severity and opt.severity_limit then
+ opt.severity = {min=severity_lsp_to_vim(opt.severity_limit)}
+ end
end
- })
+ end
+
+ vim.diagnostic.config(config, namespace)
end
- set_diagnostic_cache(diagnostics, bufnr, client_id)
+ vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id))
+
+ -- Keep old autocmd for back compat. This should eventually be removed.
+ vim.api.nvim_command("doautocmd <nomodeline> User LspDiagnosticsChanged")
+end
+
+--- Clear diagnotics and diagnostic cache.
+---
+--- Diagnostic producers should prefer |vim.diagnostic.reset()|. However,
+--- this method signature is still used internally in some parts of the LSP
+--- implementation so it's simply marked @private rather than @deprecated.
+---
+---@param client_id number
+---@param buffer_client_map table map of buffers to active clients
+---@private
+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
+ local namespace = M.get_namespace(client_id)
+ vim.diagnostic.reset(namespace, bufnr)
+ end
+ end
+ end)
end
--- }}}
--- Diagnostic Retrieval {{{
+-- Deprecated Functions {{{
--- Get all diagnostics for clients
---
+---@deprecated Prefer |vim.diagnostic.get()|
+---
---@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
+ local result = {}
+ local namespace
+ if client_id then
+ namespace = M.get_namespace(client_id)
+ end
+ for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
+ local diagnostics = diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, {namespace = namespace}))
+ result[bufnr] = diagnostics
end
- return diagnostics_by_bufnr
+ return result
end
--- Return associated diagnostics for bufnr
---
+---@deprecated Prefer |vim.diagnostic.get()|
+---
---@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.
---@param predicate function|nil Optional function for filtering diagnostics
function M.get(bufnr, client_id, predicate)
+ predicate = predicate or function() return true end
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, predicate)
-
+ vim.lsp.for_each_buffer_client(bufnr, function(_, iter_client_id, _)
+ local iter_diagnostics = vim.tbl_filter(predicate, 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_vim_to_lsp(all_diagnostics)
end
- predicate = predicate or function(_) return true end
- local client_diagnostics = {}
- for _, diagnostic in ipairs(diagnostic_cache[bufnr][client_id] or {}) do
- if predicate(diagnostic) then
- table.insert(client_diagnostics, diagnostic)
- end
- end
- return client_diagnostics
+ local namespace = M.get_namespace(client_id)
+ return diagnostic_vim_to_lsp(vim.tbl_filter(predicate, vim.diagnostic.get(bufnr, {namespace=namespace})))
end
--- Get the diagnostics by line
---
+--- Marked private as this is used internally by the LSP subsystem, but
+--- most users should instead prefer |vim.diagnostic.get()|.
+---
---@param bufnr number|nil The buffer number
---@param line_nr number|nil The line number
---@param opts table|nil Configuration keys
@@ -398,216 +286,134 @@ end
--- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid.
---@param client_id|nil number the client id
---@return table Table with map of line number to list of diagnostics.
--- Structured: { [1] = {...}, [5] = {.... } }
+--- Structured: { [1] = {...}, [5] = {.... } }
+---@private
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 {}
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
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))
+ if client_id then
+ opts.namespace = M.get_namespace(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)
+ if not line_nr then
+ line_nr = vim.api.nvim_win_get_cursor(0)[1] - 1
end
- table.sort(line_diagnostics, function(a, b) return a.severity < b.severity end)
+ opts.lnum = line_nr
- return line_diagnostics
+ return diagnostic_vim_to_lsp(vim.diagnostic.get(bufnr, opts))
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
---- autocmd BufWinEnter * let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
---- </pre>
+---@deprecated Prefer |vim.diagnostic.get_count()|
---
---@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 find the next diagnostic relative to a position
----@return table the next diagnostic if found
-local _next_diagnostic = function(position, search_forward, bufnr, opts, client_id)
- position[1] = position[1] - 1
- bufnr = bufnr or vim.api.nvim_get_current_buf()
- local wrap = if_nil(opts.wrap, true)
- local line_count = vim.api.nvim_buf_line_count(bufnr)
- for i = 0, line_count do
- local offset = i * (search_forward and 1 or -1)
- local line_nr = position[1] + offset
- if line_nr < 0 or line_nr >= line_count then
- if not wrap then
- return
- end
- line_nr = (line_nr + line_count) % line_count
- end
- local line_diagnostics = M.get_line_diagnostics(bufnr, line_nr, opts, client_id)
- if line_diagnostics and not vim.tbl_isempty(line_diagnostics) then
- local sort_diagnostics, is_next
- if search_forward then
- sort_diagnostics = function(a, b) return a.range.start.character < b.range.start.character end
- is_next = function(diagnostic) return diagnostic.range.start.character > position[2] end
- else
- sort_diagnostics = function(a, b) return a.range.start.character > b.range.start.character end
- is_next = function(diagnostic) return diagnostic.range.start.character < position[2] end
- end
- table.sort(line_diagnostics, sort_diagnostics)
- if i == 0 then
- for _, v in pairs(line_diagnostics) do
- if is_next(v) then
- return v
- end
- end
- else
- return line_diagnostics[1]
- end
- end
- end
-end
-
----@private
---- Helper function to return a position from a diagnostic
----
----@return table {row, col}
-local function _diagnostic_pos(opts, diagnostic)
- 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 not diagnostic then return false end
-
- return to_position(diagnostic.range.start, bufnr)
-end
-
----@private
--- Move to the diagnostic position
-local function _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
+ severity = severity_lsp_to_vim(severity)
+ local opts = { severity = severity }
+ if client_id ~= nil then
+ opts.namespace = M.get_namespace(client_id)
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_position_diagnostics(opts.popup_opts, vim.api.nvim_win_get_buf(win_id))
- end)
- end
+ return #vim.diagnostic.get(bufnr, opts)
end
--- Get the previous diagnostic closest to the cursor_position
---
+---@deprecated Prefer |vim.diagnostic.get_prev()|
+---
---@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 _next_diagnostic(cursor_position, false, bufnr, opts, opts.client_id)
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return diagnostic_vim_to_lsp({vim.diagnostic.get_prev(opts)})[1]
end
--- Return the pos, {row, col}, for the prev diagnostic in the current buffer.
+---
+---@deprecated Prefer |vim.diagnostic.get_prev_pos()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Previous diagnostic position
function M.get_prev_pos(opts)
- return _diagnostic_pos(
- opts,
- M.get_prev(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.get_prev_pos(opts)
end
--- Move to the previous diagnostic
+---
+---@deprecated Prefer |vim.diagnostic.goto_prev()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
function M.goto_prev(opts)
- return _diagnostic_move_pos(
- "DiagnosticPrevious",
- opts,
- M.get_prev_pos(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.goto_prev(opts)
end
--- Get the next diagnostic closest to the cursor_position
+---
+---@deprecated Prefer |vim.diagnostic.get_next()|
+---
---@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 _next_diagnostic(cursor_position, true, bufnr, opts, opts.client_id)
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return diagnostic_vim_to_lsp({vim.diagnostic.get_next(opts)})[1]
end
--- Return the pos, {row, col}, for the next diagnostic in the current buffer.
+---
+---@deprecated Prefer |vim.diagnostic.get_next_pos()|
+---
---@param opts table See |vim.lsp.diagnostic.goto_next()|
---@return table Next diagnostic position
function M.get_next_pos(opts)
- return _diagnostic_pos(
- opts,
- M.get_next(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.get_next_pos(opts)
end
--- Move to the next diagnostic
+---
+---@deprecated Prefer |vim.diagnostic.goto_next()|
+---
---@param opts table|nil Configuration table. Keys:
--- - {client_id}: (number)
--- - If nil, will consider all clients attached to buffer.
@@ -626,17 +432,20 @@ end
--- - {win_id}: (number, default 0)
--- - Window ID
function M.goto_next(opts)
- return _diagnostic_move_pos(
- "DiagnosticNext",
- opts,
- M.get_next_pos(opts)
- )
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ end
+ return vim.diagnostic.goto_next(opts)
end
--- }}}
--- Diagnostic Setters {{{
--- Set signs for given diagnostics
---
+---@deprecated Prefer |vim.diagnostic._set_signs()|
+---
--- Sign characters can be customized with the following commands:
---
--- <pre>
@@ -654,35 +463,12 @@ end
--- - 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
- }
- )
+ local namespace = M.get_namespace(client_id)
+ if opts and not opts.severity and opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
+ local ok = vim.diagnostic._set_signs(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
if not ok then
log.debug("Failed to place signs:", diagnostics)
end
@@ -690,6 +476,8 @@ end
--- Set underline for given diagnostics
---
+---@deprecated Prefer |vim.diagnostic._set_underline()|
+---
--- Underline highlights can be customized by changing the following |:highlight| groups.
---
--- <pre>
@@ -707,34 +495,17 @@ end
--- - 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)
- )
+ local namespace = M.get_namespace(client_id)
+ if opts and not opts.severity and opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
+ return vim.diagnostic._set_underline(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
end
--- Virtual Text {{{
--- Set virtual text given diagnostics
---
+---@deprecated Prefer |vim.diagnostic._set_virtual_text()|
+---
--- Virtual text highlights can be customized by changing the following |:highlight| groups.
---
--- <pre>
@@ -754,35 +525,17 @@ end
--- - 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_extmark(bufnr, diagnostic_ns, line, 0, {
- virt_text = virt_texts,
- })
- end
+ local namespace = M.get_namespace(client_id)
+ if opts and not opts.severity and opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
end
+ return vim.diagnostic._set_virtual_text(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id), opts)
end
--- Default function to get text chunks to display using |nvim_buf_set_extmark()|.
+---
+---@deprecated Prefer |vim.diagnostic.get_virt_text_chunks()|
+---
---@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
@@ -790,399 +543,56 @@ end
---@return an array of [text, hl_group] arrays. This can be passed directly to
--- the {virt_text} option of |nvim_buf_set_extmark()|.
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)
+ return vim.diagnostic.get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts)
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.
+--- Open a floating window with the diagnostics from {position}
---
---- 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
+---@deprecated Prefer |vim.diagnostic.show_position_diagnostics()|
---
---- 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)
+---@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.
+--- - all opts for |show_diagnostics()| can be used here
+---@param buf_nr number|nil The buffer number
+---@param position table|nil The (0,0)-indexed position
+---@return table {popup_bufnr, win_id}
+function M.show_position_diagnostics(opts, buf_nr, position)
+ if opts then
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
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
+ return vim.diagnostic.show_position_diagnostics(opts, buf_nr, position)
end
--- }}}
--- Diagnostic Display {{{
---- |lsp-handler| for the method "textDocument/publishDiagnostics"
+--- Open a floating window with the diagnostics from {line_nr}
---
----@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>
+---@deprecated Prefer |vim.diagnostic.show_line_diagnostics()|
---
----@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(_, result, ctx, config)
- local client_id = ctx.client_id
- local uri = result.uri
- local bufnr = vim.uri_to_bufnr(uri)
-
- if not bufnr then
- return
- end
-
- local diagnostics = result.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)
+---@param opts table Configuration table
+--- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
+--- |show_diagnostics()| can be used here
+---@param buf_nr number|nil The buffer number
+---@param line_nr number|nil The line number
+---@param client_id number|nil the client id
+---@return table {popup_bufnr, win_id}
+function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
+ if client_id then
+ opts = opts or {}
+ opts.namespace = M.get_namespace(client_id)
end
-
- -- cache extmarks
- save_extmarks(bufnr, client_id)
+ return vim.diagnostic.show_line_diagnostics(opts, buf_nr, line_nr)
end
--- Redraw diagnostics for the given buffer and client
---
+---@deprecated Prefer |vim.diagnostic.redraw()|
+---
--- 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
@@ -1200,183 +610,14 @@ function 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,
- {
- uri = vim.uri_from_bufnr(bufnr),
- diagnostics = M.get(bufnr, client_id),
- },
- {
- method = "textDocument/publishDiagnostics",
- client_id = client_id,
- bufnr = bufnr,
- }
- )
- end
-
-
----@private
---- Open a floating window with the provided diagnostics
----
---- 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
---- - all opts for |vim.lsp.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 = 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 = 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
-
- 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
-
-
--- }}}
--- Diagnostic User Functions {{{
-
---- Open a floating window with the diagnostics from {position}
----@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.
---- - all opts for |show_diagnostics()| can be used here
----@param buf_nr number|nil The buffer number
----@param position table|nil The (0,0)-indexed position
----@return table {popup_bufnr, win_id}
-function M.show_position_diagnostics(opts, buf_nr, position)
- opts = opts or {}
- opts.focus_id = "position_diagnostics"
- buf_nr = buf_nr or vim.api.nvim_get_current_buf()
- 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.range['start'].line and
- position[2] >= diag.range['start'].character and
- (position[2] <= diag.range['end'].character or position[1] < diag.range['end'].line)
- end
- local position_diagnostics = M.get(buf_nr, nil, match_position_predicate)
- if opts.severity then
- position_diagnostics = filter_to_severity_limit(opts.severity, position_diagnostics)
- elseif opts.severity_limit then
- position_diagnostics = filter_by_severity_limit(opts.severity_limit, position_diagnostics)
- end
- table.sort(position_diagnostics, function(a, b) return a.severity < b.severity end)
- return show_diagnostics(opts, position_diagnostics)
-end
-
---- Open a floating window with the diagnostics from {line_nr}
-
----@param opts table Configuration table
---- - all opts for |vim.lsp.diagnostic.get_line_diagnostics()| and
---- |show_diagnostics()| can be used here
----@param buf_nr number|nil The buffer number
----@param line_nr number|nil The line number
----@param client_id number|nil the client id
----@return table {popup_bufnr, win_id}
-function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id)
- opts = opts or {}
- opts.focus_id = "line_diagnostics"
- line_nr = line_nr or (vim.api.nvim_win_get_cursor(0)[1] - 1)
- local line_diagnostics = M.get_line_diagnostics(buf_nr, line_nr, opts, client_id)
- return show_diagnostics(opts, line_diagnostics)
-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
-
----@private
---- 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
+ local namespace = M.get_namespace(client_id)
+ return vim.diagnostic.show(namespace, bufnr)
end
--- Sets the quickfix list
+---
+---@deprecated Prefer |vim.diagnostic.setqflist()|
+---
---@param opts table|nil Configuration table. Keys:
--- - {open}: (boolean, default true)
--- - Open quickfix list after set
@@ -1390,13 +631,24 @@ end
--- - 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)
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ if opts.client_id then
+ opts.client_id = nil
+ opts.namespace = M.get_namespace(opts.client_id)
+ end
+ local workspace = vim.F.if_nil(opts.workspace, true)
+ opts.bufnr = not workspace and 0
+ return vim.diagnostic.setqflist(opts)
end
--- Sets the location list
+---
+---@deprecated Prefer |vim.diagnostic.setloclist()|
+---
---@param opts table|nil Configuration table. Keys:
--- - {open}: (boolean, default true)
--- - Open loclist after set
@@ -1410,12 +662,24 @@ end
--- - 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)
+ if opts.severity then
+ opts.severity = severity_lsp_to_vim(opts.severity)
+ elseif opts.severity_limit then
+ opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)}
+ end
+ if opts.client_id then
+ opts.client_id = nil
+ opts.namespace = M.get_namespace(opts.client_id)
+ end
+ local workspace = vim.F.if_nil(opts.workspace, false)
+ opts.bufnr = not workspace and 0
+ return vim.diagnostic.setloclist(opts)
end
--- Disable diagnostics for the given buffer and client
+---
+---@deprecated Prefer |vim.diagnostic.disable()|
+---
---@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
@@ -1430,11 +694,15 @@ function M.disable(bufnr, client_id)
end)
end
- diagnostic_disabled[bufnr][client_id] = true
- M.clear(bufnr, client_id)
+ bufnr = get_bufnr(bufnr)
+ local namespace = M.get_namespace(client_id)
+ return vim.diagnostic.disable(bufnr, namespace)
end
--- Enable diagnostics for the given buffer and client
+---
+---@deprecated Prefer |vim.diagnostic.enable()|
+---
---@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
@@ -1446,14 +714,13 @@ function 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)
+ bufnr = get_bufnr(bufnr)
+ local namespace = M.get_namespace(client_id)
+ return vim.diagnostic.enable(bufnr, namespace)
end
+
-- }}}
return M
+
+-- vim: fdm=marker