mirror of
https://github.com/neovim/neovim.git
synced 2026-07-23 17:32:51 +00:00
Problem: Every LSP capability that sends a request on a document change to update its state was using its own buffer-local autocmd to do so. That means there is a separate autocmd per buffer per active capability/feature, and the actual code inside the autocmd callback was virtually identical. The same problem occurred for capabilities that had components to draw on the screen that had their own decoration providers. Solution: Introduce a central `LspNotify` autocmd and decoration provider in the capability module itself. New base class methods `on_close` and `on_change` that take a client_id have been provided, for `didClose` and `didOpen`/`didChange` notifications respectively. New base class method `on_win` that takes topline and botline has been provided to add extmarks or perform other work when lines of a buffer are being drawn. The autocmd callback loops through each active capability instance for the buffer and calls a corresponding method with the triggering client_id if it is currently attached to the buffer that triggered the autocmd. The decoration provider just calls on_win for all active capabilities on the buffer. This slightly tweaks folding range to make use of these new client-id specific callbacks.
505 lines
14 KiB
Lua
505 lines
14 KiB
Lua
local api = vim.api
|
|
local log = require('vim.lsp.log')
|
|
local tableclear = require('vim._core.table').clear
|
|
local util = require('vim.lsp.util')
|
|
|
|
local M = {}
|
|
|
|
local Capability = require('vim.lsp._capability')
|
|
|
|
---@class (private) vim.lsp.codelens.RowLenses
|
|
---@field lenses lsp.CodeLens[]
|
|
---@field version? integer `TextDocument` version most recently applied to this row.
|
|
---
|
|
---@class (private) vim.lsp.codelens.ClientState
|
|
---@field row_lenses table<integer, vim.lsp.codelens.RowLenses>
|
|
---@field namespace integer
|
|
---@field version? integer `TextDocument` version current state corresponds to.
|
|
---
|
|
---@class (private) vim.lsp.codelens.Provider : vim.lsp.Capability
|
|
---@field active table<integer, vim.lsp.codelens.Provider>
|
|
---
|
|
--- Index In the form of client_id -> client_state
|
|
---@field client_state? table<integer, vim.lsp.codelens.ClientState?>
|
|
local Provider = {
|
|
name = 'codelens',
|
|
method = 'textDocument/codeLens',
|
|
active = {},
|
|
}
|
|
Provider.__index = Provider
|
|
setmetatable(Provider, Capability)
|
|
Capability.all[Provider.name] = Provider
|
|
|
|
---@package
|
|
---@param client_id integer
|
|
function Provider:on_attach(client_id)
|
|
if not self.client_state[client_id] then
|
|
self.client_state[client_id] = {
|
|
namespace = api.nvim_create_namespace('nvim.lsp.codelens:' .. client_id),
|
|
row_lenses = {},
|
|
}
|
|
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
|
|
self:clear(client_id)
|
|
self.client_state[client_id] = nil
|
|
end
|
|
end
|
|
|
|
---@private
|
|
function Provider:on_close(client_id)
|
|
self:clear(client_id)
|
|
end
|
|
|
|
---@private
|
|
function Provider:on_change(client_id)
|
|
self:request(client_id)
|
|
end
|
|
|
|
---@package
|
|
---@param client_id integer
|
|
function Provider:clear(client_id)
|
|
local state = self.client_state[client_id]
|
|
if state then
|
|
state.version = nil
|
|
tableclear(state.row_lenses)
|
|
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
|
|
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
|
|
|
|
tableclear(state.row_lenses)
|
|
|
|
-- Code lenses should only span a single line.
|
|
for _, lens in ipairs(result or {}) do
|
|
local row = lens.range.start.line
|
|
local row_lenses = state.row_lenses[row] or { lenses = {} }
|
|
table.insert(row_lenses.lenses, lens)
|
|
state.row_lenses[row] = row_lenses
|
|
end
|
|
state.version = ctx.version
|
|
|
|
api.nvim__redraw({ buf = self.bufnr, valid = true, flush = false })
|
|
end
|
|
|
|
---@package
|
|
---@param client_id integer
|
|
function Provider:request(client_id)
|
|
---@type lsp.CodeLensParams
|
|
local params = { textDocument = util.make_text_document_params(self.bufnr) }
|
|
local state = self.client_state[client_id]
|
|
local client = vim.lsp.get_client_by_id(client_id)
|
|
if state and client then
|
|
client:request('textDocument/codeLens', params, function(...)
|
|
self:handler(...)
|
|
end, self.bufnr)
|
|
end
|
|
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 row_lenses = state.row_lenses[row]
|
|
-- A newer textDocument/codeLens response can replace row_lenses while resolve is in flight.
|
|
if not row_lenses then
|
|
return
|
|
end
|
|
|
|
for i, lens in ipairs(row_lenses.lenses) do
|
|
-- Only apply if this exact unresolved lens still exists; otherwise response is stale.
|
|
if lens == unresolved_lens then
|
|
row_lenses.lenses[i] = resolved_lens
|
|
row_lenses.version = nil
|
|
api.nvim__redraw({
|
|
buf = self.bufnr,
|
|
range = { row, row + 1 },
|
|
valid = true,
|
|
flush = false,
|
|
})
|
|
return
|
|
end
|
|
end
|
|
end, self.bufnr)
|
|
end
|
|
|
|
---@package
|
|
---@param toprow integer
|
|
---@param botrow integer
|
|
function Provider:on_win(toprow, botrow)
|
|
for row = toprow, botrow do
|
|
for client_id, state in pairs(self.client_state) do
|
|
if state.version == util.buf_versions[self.bufnr] then
|
|
local row_lenses = state.row_lenses[row]
|
|
|
|
if not row_lenses then
|
|
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, row, row + 1)
|
|
elseif row_lenses.version ~= state.version then
|
|
row_lenses.version = state.version
|
|
|
|
local bufnr = self.bufnr
|
|
local namespace = state.namespace
|
|
|
|
table.sort(row_lenses.lenses, function(a, b)
|
|
return a.range.start.character < b.range.start.character
|
|
end)
|
|
|
|
local client = assert(vim.lsp.get_client_by_id(client_id))
|
|
local range = vim.range.lsp(bufnr, row_lenses.lenses[1].range, client.offset_encoding)
|
|
---@type [string, string][]
|
|
local virt_text = {
|
|
{ string.rep(' ', range.start_col), 'LspCodeLensSeparator' },
|
|
}
|
|
local has_unresolved = false
|
|
|
|
for _, lens in ipairs(row_lenses.lenses) do
|
|
-- A code lens is unresolved when no command is associated to it.
|
|
if not lens.command then
|
|
has_unresolved = true
|
|
self:resolve(client, lens)
|
|
else
|
|
vim.list_extend(virt_text, {
|
|
{ lens.command.title, 'LspCodeLens' },
|
|
{ ' | ', 'LspCodeLensSeparator' },
|
|
})
|
|
end
|
|
end
|
|
|
|
local had_extmark = #api.nvim_buf_get_extmarks(
|
|
bufnr,
|
|
namespace,
|
|
{ row, 0 },
|
|
{ row, -1 },
|
|
{}
|
|
) > 0
|
|
|
|
if not has_unresolved or not had_extmark then
|
|
-- Remove trailing separator.
|
|
table.remove(virt_text)
|
|
|
|
-- Use a placeholder to prevent flickering caused by layout shifts.
|
|
if #virt_text == 1 then
|
|
table.insert(virt_text, { '', 'LspCodeLens' })
|
|
end
|
|
|
|
api.nvim_buf_clear_namespace(bufnr, namespace, row, row + 1)
|
|
api.nvim_buf_set_extmark(bufnr, namespace, row, 0, {
|
|
virt_lines = { virt_text },
|
|
virt_lines_above = true,
|
|
virt_lines_overflow = 'scroll',
|
|
hl_mode = 'combine',
|
|
})
|
|
|
|
-- Fix https://github.com/neovim/neovim/issues/16166
|
|
-- Make sure the code lens on the first line is visible when updating.
|
|
if row == 0 then
|
|
vim.fn.winrestview({ topfill = 1 })
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Clear extmarks beyond the bottom of the buffer.
|
|
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, -1)
|
|
end
|
|
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 _, row_lenses in pairs(state.row_lenses) do
|
|
result = vim.list_extend(result, row_lenses.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 _, row_lenses in pairs(state.row_lenses) do
|
|
for _, lens in ipairs(row_lenses.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(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 _, provider in pairs(Provider.active) do
|
|
local state = provider.client_state[ctx.client_id]
|
|
if state then
|
|
provider:request(ctx.client_id)
|
|
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
|