mirror of
https://github.com/neovim/neovim
synced 2025-07-16 09:11:51 +00:00
feat(lsp): workspace diagnostic support (#34262)
* refactor(lsp): remove underscore prefix from local variables * feat(lsp): workspace diagnostic support
This commit is contained in:
committed by
GitHub
parent
d75ffa5934
commit
cb4559bc32
@ -1855,6 +1855,18 @@ typehierarchy({kind}) *vim.lsp.buf.typehierarchy()*
|
||||
Parameters: ~
|
||||
• {kind} (`"subtypes"|"supertypes"`)
|
||||
|
||||
workspace_diagnostics({opts}) *vim.lsp.buf.workspace_diagnostics()*
|
||||
Request workspace-wide diagnostics.
|
||||
|
||||
Parameters: ~
|
||||
• {opts} (`table?`) A table with the following fields:
|
||||
• {client_id}? (`integer`) Only request diagnostics from the
|
||||
indicated client. If nil, the request is sent to all
|
||||
clients.
|
||||
|
||||
See also: ~
|
||||
• https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_dagnostics
|
||||
|
||||
workspace_symbol({query}, {opts}) *vim.lsp.buf.workspace_symbol()*
|
||||
Lists all symbols in the current workspace in the quickfix window.
|
||||
|
||||
|
@ -165,6 +165,8 @@ LSP
|
||||
non-applicable LSP clients.
|
||||
• |vim.lsp.is_enabled()| checks if a LSP config is enabled (without
|
||||
"resolving" it).
|
||||
• Support for `workspace/diagnostic`: |vim.lsp.buf.workspace_diagnostics()|
|
||||
https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_dagnostics
|
||||
|
||||
LUA
|
||||
|
||||
|
@ -1015,6 +1015,21 @@ function M.workspace_symbol(query, opts)
|
||||
request_with_opts(ms.workspace_symbol, params, opts)
|
||||
end
|
||||
|
||||
--- @class vim.lsp.WorkspaceDiagnosticsOpts
|
||||
--- @inlinedoc
|
||||
---
|
||||
--- Only request diagnostics from the indicated client. If nil, the request is sent to all clients.
|
||||
--- @field client_id? integer
|
||||
|
||||
--- Request workspace-wide diagnostics.
|
||||
--- @param opts? vim.lsp.WorkspaceDiagnosticsOpts
|
||||
--- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_dagnostics
|
||||
function M.workspace_diagnostics(opts)
|
||||
vim.validate('opts', opts, 'table', true)
|
||||
|
||||
lsp.diagnostic._workspace_diagnostics(opts or {})
|
||||
end
|
||||
|
||||
--- Send request to the server to resolve document highlights for the current
|
||||
--- text document position. This request can be triggered by a key mapping or
|
||||
--- by events such as `CursorHold`, e.g.:
|
||||
|
@ -1,6 +1,7 @@
|
||||
local protocol = require('vim.lsp.protocol')
|
||||
local lsp = vim.lsp
|
||||
local protocol = lsp.protocol
|
||||
local ms = protocol.Methods
|
||||
local util = vim.lsp.util
|
||||
local util = lsp.util
|
||||
|
||||
local api = vim.api
|
||||
|
||||
@ -9,10 +10,10 @@ local M = {}
|
||||
local augroup = api.nvim_create_augroup('nvim.lsp.diagnostic', {})
|
||||
|
||||
---@class (private) vim.lsp.diagnostic.BufState
|
||||
---@field enabled boolean Whether diagnostics are enabled for this buffer
|
||||
---@field pull_kind 'document'|'workspace'|'disabled' Whether diagnostics are being updated via document pull, workspace pull, or disabled.
|
||||
---@field client_result_id table<integer, string?> Latest responded `resultId`
|
||||
|
||||
---@type table<integer,vim.lsp.diagnostic.BufState>
|
||||
---@type table<integer, vim.lsp.diagnostic.BufState>
|
||||
local bufstates = {}
|
||||
|
||||
local DEFAULT_CLIENT_ID = -1
|
||||
@ -38,11 +39,11 @@ end
|
||||
---@param bufnr integer
|
||||
---@return string[]?
|
||||
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)
|
||||
if api.nvim_buf_is_loaded(bufnr) then
|
||||
return api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
end
|
||||
|
||||
local filename = vim.api.nvim_buf_get_name(bufnr)
|
||||
local filename = api.nvim_buf_get_name(bufnr)
|
||||
local f = io.open(filename)
|
||||
if not f then
|
||||
return
|
||||
@ -74,7 +75,7 @@ local function tags_lsp_to_vim(diagnostic, client_id)
|
||||
tags = tags or {}
|
||||
tags.deprecated = true
|
||||
else
|
||||
vim.lsp.log.info(string.format('Unknown DiagnosticTag %d from LSP client %d', tag, client_id))
|
||||
lsp.log.info(string.format('Unknown DiagnosticTag %d from LSP client %d', tag, client_id))
|
||||
end
|
||||
end
|
||||
return tags
|
||||
@ -86,7 +87,7 @@ end
|
||||
---@return vim.Diagnostic.Set[]
|
||||
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 client = lsp.get_client_by_id(client_id)
|
||||
local position_encoding = client and client.offset_encoding or 'utf-16'
|
||||
--- @param diagnostic lsp.Diagnostic
|
||||
--- @return vim.Diagnostic.Set
|
||||
@ -97,7 +98,7 @@ local function diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)
|
||||
if type(message) ~= 'string' then
|
||||
vim.notify_once(
|
||||
string.format('Unsupported Markup message from LSP client %d', client_id),
|
||||
vim.lsp.log_levels.ERROR
|
||||
lsp.log_levels.ERROR
|
||||
)
|
||||
--- @diagnostic disable-next-line: undefined-field,no-unknown
|
||||
message = diagnostic.message.value
|
||||
@ -174,10 +175,10 @@ function M.from(diagnostics)
|
||||
end
|
||||
|
||||
---@type table<integer, integer>
|
||||
local _client_push_namespaces = {}
|
||||
local client_push_namespaces = {}
|
||||
|
||||
---@type table<string, integer>
|
||||
local _client_pull_namespaces = {}
|
||||
local client_pull_namespaces = {}
|
||||
|
||||
--- Get the diagnostic namespace associated with an LSP client |vim.diagnostic| for diagnostics
|
||||
---
|
||||
@ -186,7 +187,7 @@ local _client_pull_namespaces = {}
|
||||
function M.get_namespace(client_id, is_pull)
|
||||
vim.validate('client_id', client_id, 'number')
|
||||
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
local client = lsp.get_client_by_id(client_id)
|
||||
if is_pull then
|
||||
local server_id =
|
||||
vim.tbl_get((client or {}).server_capabilities or {}, 'diagnosticProvider', 'identifier')
|
||||
@ -196,19 +197,19 @@ function M.get_namespace(client_id, is_pull)
|
||||
client_id,
|
||||
server_id or 'nil'
|
||||
)
|
||||
local ns = _client_pull_namespaces[key]
|
||||
local ns = client_pull_namespaces[key]
|
||||
if not ns then
|
||||
ns = api.nvim_create_namespace(name)
|
||||
_client_pull_namespaces[key] = ns
|
||||
client_pull_namespaces[key] = ns
|
||||
end
|
||||
return ns
|
||||
end
|
||||
|
||||
local ns = _client_push_namespaces[client_id]
|
||||
local ns = client_push_namespaces[client_id]
|
||||
if not ns then
|
||||
local name = ('nvim.lsp.%s.%d'):format(client and client.name or 'unknown', client_id)
|
||||
ns = api.nvim_create_namespace(name)
|
||||
_client_push_namespaces[client_id] = ns
|
||||
client_push_namespaces[client_id] = ns
|
||||
end
|
||||
return ns
|
||||
end
|
||||
@ -257,7 +258,7 @@ end
|
||||
function M.on_diagnostic(error, result, ctx)
|
||||
if error ~= nil and error.code == protocol.ErrorCodes.ServerCancelled then
|
||||
if error.data == nil or error.data.retriggerRequest ~= false then
|
||||
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
|
||||
local client = assert(lsp.get_client_by_id(ctx.client_id))
|
||||
client:request(ctx.method, ctx.params)
|
||||
end
|
||||
return
|
||||
@ -271,7 +272,7 @@ function M.on_diagnostic(error, result, ctx)
|
||||
handle_diagnostics(ctx.params.textDocument.uri, client_id, result.items, true)
|
||||
|
||||
local bufnr = assert(ctx.bufnr)
|
||||
local bufstate = assert(bufstates[bufnr])
|
||||
local bufstate = bufstates[bufnr]
|
||||
bufstate.client_result_id[client_id] = result.resultId
|
||||
end
|
||||
|
||||
@ -329,7 +330,7 @@ end
|
||||
|
||||
--- Clear diagnostics from pull based clients
|
||||
local function clear(bufnr)
|
||||
for _, namespace in pairs(_client_pull_namespaces) do
|
||||
for _, namespace in pairs(client_pull_namespaces) do
|
||||
vim.diagnostic.reset(namespace, bufnr)
|
||||
end
|
||||
end
|
||||
@ -339,7 +340,7 @@ end
|
||||
local function disable(bufnr)
|
||||
local bufstate = bufstates[bufnr]
|
||||
if bufstate then
|
||||
bufstate.enabled = false
|
||||
bufstate.pull_kind = 'disabled'
|
||||
end
|
||||
clear(bufnr)
|
||||
end
|
||||
@ -348,7 +349,7 @@ end
|
||||
---@param bufnr integer buffer number
|
||||
---@param client_id? integer Client ID to refresh (default: all clients)
|
||||
---@param only_visible? boolean Whether to only refresh for the visible regions of the buffer (default: false)
|
||||
local function _refresh(bufnr, client_id, only_visible)
|
||||
local function refresh(bufnr, client_id, only_visible)
|
||||
if
|
||||
only_visible
|
||||
and vim.iter(api.nvim_list_wins()):all(function(window)
|
||||
@ -359,8 +360,8 @@ local function _refresh(bufnr, client_id, only_visible)
|
||||
end
|
||||
|
||||
local method = ms.textDocument_diagnostic
|
||||
local clients = vim.lsp.get_clients({ bufnr = bufnr, method = method, id = client_id })
|
||||
local bufstate = assert(bufstates[bufnr])
|
||||
local clients = lsp.get_clients({ bufnr = bufnr, method = method, id = client_id })
|
||||
local bufstate = bufstates[bufnr]
|
||||
|
||||
util._cancel_requests({
|
||||
bufnr = bufnr,
|
||||
@ -383,54 +384,130 @@ end
|
||||
function M._enable(bufnr)
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
|
||||
if not bufstates[bufnr] then
|
||||
bufstates[bufnr] = { enabled = true, client_result_id = {} }
|
||||
|
||||
api.nvim_create_autocmd('LspNotify', {
|
||||
buffer = bufnr,
|
||||
callback = function(opts)
|
||||
if
|
||||
opts.data.method ~= ms.textDocument_didChange
|
||||
and opts.data.method ~= ms.textDocument_didOpen
|
||||
then
|
||||
return
|
||||
end
|
||||
if bufstates[bufnr] and bufstates[bufnr].enabled then
|
||||
local client_id = opts.data.client_id --- @type integer?
|
||||
_refresh(bufnr, client_id, true)
|
||||
end
|
||||
end,
|
||||
group = augroup,
|
||||
})
|
||||
|
||||
api.nvim_buf_attach(bufnr, false, {
|
||||
on_reload = function()
|
||||
if bufstates[bufnr] and bufstates[bufnr].enabled then
|
||||
_refresh(bufnr)
|
||||
end
|
||||
end,
|
||||
on_detach = function()
|
||||
disable(bufnr)
|
||||
end,
|
||||
})
|
||||
|
||||
api.nvim_create_autocmd('LspDetach', {
|
||||
buffer = bufnr,
|
||||
callback = function(args)
|
||||
local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_diagnostic })
|
||||
|
||||
if
|
||||
not vim.iter(clients):any(function(c)
|
||||
return c.id ~= args.data.client_id
|
||||
end)
|
||||
then
|
||||
disable(bufnr)
|
||||
end
|
||||
end,
|
||||
group = augroup,
|
||||
})
|
||||
if bufstates[bufnr] then
|
||||
-- If we're already pulling diagnostics for this buffer, nothing to do here.
|
||||
if bufstates[bufnr].pull_kind == 'document' then
|
||||
return
|
||||
end
|
||||
-- Else diagnostics were disabled or we were using workspace diagnostics.
|
||||
bufstates[bufnr].pull_kind = 'document'
|
||||
else
|
||||
bufstates[bufnr].enabled = true
|
||||
bufstates[bufnr] = { pull_kind = 'document', client_result_id = {} }
|
||||
end
|
||||
|
||||
api.nvim_create_autocmd('LspNotify', {
|
||||
buffer = bufnr,
|
||||
callback = function(opts)
|
||||
if
|
||||
opts.data.method ~= ms.textDocument_didChange
|
||||
and opts.data.method ~= ms.textDocument_didOpen
|
||||
then
|
||||
return
|
||||
end
|
||||
if bufstates[bufnr] and bufstates[bufnr].pull_kind == 'document' then
|
||||
local client_id = opts.data.client_id --- @type integer?
|
||||
refresh(bufnr, client_id, true)
|
||||
end
|
||||
end,
|
||||
group = augroup,
|
||||
})
|
||||
|
||||
api.nvim_buf_attach(bufnr, false, {
|
||||
on_reload = function()
|
||||
if bufstates[bufnr] and bufstates[bufnr].pull_kind == 'document' then
|
||||
refresh(bufnr)
|
||||
end
|
||||
end,
|
||||
on_detach = function()
|
||||
disable(bufnr)
|
||||
end,
|
||||
})
|
||||
|
||||
api.nvim_create_autocmd('LspDetach', {
|
||||
buffer = bufnr,
|
||||
callback = function(args)
|
||||
local clients = lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_diagnostic })
|
||||
|
||||
if
|
||||
not vim.iter(clients):any(function(c)
|
||||
return c.id ~= args.data.client_id
|
||||
end)
|
||||
then
|
||||
disable(bufnr)
|
||||
end
|
||||
end,
|
||||
group = augroup,
|
||||
})
|
||||
end
|
||||
|
||||
--- Returns the result IDs from the reports provided by the given client.
|
||||
--- @return lsp.PreviousResultId[]
|
||||
local function previous_result_ids(client_id)
|
||||
local results = {}
|
||||
|
||||
for bufnr, state in pairs(bufstates) do
|
||||
if state.pull_kind ~= 'disabled' then
|
||||
for buf_client_id, result_id in pairs(state.client_result_id) do
|
||||
if buf_client_id == client_id then
|
||||
table.insert(results, {
|
||||
textDocument = util.make_text_document_params(bufnr),
|
||||
previousResultId = result_id,
|
||||
})
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
--- Request workspace-wide diagnostics.
|
||||
--- @param opts vim.lsp.WorkspaceDiagnosticsOpts
|
||||
function M._workspace_diagnostics(opts)
|
||||
local clients = lsp.get_clients({ method = ms.workspace_diagnostic, id = opts.client_id })
|
||||
|
||||
--- @param error lsp.ResponseError?
|
||||
--- @param result lsp.WorkspaceDiagnosticReport
|
||||
--- @param ctx lsp.HandlerContext
|
||||
local function handler(error, result, ctx)
|
||||
-- Check for retrigger requests on cancellation errors.
|
||||
-- Unless `retriggerRequest` is explicitly disabled, try again.
|
||||
if error ~= nil and error.code == lsp.protocol.ErrorCodes.ServerCancelled then
|
||||
if error.data == nil or error.data.retriggerRequest ~= false then
|
||||
local client = assert(lsp.get_client_by_id(ctx.client_id))
|
||||
client:request(ms.workspace_diagnostic, ctx.params, handler)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if error == nil and result ~= nil then
|
||||
for _, report in ipairs(result.items) do
|
||||
local bufnr = vim.uri_to_bufnr(report.uri)
|
||||
|
||||
-- Start tracking the buffer (but don't send "textDocument/diagnostic" requests for it).
|
||||
if not bufstates[bufnr] then
|
||||
bufstates[bufnr] = { pull_kind = 'workspace', client_result_id = {} }
|
||||
end
|
||||
|
||||
-- We favor document pull requests over workspace results, so only update the buffer
|
||||
-- state if we're not pulling document diagnostics for this buffer.
|
||||
if bufstates[bufnr].pull_kind == 'workspace' and report.kind == 'full' then
|
||||
handle_diagnostics(report.uri, ctx.client_id, report.items, true)
|
||||
bufstates[bufnr].client_result_id[ctx.client_id] = report.resultId
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for _, client in ipairs(clients) do
|
||||
--- @type lsp.WorkspaceDiagnosticParams
|
||||
local params = {
|
||||
identifier = vim.tbl_get(client, 'server_capabilities, diagnosticProvider', 'identifier'),
|
||||
previousResultIds = previous_result_ids(client.id),
|
||||
}
|
||||
|
||||
client:request(ms.workspace_diagnostic, params, handler)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -566,6 +566,9 @@ function protocol.make_client_capabilities()
|
||||
inlayHint = {
|
||||
refreshSupport = true,
|
||||
},
|
||||
workspace = {
|
||||
refreshSupport = false,
|
||||
},
|
||||
},
|
||||
experimental = nil,
|
||||
window = {
|
||||
|
@ -6801,4 +6801,128 @@ describe('LSP', function()
|
||||
eq(false, exec_lua([[return vim.lsp.is_enabled('foo')]]))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('vim.lsp.buf.workspace_diagnostics()', function()
|
||||
local fake_uri = 'file:///fake/uri'
|
||||
|
||||
--- @param kind lsp.DocumentDiagnosticReportKind
|
||||
--- @param msg string
|
||||
--- @param pos integer
|
||||
--- @return lsp.WorkspaceDocumentDiagnosticReport
|
||||
local function make_report(kind, msg, pos)
|
||||
return {
|
||||
kind = kind,
|
||||
uri = fake_uri,
|
||||
items = {
|
||||
{
|
||||
range = {
|
||||
start = { line = pos, character = pos },
|
||||
['end'] = { line = pos, character = pos },
|
||||
},
|
||||
message = msg,
|
||||
severity = 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
--- @param items lsp.WorkspaceDocumentDiagnosticReport[]
|
||||
--- @return integer
|
||||
local function setup_server(items)
|
||||
exec_lua(create_server_definition)
|
||||
return exec_lua(function()
|
||||
_G.server = _G._create_server({
|
||||
capabilities = {
|
||||
diagnosticProvider = { workspaceDiagnostics = true },
|
||||
},
|
||||
handlers = {
|
||||
['workspace/diagnostic'] = function(_, _, callback)
|
||||
callback(nil, { items = items })
|
||||
end,
|
||||
},
|
||||
})
|
||||
local client_id = assert(vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }))
|
||||
vim.lsp.buf.workspace_diagnostics()
|
||||
return client_id
|
||||
end, { items })
|
||||
end
|
||||
|
||||
it('updates diagnostics obtained with vim.diagnostic.get()', function()
|
||||
setup_server({ make_report('full', 'Error here', 1) })
|
||||
|
||||
retry(nil, nil, function()
|
||||
eq(
|
||||
1,
|
||||
exec_lua(function()
|
||||
return #vim.diagnostic.get()
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
eq(
|
||||
'Error here',
|
||||
exec_lua(function()
|
||||
return vim.diagnostic.get()[1].message
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it('ignores unchanged diagnostic reports', function()
|
||||
setup_server({ make_report('unchanged', '', 1) })
|
||||
|
||||
eq(
|
||||
0,
|
||||
exec_lua(function()
|
||||
-- Wait for diagnostics to be processed.
|
||||
vim.uv.sleep(50)
|
||||
|
||||
return #vim.diagnostic.get()
|
||||
end)
|
||||
)
|
||||
end)
|
||||
|
||||
it('favors document diagnostics over workspace diagnostics', function()
|
||||
local client_id = setup_server({ make_report('full', 'Workspace error', 1) })
|
||||
local diagnostic_bufnr = exec_lua(function()
|
||||
return vim.uri_to_bufnr(fake_uri)
|
||||
end)
|
||||
|
||||
exec_lua(function()
|
||||
vim.lsp.diagnostic.on_diagnostic(nil, {
|
||||
kind = 'full',
|
||||
items = {
|
||||
{
|
||||
range = {
|
||||
start = { line = 2, character = 2 },
|
||||
['end'] = { line = 2, character = 2 },
|
||||
},
|
||||
message = 'Document error',
|
||||
severity = 1,
|
||||
},
|
||||
},
|
||||
}, {
|
||||
method = 'textDocument/diagnostic',
|
||||
params = {
|
||||
textDocument = { uri = fake_uri },
|
||||
},
|
||||
client_id = client_id,
|
||||
bufnr = diagnostic_bufnr,
|
||||
})
|
||||
end)
|
||||
|
||||
eq(
|
||||
1,
|
||||
exec_lua(function()
|
||||
return #vim.diagnostic.get(diagnostic_bufnr)
|
||||
end)
|
||||
)
|
||||
|
||||
eq(
|
||||
'Document error',
|
||||
exec_lua(function()
|
||||
return vim.diagnostic.get(vim.uri_to_bufnr(fake_uri))[1].message
|
||||
end)
|
||||
)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
Reference in New Issue
Block a user