From 653f2092ce5545da3a10743e95aae435d0e0c182 Mon Sep 17 00:00:00 2001 From: Caleb White Date: Wed, 8 Jul 2026 06:26:22 -0500 Subject: [PATCH] fix(lsp): refresh document-pull buffers on workspace/diagnostic/refresh #40623 When a server supports both document and workspace pull diagnostics, `on_refresh` only dispatched a `workspace/diagnostic` request. The workspace response handler skips buffers with `pull_kind == "document"` (i.e. all buffers opened by the user), so their diagnostics went stale until the next `didChange` or `didOpen` event. Change `on_refresh` to always refresh document-pull buffers via `textDocument/diagnostic`, regardless of whether the server also supports workspace diagnostics. This ensures that opened buffers see updated diagnostics (e.g. after a save triggers an external tool like PHPStan) without requiring the user to re-enter insert mode. --- runtime/lua/vim/lsp/diagnostic.lua | 17 +++-- .../functional/plugin/lsp/diagnostic_spec.lua | 68 +++++++++++++++---- 2 files changed, 65 insertions(+), 20 deletions(-) diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index 60fb66999b..9c07b05165 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -405,13 +405,16 @@ function M.on_refresh(err, _, ctx) end if client:supports_method('workspace/diagnostic') then M._workspace_diagnostics({ client_id = ctx.client_id }) - else - for bufnr in pairs(client.attached_buffers or {}) do - local provider = Diagnostics.active[bufnr] - local state = provider and provider.client_state[ctx.client_id] - if state and state.pull_kind == 'document' then - provider:refresh(ctx.client_id) - end + end + + -- Always refresh document-pull buffers. Workspace diagnostics only cover + -- buffers with pull_kind == 'workspace', so open buffers must be refreshed + -- individually or their diagnostics go stale until the next didChange. + for bufnr in pairs(client.attached_buffers or {}) do + local provider = Diagnostics.active[bufnr] + local state = provider and provider.client_state[ctx.client_id] + if state and state.pull_kind == 'document' then + provider:refresh(ctx.client_id) end end diff --git a/test/functional/plugin/lsp/diagnostic_spec.lua b/test/functional/plugin/lsp/diagnostic_spec.lua index 89516f2307..5481638bf2 100644 --- a/test/functional/plugin/lsp/diagnostic_spec.lua +++ b/test/functional/plugin/lsp/diagnostic_spec.lua @@ -825,14 +825,24 @@ describe('vim.lsp.diagnostic', function() local fake_uri_3 = 'file:///fake/uri3' exec_lua(create_server_definition) exec_lua(function() - _G.requests = 0 + _G.workspace_requests = 0 + _G.doc_requests = 0 _G.server = _G._create_server({ capabilities = { diagnosticProvider = { workspaceDiagnostics = true }, }, handlers = { + ['textDocument/diagnostic'] = function(_, _, callback) + _G.doc_requests = _G.doc_requests + 1 + callback(nil, { + kind = 'full', + items = { + _G.make_warning('Refreshed Diagnostic', 4, 4, 4, 4), + }, + }) + end, ['workspace/diagnostic'] = function(_, _, callback) - _G.requests = _G.requests + 1 + _G.workspace_requests = _G.workspace_requests + 1 callback(nil, { items = { { @@ -850,8 +860,26 @@ describe('vim.lsp.diagnostic', function() client_id = assert(vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })) end) + -- Seed the opened buffer with a document diagnostic so on_refresh must + -- re-request it even when workspace diagnostics are supported. + exec_lua(function() + vim.lsp.diagnostic.on_diagnostic(nil, { + kind = 'full', + items = { + _G.make_error('Initial Diagnostic', 4, 4, 4, 4), + }, + }, { + params = { + textDocument = { uri = fake_uri }, + }, + uri = fake_uri, + client_id = client_id, + bufnr = diagnostic_bufnr, + }, {}) + end) + eq( - 0, + 1, exec_lua(function() return #vim.diagnostic.get() end) @@ -870,17 +898,31 @@ describe('vim.lsp.diagnostic', function() end) ) - local requests, diags = exec_lua(function() - vim.lsp.diagnostic.on_refresh(nil, nil, { - method = 'workspace/diagnostic/refresh', - client_id = client_id, - }) - - return _G.requests, vim.diagnostic.get() + exec_lua(function() + _G.doc_requests = 0 end) - eq(1, requests) - eq(1, #diags) - eq(1, diags[1].severity) + + local workspace_requests, doc_requests, diags, opened_diags, workspace_diags = exec_lua( + function() + vim.lsp.diagnostic.on_refresh(nil, nil, { + method = 'workspace/diagnostic/refresh', + client_id = client_id, + }) + + return _G.workspace_requests, + _G.doc_requests, + vim.diagnostic.get(), + vim.diagnostic.get(diagnostic_bufnr), + vim.diagnostic.get(vim.uri_to_bufnr(fake_uri_3)) + end + ) + eq(1, workspace_requests) + eq(1, doc_requests) + eq(2, #diags) + eq(1, #opened_diags) + eq(2, opened_diags[1].severity) + eq(1, #workspace_diags) + eq(1, workspace_diags[1].severity) end) end) end)