commit c4f322b769ea42829180d0be32d68014efd159cc
parent 6dd0a7d60a28274399928e960a0a520ab2b86bcd
Author: Mathias Fußenegger <mfussenegger@users.noreply.github.com>
Date: Fri, 6 Feb 2026 13:31:44 +0100
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.
Diffstat:
2 files changed, 67 insertions(+), 44 deletions(-)
diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt
@@ -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|:
diff --git a/runtime/lua/vim/lsp/codelens.lua b/runtime/lua/vim/lsp/codelens.lua
@@ -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`