mirror of
https://github.com/neovim/neovim.git
synced 2026-02-10 05:48:45 +00:00
Problem: Code lenses currently display as virtual text on the same line and after the relevant item. While the spec does not say how lenses should be rendered, above the line is most typical. For longer lines, lenses rendered as virtual text can run off the side of the screen. Solution: Display lenses as virtual lines above the text. Closes https://github.com/neovim/neovim/issues/33923 Co-authored-by: Yi Ming <ofseed@foxmail.com>
533 lines
14 KiB
Lua
533 lines
14 KiB
Lua
local util = require('vim.lsp.util')
|
|
local log = require('vim.lsp.log')
|
|
local api = vim.api
|
|
local M = {}
|
|
|
|
local Capability = require('vim.lsp._capability')
|
|
|
|
---@class (private) vim.lsp.codelens.ClientState
|
|
---@field row_lenses table<integer, lsp.CodeLens[]?> row -> lens
|
|
---@field namespace integer
|
|
|
|
---@class (private) vim.lsp.codelens.Provider : vim.lsp.Capability
|
|
---@field active table<integer, vim.lsp.codelens.Provider?>
|
|
---
|
|
--- `TextDocument` version current state corresponds to.
|
|
---@field version? integer
|
|
---
|
|
--- Last version of codelens applied to this line.
|
|
---
|
|
--- Index In the form of row -> true?
|
|
---@field row_version table<integer, integer?>
|
|
---
|
|
--- Index In the form of client_id -> client_state
|
|
---@field client_state? table<integer, vim.lsp.codelens.ClientState?>
|
|
---
|
|
--- Timer for debouncing automatic requests.
|
|
---
|
|
---@field timer? uv.uv_timer_t
|
|
local Provider = {
|
|
name = 'codelens',
|
|
method = 'textDocument/codeLens',
|
|
active = {},
|
|
}
|
|
Provider.__index = Provider
|
|
setmetatable(Provider, Capability)
|
|
Capability.all[Provider.name] = Provider
|
|
|
|
---@package
|
|
---@param bufnr integer
|
|
---@return vim.lsp.codelens.Provider
|
|
function Provider:new(bufnr)
|
|
---@type vim.lsp.codelens.Provider
|
|
self = Capability.new(self, bufnr)
|
|
self.client_state = {}
|
|
self.row_version = {}
|
|
|
|
api.nvim_buf_attach(bufnr, false, {
|
|
on_lines = function(_, buf)
|
|
local provider = Provider.active[buf]
|
|
if not provider then
|
|
return true
|
|
end
|
|
provider:automatic_request()
|
|
end,
|
|
on_reload = function(_, buf)
|
|
local provider = Provider.active[buf]
|
|
if provider then
|
|
provider:automatic_request()
|
|
end
|
|
end,
|
|
})
|
|
|
|
return self
|
|
end
|
|
|
|
---@package
|
|
---@param client_id integer
|
|
function Provider:on_attach(client_id)
|
|
local state = self.client_state[client_id]
|
|
if not state then
|
|
state = {
|
|
namespace = api.nvim_create_namespace('nvim.lsp.codelens:' .. client_id),
|
|
row_lenses = {},
|
|
}
|
|
self.client_state[client_id] = state
|
|
end
|
|
self:request(client_id)
|
|
end
|
|
|
|
---@package
|
|
---@param client_id integer
|
|
function Provider:on_detach(client_id)
|
|
local state = self.client_state[client_id]
|
|
if state then
|
|
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
|
|
self.client_state[client_id] = nil
|
|
end
|
|
end
|
|
|
|
--- `lsp.Handler` for `textDocument/codeLens`.
|
|
---
|
|
---@package
|
|
---@param err? lsp.ResponseError
|
|
---@param result? lsp.CodeLens[]
|
|
---@param ctx lsp.HandlerContext
|
|
function Provider:handler(err, result, ctx)
|
|
local state = self.client_state[ctx.client_id]
|
|
if not state then
|
|
return
|
|
end
|
|
|
|
if err then
|
|
log.error('codelens', err)
|
|
return
|
|
end
|
|
|
|
if util.buf_versions[self.bufnr] ~= ctx.version then
|
|
return
|
|
end
|
|
|
|
---@type table<integer, lsp.CodeLens[]>
|
|
local row_lenses = {}
|
|
|
|
-- Code lenses should only span a single line.
|
|
for _, lens in ipairs(result or {}) do
|
|
local row = lens.range.start.line
|
|
local lenses = row_lenses[row] or {}
|
|
table.insert(lenses, lens)
|
|
row_lenses[row] = lenses
|
|
end
|
|
|
|
state.row_lenses = row_lenses
|
|
self.version = ctx.version
|
|
end
|
|
|
|
---@package
|
|
---@param client_id? integer
|
|
---@param on_response? function
|
|
function Provider:request(client_id, on_response)
|
|
---@type lsp.CodeLensParams
|
|
local params = { textDocument = util.make_text_document_params(self.bufnr) }
|
|
for id in pairs(self.client_state) do
|
|
if not client_id or client_id == id then
|
|
local client = assert(vim.lsp.get_client_by_id(id))
|
|
client:request('textDocument/codeLens', params, function(...)
|
|
self:handler(...)
|
|
|
|
if on_response then
|
|
on_response()
|
|
end
|
|
end, self.bufnr)
|
|
end
|
|
end
|
|
end
|
|
|
|
---@private
|
|
function Provider:reset_timer()
|
|
local timer = self.timer
|
|
if timer then
|
|
self.timer = nil
|
|
if not timer:is_closing() then
|
|
timer:stop()
|
|
timer:close()
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Automatically request with debouncing, used as callbacks in autocmd events.
|
|
---
|
|
---@package
|
|
function Provider:automatic_request()
|
|
self:reset_timer()
|
|
self.timer = vim.defer_fn(function()
|
|
self:request()
|
|
end, 200)
|
|
end
|
|
|
|
---@private
|
|
---@param client vim.lsp.Client
|
|
---@param unresolved_lens lsp.CodeLens
|
|
function Provider:resolve(client, unresolved_lens)
|
|
---@param resolved_lens lsp.CodeLens
|
|
client:request('codeLens/resolve', unresolved_lens, function(err, resolved_lens, ctx)
|
|
local state = self.client_state[client.id]
|
|
if not state then
|
|
return
|
|
end
|
|
|
|
if err then
|
|
log.error('codelens/resolve', err)
|
|
return
|
|
end
|
|
|
|
if util.buf_versions[self.bufnr] ~= ctx.version then
|
|
return
|
|
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
|
|
end
|
|
|
|
self.row_version[row] = nil
|
|
api.nvim__redraw({
|
|
buf = self.bufnr,
|
|
range = { row, row + 1 },
|
|
valid = true,
|
|
flush = false,
|
|
})
|
|
end, self.bufnr)
|
|
end
|
|
|
|
---@package
|
|
---@param toprow integer
|
|
---@param botrow integer
|
|
function Provider:on_win(toprow, botrow)
|
|
for row = toprow, botrow do
|
|
if self.row_version[row] ~= self.version then
|
|
for client_id, state in pairs(self.client_state) do
|
|
local bufnr = self.bufnr
|
|
local namespace = state.namespace
|
|
|
|
api.nvim_buf_clear_namespace(bufnr, namespace, row, row + 1)
|
|
|
|
local lenses = state.row_lenses[row]
|
|
if lenses then
|
|
table.sort(lenses, function(a, b)
|
|
return a.range.start.character < b.range.start.character
|
|
end)
|
|
|
|
---@type integer
|
|
local indent = api.nvim_buf_call(bufnr, function()
|
|
return vim.fn.indent(row + 1)
|
|
end)
|
|
|
|
---@type [string, string|integer][][]
|
|
local virt_lines = { { { string.rep(' ', indent), 'LspCodeLensSeparator' } } }
|
|
local virt_text = virt_lines[1]
|
|
for _, lens in ipairs(lenses) do
|
|
-- A code lens is unresolved when no command is associated to it.
|
|
if not lens.command then
|
|
local client = assert(vim.lsp.get_client_by_id(client_id)) ---@type vim.lsp.Client
|
|
self:resolve(client, lens)
|
|
else
|
|
virt_text[#virt_text + 1] = { lens.command.title, 'LspCodeLens' }
|
|
virt_text[#virt_text + 1] = { ' | ', 'LspCodeLensSeparator' }
|
|
end
|
|
end
|
|
|
|
if #virt_text > 1 then
|
|
-- Remove trailing separator.
|
|
virt_text[#virt_text] = nil
|
|
else
|
|
-- Use a placeholder to prevent flickering caused by layout shifts.
|
|
virt_text[#virt_text + 1] = { '...', 'LspCodeLens' }
|
|
end
|
|
|
|
api.nvim_buf_set_extmark(bufnr, namespace, row, 0, {
|
|
virt_lines = virt_lines,
|
|
virt_lines_above = true,
|
|
virt_lines_overflow = 'scroll',
|
|
hl_mode = 'combine',
|
|
})
|
|
end
|
|
self.row_version[row] = self.version
|
|
end
|
|
end
|
|
end
|
|
|
|
if botrow == api.nvim_buf_line_count(self.bufnr) - 1 then
|
|
for _, state in pairs(self.client_state) do
|
|
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, botrow, -1)
|
|
end
|
|
end
|
|
end
|
|
|
|
local namespace = api.nvim_create_namespace('nvim.lsp.codelens')
|
|
api.nvim_set_decoration_provider(namespace, {
|
|
on_win = function(_, _, bufnr, toprow, botrow)
|
|
local provider = Provider.active[bufnr]
|
|
if provider then
|
|
provider:on_win(toprow, botrow)
|
|
end
|
|
end,
|
|
})
|
|
|
|
--- Query whether code lens is enabled in the {filter}ed scope
|
|
---
|
|
---@param filter? vim.lsp.capability.enable.Filter
|
|
---@return boolean whether code lens is enabled.
|
|
function M.is_enabled(filter)
|
|
return vim.lsp._capability.is_enabled('codelens', filter)
|
|
end
|
|
|
|
--- Enables or disables code lens for the {filter}ed scope.
|
|
---
|
|
--- To "toggle", pass the inverse of `is_enabled()`:
|
|
---
|
|
--- ```lua
|
|
--- vim.lsp.codelens.enable(not vim.lsp.codelens.is_enabled())
|
|
--- ```
|
|
---
|
|
--- To run a code lens, see |vim.lsp.codelens.run()|.
|
|
---
|
|
---@param enable? boolean true/nil to enable, false to disable
|
|
---@param filter? vim.lsp.capability.enable.Filter
|
|
function M.enable(enable, filter)
|
|
vim.lsp._capability.enable('codelens', enable, filter)
|
|
end
|
|
|
|
--- Optional filters |kwargs|:
|
|
---@class vim.lsp.codelens.get.Filter
|
|
---@inlinedoc
|
|
---
|
|
--- Buffer handle, or 0 for current.
|
|
--- (default: 0)
|
|
---@field bufnr? integer
|
|
---
|
|
--- Client ID, or nil for all.
|
|
--- (default: all)
|
|
---@field client_id? integer
|
|
|
|
---@class vim.lsp.codelens.get.Result
|
|
---@inlinedoc
|
|
---@field client_id integer
|
|
---@field lens lsp.CodeLens
|
|
|
|
--- Get all code lenses in the {filter}ed scope.
|
|
---
|
|
---@param filter? vim.lsp.codelens.get.Filter
|
|
---@return vim.lsp.codelens.get.Result[]
|
|
function M.get(filter)
|
|
if type(filter) == 'number' then
|
|
vim.deprecate(
|
|
'vim.lsp.codelens.get(bufnr)',
|
|
'vim.lsp.codelens.get({ bufnr = bufnr })',
|
|
'0.13.0'
|
|
)
|
|
local bufnr = vim._resolve_bufnr(filter)
|
|
local provider = Provider.active[bufnr]
|
|
if not provider then
|
|
return {}
|
|
end
|
|
---@type lsp.CodeLens[]
|
|
local result = {}
|
|
for _, state in pairs(provider.client_state) do
|
|
for _, lenses in pairs(state.row_lenses) do
|
|
result = vim.list_extend(result, lenses)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
vim.validate('filter', filter, 'table', true)
|
|
filter = filter or {}
|
|
|
|
local bufnr = vim._resolve_bufnr(filter.bufnr)
|
|
local provider = Provider.active[bufnr]
|
|
if not provider then
|
|
return {}
|
|
end
|
|
|
|
local result = {}
|
|
for client_id, state in pairs(provider.client_state) do
|
|
if not filter.client_id or filter.client_id == client_id then
|
|
for _, lenses in pairs(state.row_lenses) do
|
|
for _, lens in ipairs(lenses) do
|
|
table.insert(result, { client_id = client_id, lens = lens })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
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
|
|
---
|
|
--- Client ID, or nil for all.
|
|
--- (default: all)
|
|
---@field client_id? integer
|
|
|
|
--- Run code lens at the current cursor position.
|
|
---
|
|
---@param opts? vim.lsp.codelens.run.Opts
|
|
function M.run(opts)
|
|
vim.validate('opts', opts, 'table', true)
|
|
opts = opts or {}
|
|
|
|
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 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`
|
|
---
|
|
---@private
|
|
---@type lsp.Handler
|
|
function M.on_refresh(err, _, ctx)
|
|
if err then
|
|
return vim.NIL
|
|
end
|
|
|
|
for bufnr, provider in pairs(Provider.active) do
|
|
for client_id in pairs(provider.client_state) do
|
|
if client_id == ctx.client_id then
|
|
provider:request(client_id, function()
|
|
provider.row_version = {}
|
|
vim.api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
|
|
end)
|
|
end
|
|
end
|
|
end
|
|
return vim.NIL
|
|
end
|
|
|
|
---@deprecated
|
|
---@param client_id? integer
|
|
---@param bufnr? integer
|
|
function M.clear(client_id, bufnr)
|
|
vim.deprecate(
|
|
'vim.lsp.codelens.clear(client_id, bufnr)',
|
|
'vim.lsp.codelens.enable(false, { bufnr = bufnr, client_id = client_id })',
|
|
'0.13.0'
|
|
)
|
|
M.enable(false, { bufnr = bufnr, client_id = client_id })
|
|
end
|
|
|
|
---@deprecated
|
|
---@param lenses? lsp.CodeLens[] lenses to display
|
|
---@param bufnr integer
|
|
---@param client_id integer
|
|
function M.display(lenses, bufnr, client_id)
|
|
vim.deprecate('vim.lsp.codelens.display()', nil, '0.13.0')
|
|
local _, _, _ = lenses, bufnr, client_id
|
|
end
|
|
|
|
---@deprecated
|
|
---@param lenses? lsp.CodeLens[] lenses to store
|
|
---@param bufnr integer
|
|
---@param client_id integer
|
|
function M.save(lenses, bufnr, client_id)
|
|
vim.deprecate('vim.lsp.codelens.save()', nil, '0.13.0')
|
|
local _, _, _ = lenses, bufnr, client_id
|
|
end
|
|
|
|
---@deprecated
|
|
---@param err? lsp.ResponseError
|
|
---@param result lsp.CodeLens[]
|
|
---@param ctx lsp.HandlerContext
|
|
function M.on_codelens(err, result, ctx)
|
|
vim.deprecate('vim.lsp.codelens.on_codelens()', nil, '0.13.0')
|
|
local _, _, _ = err, result, ctx
|
|
end
|
|
|
|
---@class vim.lsp.codelens.refresh.Opts
|
|
---@inlinedoc
|
|
---@field bufnr? integer
|
|
|
|
---@deprecated
|
|
---@param opts? vim.lsp.codelens.refresh.Opts Optional fields
|
|
function M.refresh(opts)
|
|
vim.deprecate(
|
|
'vim.lsp.codelens.refresh({ bufnr = bufnr})',
|
|
'vim.lsp.codelens.enable(true, { bufnr = bufnr })',
|
|
'0.13.0'
|
|
)
|
|
|
|
vim.validate('opts', opts, 'table', true)
|
|
M.enable(true, { bufnr = opts and opts.bufnr })
|
|
end
|
|
|
|
return M
|