refactor(lsp): always fetch lenses again in codelens.run (#37720)

The auto-refresh has a bit of a delay so it can happen that when a user
runs `codelens.run` it operates on an outdated state and either
does nothing, or fails.

This changes the logic for `.run` to always fetch the current lenses
before (optional) prompt and execution.

See discussion in https://github.com/neovim/neovim/pull/37689#discussion_r2764235931

This could potentially be optimized to first check if there's local
state with a version that matches the current buf-version, but in my
testing re-fetching them always was quickly enough that `run` still
feels instant and doing it this way simplifies the logic.

Side effect of the change is that `.run` also works if codelens aren't
enabled - for power users who know what the codelens would show that can
be useful.
This commit is contained in:
Mathias Fußenegger
2026-02-06 13:31:44 +01:00
committed by GitHub
parent 6dd0a7d60a
commit c4f322b769
2 changed files with 67 additions and 44 deletions

View File

@@ -1950,7 +1950,7 @@ is_enabled({filter}) *vim.lsp.codelens.is_enabled()*
(`boolean`) whether code lens is enabled.
run({opts}) *vim.lsp.codelens.run()*
Run code lens above the current cursor position.
Run code lens at the current cursor position.
Parameters: ~
• {opts} (`table?`) Optional parameters |kwargs|:

View File

@@ -347,6 +347,65 @@ function M.get(filter)
return result
end
---@param lnum integer
---@param opts vim.lsp.codelens.run.Opts
---@param results table<integer, {err: lsp.ResponseError?, result: lsp.CodeLens[]?}>
---@param context lsp.HandlerContext
local function on_lenses_run(lnum, opts, results, context)
local bufnr = context.bufnr or 0
---@type {client: vim.lsp.Client, lens: lsp.CodeLens}[]
local candidates = {}
local pending_resolve = 1
local function on_resolved()
pending_resolve = pending_resolve - 1
if pending_resolve > 0 then
return
end
if #candidates == 0 then
vim.notify('No codelens at current line')
elseif #candidates == 1 then
local candidate = candidates[1]
candidate.client:exec_cmd(candidate.lens.command, { bufnr = bufnr })
else
local selectopts = {
prompt = 'Code lenses: ',
kind = 'codelens',
---@param candidate {client: vim.lsp.Client, lens: lsp.CodeLens}
format_item = function(candidate)
return string.format('%s [%s]', candidate.lens.command.title, candidate.client.name)
end,
}
vim.ui.select(candidates, selectopts, function(candidate)
if candidate then
candidate.client:exec_cmd(candidate.lens.command, { bufnr = bufnr })
end
end)
end
end
for client_id, result in pairs(results) do
if opts.client_id == nil or opts.client_id == client_id then
local client = assert(vim.lsp.get_client_by_id(client_id))
for _, lens in ipairs(result.result or {}) do
if lens.range.start.line == lnum then
if lens.command then
table.insert(candidates, { client = client, lens = lens })
else
pending_resolve = pending_resolve + 1
client:request('codeLens/resolve', lens, function(_, resolved_lens)
if resolved_lens then
table.insert(candidates, { client = client, lens = resolved_lens })
end
on_resolved()
end, bufnr)
end
end
end
end
end
on_resolved()
end
--- Optional parameters |kwargs|:
---@class vim.lsp.codelens.run.Opts
---@inlinedoc
@@ -355,7 +414,7 @@ end
--- (default: all)
---@field client_id? integer
--- Run code lens above the current cursor position.
--- Run code lens at the current cursor position.
---
---@param opts? vim.lsp.codelens.run.Opts
function M.run(opts)
@@ -365,48 +424,12 @@ function M.run(opts)
local winid = api.nvim_get_current_win()
local bufnr = api.nvim_win_get_buf(winid)
local pos = vim.pos.cursor(api.nvim_win_get_cursor(winid))
local provider = Provider.active[bufnr]
if not provider then
return
end
---@type [integer, lsp.CodeLens][]
local items = {}
for client_id, state in pairs(provider.client_state) do
if not opts.client_id or opts.client_id == client_id then
for _, lens in ipairs(state.row_lenses[pos.row] or {}) do
-- Ignore unresolved and empty command lenses.
if lens.command and lens.command.command ~= '' then
table.insert(items, { client_id, lens })
end
end
end
end
if #items == 0 then
vim.notify('No code lens avaliable')
return
elseif #items == 1 then
local client_id, lens = unpack(items[1])
local client = assert(vim.lsp.get_client_by_id(client_id))
client:exec_cmd(lens.command)
else
vim.ui.select(items, {
prompt = 'Code Lens',
---@param item [integer, lsp.CodeLens]
format_item = function(item)
local client_id, lens = unpack(item)
local client = assert(vim.lsp.get_client_by_id(client_id))
return ('%s [%s]'):format(lens.command.title, client.name)
end,
}, function(item)
if item then
local client_id, lens = unpack(item)
local client = assert(vim.lsp.get_client_by_id(client_id))
client:exec_cmd(lens.command)
end
end)
end
local params = {
textDocument = vim.lsp.util.make_text_document_params(bufnr),
}
vim.lsp.buf_request_all(bufnr, 'textDocument/codeLens', params, function(results, context)
on_lenses_run(pos.row, opts, results, context)
end)
end
--- |lsp-handler| for the method `workspace/codeLens/refresh`