diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 7bb3603f6a..ff3bd8440b 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -2074,14 +2074,15 @@ from({diagnostics}) *vim.lsp.diagnostic.from()* (`lsp.Diagnostic[]`) *vim.lsp.diagnostic.get_namespace()* -get_namespace({client_id}, {is_pull}) +get_namespace({client_id}, {pull_id}) Get the diagnostic namespace associated with an LSP client |vim.diagnostic| for diagnostics Parameters: ~ • {client_id} (`integer`) The id of the LSP client - • {is_pull} (`boolean?`) Whether the namespace is for a pull or push - client. Defaults to push + • {pull_id} (`(boolean|string)?`) (default: nil) Pull diagnostics + provider id (indicates "pull" client), or `nil` for a + "push" client. *vim.lsp.diagnostic.on_diagnostic()* on_diagnostic({error}, {result}, {ctx}) diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index b4159a0ac7..904ef611c0 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -187,14 +187,25 @@ local client_pull_namespaces = {} --- Get the diagnostic namespace associated with an LSP client |vim.diagnostic| for diagnostics --- ---@param client_id integer The id of the LSP client ----@param is_pull boolean? Whether the namespace is for a pull or push client. Defaults to push -function M.get_namespace(client_id, is_pull) +---@param pull_id (boolean|string)? (default: nil) Pull diagnostics provider id +--- (indicates "pull" client), or `nil` for a "push" client. +function M.get_namespace(client_id, pull_id) vim.validate('client_id', client_id, 'number') + vim.validate('pull_id', pull_id, { 'boolean', 'string' }, true) + + if type(pull_id) == 'boolean' then + vim.deprecate('get_namespace(pull_id:boolean)', 'get_namespace(pull_id:string)', '0.14') + end local client = lsp.get_client_by_id(client_id) - if is_pull then - local key = ('%d'):format(client_id) - local name = ('nvim.lsp.%s.%d'):format(client and client.name or 'unknown', client_id) + if pull_id then + local provider_id = type(pull_id) == 'string' and pull_id or 'nil' + local key = ('%d:%s'):format(client_id, provider_id) + local name = ('nvim.lsp.%s.%d.%s'):format( + client and client.name or 'unknown', + client_id, + provider_id + ) local ns = client_pull_namespaces[key] if not ns then ns = api.nvim_create_namespace(name) @@ -215,8 +226,8 @@ end --- @param uri string --- @param client_id? integer --- @param diagnostics lsp.Diagnostic[] ---- @param is_pull boolean -local function handle_diagnostics(uri, client_id, diagnostics, is_pull) +--- @param pull_id boolean|string +local function handle_diagnostics(uri, client_id, diagnostics, pull_id) local fname = vim.uri_to_fname(uri) if #diagnostics == 0 and vim.fn.bufexists(fname) == 0 then @@ -230,7 +241,7 @@ local function handle_diagnostics(uri, client_id, diagnostics, is_pull) client_id = client_id or DEFAULT_CLIENT_ID - local namespace = M.get_namespace(client_id, is_pull) + local namespace = M.get_namespace(client_id, pull_id) vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) end @@ -276,11 +287,13 @@ function M.on_diagnostic(error, result, ctx) return end - handle_diagnostics(ctx.params.textDocument.uri, client_id, result.items, true) + ---@type lsp.DocumentDiagnosticParams + local params = ctx.params + handle_diagnostics(params.textDocument.uri, client_id, result.items, params.identifier or true) for uri, related_result in pairs(result.relatedDocuments or {}) do if related_result.kind == 'full' then - handle_diagnostics(uri, client_id, related_result.items, true) + handle_diagnostics(uri, client_id, related_result.items, params.identifier or true) end local related_bufnr = vim.uri_to_bufnr(uri) @@ -504,6 +517,8 @@ function M._workspace_diagnostics(opts) end if error == nil and result ~= nil then + ---@type lsp.WorkspaceDiagnosticParams + local params = ctx.params for _, report in ipairs(result.items) do local bufnr = vim.uri_to_bufnr(report.uri) @@ -515,7 +530,7 @@ function M._workspace_diagnostics(opts) -- 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) + handle_diagnostics(report.uri, ctx.client_id, report.items, params.identifier or true) bufstates[bufnr].client_result_id[ctx.client_id] = report.resultId end end diff --git a/test/functional/plugin/lsp/diagnostic_spec.lua b/test/functional/plugin/lsp/diagnostic_spec.lua index 6952f9fdb1..12726bc2c6 100644 --- a/test/functional/plugin/lsp/diagnostic_spec.lua +++ b/test/functional/plugin/lsp/diagnostic_spec.lua @@ -351,6 +351,89 @@ describe('vim.lsp.diagnostic', function() eq('Pull Diagnostic', diags[1].message) end) + it('preserves push diagnostics when pull diagnostics are empty', function() + local push_ns_count, pull_ns_count, all_diags_count, push_ns, pull_ns = exec_lua(function() + vim.lsp.diagnostic.on_publish_diagnostics(nil, { + uri = fake_uri, + diagnostics = { + _G.make_error('Push Diagnostic', 0, 0, 0, 0), + }, + }, { client_id = client_id }) + + vim.lsp.diagnostic.on_diagnostic(nil, { + kind = 'full', + items = {}, + }, { + params = { + textDocument = { uri = fake_uri }, + }, + uri = fake_uri, + client_id = client_id, + bufnr = diagnostic_bufnr, + }, {}) + + local push_ns = vim.lsp.diagnostic.get_namespace(client_id, false) + local pull_ns = vim.lsp.diagnostic.get_namespace(client_id, true) + + return #vim.diagnostic.get(diagnostic_bufnr, { namespace = push_ns }), + #vim.diagnostic.get(diagnostic_bufnr, { namespace = pull_ns }), + #vim.diagnostic.get(diagnostic_bufnr), + push_ns, + pull_ns + end) + + eq(1, push_ns_count) + eq(0, pull_ns_count) + eq(1, all_diags_count) + neq(push_ns, pull_ns) + end) + + it('uses pull_id to isolate pull diagnostic namespaces', function() + local first_count, second_count, total_count, first_ns, second_ns = exec_lua(function() + vim.lsp.diagnostic.on_diagnostic(nil, { + kind = 'full', + items = { + _G.make_error('Pull Diagnostic A', 0, 0, 0, 0), + }, + }, { + params = { + identifier = 'provider-a', + textDocument = { uri = fake_uri }, + }, + uri = fake_uri, + client_id = client_id, + bufnr = diagnostic_bufnr, + }, {}) + + vim.lsp.diagnostic.on_diagnostic(nil, { + kind = 'full', + items = {}, + }, { + params = { + identifier = 'provider-b', + textDocument = { uri = fake_uri }, + }, + uri = fake_uri, + client_id = client_id, + bufnr = diagnostic_bufnr, + }, {}) + + local first_ns = vim.lsp.diagnostic.get_namespace(client_id, 'provider-a') + local second_ns = vim.lsp.diagnostic.get_namespace(client_id, 'provider-b') + + return #vim.diagnostic.get(diagnostic_bufnr, { namespace = first_ns }), + #vim.diagnostic.get(diagnostic_bufnr, { namespace = second_ns }), + #vim.diagnostic.get(diagnostic_bufnr), + first_ns, + second_ns + end) + + eq(1, first_count) + eq(0, second_count) + eq(1, total_count) + neq(first_ns, second_ns) + end) + it('handles multiline diagnostic ranges #33782', function() local diags = exec_lua(function() vim.lsp.diagnostic.on_diagnostic(nil, {