diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua index ed66d023ae..f29677fde3 100644 --- a/runtime/lua/vim/lsp/codelens.lua +++ b/runtime/lua/vim/lsp/codelens.lua @@ -186,20 +186,26 @@ function Provider:resolve(client, unresolved_lens) end local row = unresolved_lens.range.start.line - local lenses = assert(state.row_lenses[row]) - for i, lens in ipairs(lenses) do - if lens == unresolved_lens then - lenses[i] = resolved_lens - end + local lenses = state.row_lenses[row] + -- A newer textDocument/codeLens response can replace row_lenses while resolve is in flight. + if not lenses then + return end - self.row_version[row] = nil - api.nvim__redraw({ - buf = self.bufnr, - range = { row, row + 1 }, - valid = true, - flush = false, - }) + for i, lens in ipairs(lenses) do + -- Only apply if this exact unresolved lens still exists; otherwise response is stale. + if lens == unresolved_lens then + lenses[i] = resolved_lens + self.row_version[row] = nil + api.nvim__redraw({ + buf = self.bufnr, + range = { row, row + 1 }, + valid = true, + flush = false, + }) + return + end + end end, self.bufnr) end diff --git a/test/functional/plugin/lsp/codelens_spec.lua b/test/functional/plugin/lsp/codelens_spec.lua index a06aff5246..5c46452b30 100644 --- a/test/functional/plugin/lsp/codelens_spec.lua +++ b/test/functional/plugin/lsp/codelens_spec.lua @@ -305,6 +305,83 @@ describe('vim.lsp.codelens', function() ]]) end) + it('ignores stale codeLens/resolve responses', function() + clear_notrace() + exec_lua(create_server_definition) + + insert('line1\nline2\n') + + exec_lua(function() + local codelens_request_count = 0 + _G.stale_resolve_sent = false + _G.server = _G._create_server({ + capabilities = { + codeLensProvider = { + resolveProvider = true, + }, + }, + handlers = { + ['textDocument/codeLens'] = function(_, _, callback) + codelens_request_count = codelens_request_count + 1 + if codelens_request_count == 1 then + callback(nil, { + { + range = { + ['end'] = { + character = 1, + line = 0, + }, + start = { + character = 0, + line = 0, + }, + }, + }, + }) + else + callback(nil, {}) + end + end, + ['codeLens/resolve'] = function(_, lens, callback) + vim.defer_fn(function() + _G.stale_resolve_sent = true + callback(nil, { + command = { + arguments = {}, + command = 'dummy.command', + title = 'resolved', + }, + range = lens.range, + }) + end, 100) + end, + }, + }) + + local stale_client_id = vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + vim.lsp.codelens.enable() + vim.wait(1000, function() + return #vim.lsp.codelens.get() > 0 + end) + + vim.api.nvim__redraw({ flush = true }) + + vim.lsp.codelens.on_refresh(nil, nil, { + method = 'workspace/codeLens/refresh', + client_id = stale_client_id, + }) + + assert( + vim.wait(1000, function() + return _G.stale_resolve_sent + end), + 'timed out waiting for stale resolve response' + ) + end) + + eq('', api.nvim_get_vvar('errmsg')) + end) + it('clears extmarks beyond the bottom of the buffer', function() feed('13G4dd') screen:expect([[