mirror of
https://github.com/neovim/neovim.git
synced 2026-04-21 06:45:37 +00:00
feat(lsp)!: reimplement textDocument/codeLens as decoration provider
This commit is contained in:
@@ -42,6 +42,11 @@ LSP
|
|||||||
• *vim.lsp.get_buffers_by_client_id()* Use `vim.lsp.get_client_by_id(id).attached_buffers`
|
• *vim.lsp.get_buffers_by_client_id()* Use `vim.lsp.get_client_by_id(id).attached_buffers`
|
||||||
instead
|
instead
|
||||||
• *vim.lsp.stop_client()* Use |Client:stop()| instead
|
• *vim.lsp.stop_client()* Use |Client:stop()| instead
|
||||||
|
• *vim.lsp.codelens.refresh()* Use `vim.lsp.codelens.enable(true)` instead
|
||||||
|
• *vim.lsp.codelens.clear()* Use `vim.lsp.codelens.enable(false)` instead
|
||||||
|
• *vim.lsp.codelens.display()*
|
||||||
|
• *vim.lsp.codelens.save()*
|
||||||
|
• *vim.lsp.codelens.on_codelens()*
|
||||||
|
|
||||||
LUA
|
LUA
|
||||||
|
|
||||||
|
|||||||
@@ -1904,64 +1904,58 @@ Client:supports_method({method}, {bufnr}) *Client:supports_method()*
|
|||||||
==============================================================================
|
==============================================================================
|
||||||
Lua module: vim.lsp.codelens *lsp-codelens*
|
Lua module: vim.lsp.codelens *lsp-codelens*
|
||||||
|
|
||||||
clear({client_id}, {bufnr}) *vim.lsp.codelens.clear()*
|
enable({enable}, {filter}) *vim.lsp.codelens.enable()*
|
||||||
Clear the lenses
|
Enables or disables code lens for the {filter}ed scope.
|
||||||
|
|
||||||
Parameters: ~
|
To "toggle", pass the inverse of `is_enabled()`: >lua
|
||||||
• {client_id} (`integer?`) filter by client_id. All clients if nil
|
vim.lsp.codelens.enable(not vim.lsp.codelens.is_enabled())
|
||||||
• {bufnr} (`integer?`) filter by buffer. All buffers if nil, 0 for
|
|
||||||
current buffer
|
|
||||||
|
|
||||||
display({lenses}, {bufnr}, {client_id}) *vim.lsp.codelens.display()*
|
|
||||||
Display the lenses using virtual text
|
|
||||||
|
|
||||||
Parameters: ~
|
|
||||||
• {lenses} (`lsp.CodeLens[]?`) lenses to display
|
|
||||||
• {bufnr} (`integer`)
|
|
||||||
• {client_id} (`integer`)
|
|
||||||
|
|
||||||
get({bufnr}) *vim.lsp.codelens.get()*
|
|
||||||
Return all lenses for the given buffer
|
|
||||||
|
|
||||||
Parameters: ~
|
|
||||||
• {bufnr} (`integer`) Buffer number. 0 can be used for the current
|
|
||||||
buffer.
|
|
||||||
|
|
||||||
Return: ~
|
|
||||||
(`lsp.CodeLens[]`)
|
|
||||||
|
|
||||||
on_codelens({err}, {result}, {ctx}) *vim.lsp.codelens.on_codelens()*
|
|
||||||
|lsp-handler| for the method `textDocument/codeLens`
|
|
||||||
|
|
||||||
Parameters: ~
|
|
||||||
• {err} (`lsp.ResponseError?`)
|
|
||||||
• {result} (`lsp.CodeLens[]`)
|
|
||||||
• {ctx} (`lsp.HandlerContext`)
|
|
||||||
|
|
||||||
refresh({opts}) *vim.lsp.codelens.refresh()*
|
|
||||||
Refresh the lenses.
|
|
||||||
|
|
||||||
It is recommended to trigger this using an autocmd or via keymap.
|
|
||||||
|
|
||||||
Example: >vim
|
|
||||||
autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh({ bufnr = 0 })
|
|
||||||
<
|
<
|
||||||
|
|
||||||
Parameters: ~
|
To run a code lens, see |vim.lsp.codelens.run()|.
|
||||||
• {opts} (`table?`) Optional fields
|
|
||||||
• {bufnr} (`integer?`) filter by buffer. All buffers if nil, 0
|
|
||||||
for current buffer
|
|
||||||
|
|
||||||
run() *vim.lsp.codelens.run()*
|
|
||||||
Run the code lens available in the current line.
|
|
||||||
|
|
||||||
save({lenses}, {bufnr}, {client_id}) *vim.lsp.codelens.save()*
|
|
||||||
Store lenses for a specific buffer and client
|
|
||||||
|
|
||||||
Parameters: ~
|
Parameters: ~
|
||||||
• {lenses} (`lsp.CodeLens[]?`) lenses to store
|
• {enable} (`boolean?`) true/nil to enable, false to disable
|
||||||
• {bufnr} (`integer`)
|
• {filter} (`table?`) Optional filters |kwargs|,
|
||||||
• {client_id} (`integer`)
|
• {bufnr}? (`integer`, default: all) Buffer number, or 0 for
|
||||||
|
current buffer, or nil for all.
|
||||||
|
• {client_id}? (`integer`, default: all) Client ID, or nil
|
||||||
|
for all.
|
||||||
|
|
||||||
|
get({filter}) *vim.lsp.codelens.get()*
|
||||||
|
Get all code lenses in the {filter}ed scope.
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
• {filter} (`table?`) Optional filters |kwargs|:
|
||||||
|
• {bufnr}? (`integer`, default: 0) Buffer handle, or 0 for
|
||||||
|
current.
|
||||||
|
• {client_id}? (`integer`, default: all) Client ID, or nil
|
||||||
|
for all.
|
||||||
|
|
||||||
|
Return: ~
|
||||||
|
(`table[]`) A list of objects with the following fields:
|
||||||
|
• {client_id} (`integer`)
|
||||||
|
• {lens} (`lsp.CodeLens`)
|
||||||
|
|
||||||
|
is_enabled({filter}) *vim.lsp.codelens.is_enabled()*
|
||||||
|
Query whether code lens is enabled in the {filter}ed scope
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
• {filter} (`table?`) Optional filters |kwargs|,
|
||||||
|
• {bufnr}? (`integer`, default: all) Buffer number, or 0 for
|
||||||
|
current buffer, or nil for all.
|
||||||
|
• {client_id}? (`integer`, default: all) Client ID, or nil
|
||||||
|
for all.
|
||||||
|
|
||||||
|
Return: ~
|
||||||
|
(`boolean`) whether code lens is enabled.
|
||||||
|
|
||||||
|
run({opts}) *vim.lsp.codelens.run()*
|
||||||
|
Run code lens above the current cursor position.
|
||||||
|
|
||||||
|
Parameters: ~
|
||||||
|
• {opts} (`table?`) Optional parameters |kwargs|:
|
||||||
|
• {client_id}? (`integer`, default: all) Client ID, or nil for
|
||||||
|
all.
|
||||||
|
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|||||||
@@ -298,6 +298,8 @@ LSP
|
|||||||
• Support for dynamic registration for `textDocument/diagnostic`
|
• Support for dynamic registration for `textDocument/diagnostic`
|
||||||
• |vim.lsp.buf.rename()| now highlights the symbol being renamed using the
|
• |vim.lsp.buf.rename()| now highlights the symbol being renamed using the
|
||||||
|hl-LspReferenceTarget| highlight group.
|
|hl-LspReferenceTarget| highlight group.
|
||||||
|
• Support for `textDocument/codeLens` |lsp-codelens| has been reimplemented:
|
||||||
|
https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/#textDocument_codeLens
|
||||||
|
|
||||||
LUA
|
LUA
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
local api = vim.api
|
local api = vim.api
|
||||||
|
|
||||||
---@alias vim.lsp.capability.Name
|
---@alias vim.lsp.capability.Name
|
||||||
|
---| 'codelens'
|
||||||
---| 'semantic_tokens'
|
---| 'semantic_tokens'
|
||||||
---| 'folding_range'
|
---| 'folding_range'
|
||||||
---| 'linked_editing_range'
|
---| 'linked_editing_range'
|
||||||
|
|||||||
@@ -3,352 +3,461 @@ local log = require('vim.lsp.log')
|
|||||||
local api = vim.api
|
local api = vim.api
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
--- bufnr → true|nil
|
local Capability = require('vim.lsp._capability')
|
||||||
--- to throttle refreshes to at most one at a time
|
|
||||||
local active_refreshes = {} --- @type table<integer,true>
|
|
||||||
|
|
||||||
---@type table<integer, table<integer, lsp.CodeLens[]>>
|
---@class (private) vim.lsp.codelens.ClientState
|
||||||
--- bufnr -> client_id -> lenses
|
---@field row_lenses table<integer, lsp.CodeLens[]?> row -> lens
|
||||||
local lens_cache_by_buf = setmetatable({}, {
|
---@field namespace integer
|
||||||
__index = function(t, b)
|
|
||||||
local key = b > 0 and b or api.nvim_get_current_buf()
|
|
||||||
return rawget(t, key)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
---@type table<integer, integer>
|
---@class (private) vim.lsp.codelens.Provider : vim.lsp.Capability
|
||||||
---client_id -> namespace
|
---@field active table<integer, vim.lsp.codelens.Provider?>
|
||||||
local namespaces = setmetatable({}, {
|
---
|
||||||
__index = function(t, key)
|
--- `TextDocument` version current state corresponds to.
|
||||||
local value = api.nvim_create_namespace('nvim.lsp.codelens:' .. key)
|
---@field version? integer
|
||||||
rawset(t, key, value)
|
---
|
||||||
return value
|
--- Last version of codelens applied to this line.
|
||||||
end,
|
---
|
||||||
})
|
--- 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
|
||||||
|
|
||||||
---@private
|
---@private
|
||||||
M.__namespaces = namespaces
|
---@param client_id? integer
|
||||||
|
function Provider:request(client_id)
|
||||||
local augroup = api.nvim_create_augroup('nvim.lsp.codelens', {})
|
---@type lsp.CodeLensParams
|
||||||
|
local params = { textDocument = util.make_text_document_params(self.bufnr) }
|
||||||
api.nvim_create_autocmd('LspDetach', {
|
for id in pairs(self.client_state) do
|
||||||
group = augroup,
|
if not client_id or client_id == id then
|
||||||
callback = function(ev)
|
local client = assert(vim.lsp.get_client_by_id(id))
|
||||||
M.clear(ev.data.client_id, ev.buf)
|
client:request('textDocument/codeLens', params, function(...)
|
||||||
end,
|
self:handler(...)
|
||||||
})
|
end, self.bufnr)
|
||||||
|
end
|
||||||
---@param lens lsp.CodeLens
|
end
|
||||||
---@param bufnr integer
|
|
||||||
---@param client_id integer
|
|
||||||
local function execute_lens(lens, bufnr, client_id)
|
|
||||||
local line = lens.range.start.line
|
|
||||||
api.nvim_buf_clear_namespace(bufnr, namespaces[client_id], line, line + 1)
|
|
||||||
|
|
||||||
local client = vim.lsp.get_client_by_id(client_id)
|
|
||||||
assert(client, 'Client is required to execute lens, client_id=' .. client_id)
|
|
||||||
client:exec_cmd(lens.command, { bufnr = bufnr }, function(...)
|
|
||||||
vim.lsp.handlers['workspace/executeCommand'](...)
|
|
||||||
M.refresh()
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Return all lenses for the given buffer
|
---@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.
|
||||||
---
|
---
|
||||||
---@param bufnr integer Buffer number. 0 can be used for the current buffer.
|
---@package
|
||||||
---@return lsp.CodeLens[]
|
function Provider:automatic_request()
|
||||||
function M.get(bufnr)
|
self:reset_timer()
|
||||||
local lenses_by_client = lens_cache_by_buf[bufnr or 0]
|
self.timer = vim.defer_fn(function()
|
||||||
if not lenses_by_client then
|
self:request()
|
||||||
return {}
|
end, 200)
|
||||||
end
|
|
||||||
local lenses = {}
|
|
||||||
for _, client_lenses in pairs(lenses_by_client) do
|
|
||||||
vim.list_extend(lenses, client_lenses)
|
|
||||||
end
|
|
||||||
return lenses
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Run the code lens available in the current line.
|
---@private
|
||||||
function M.run()
|
---@param client vim.lsp.Client
|
||||||
local line = api.nvim_win_get_cursor(0)[1] - 1
|
---@param unresolved_lens lsp.CodeLens
|
||||||
local bufnr = api.nvim_get_current_buf()
|
function Provider:resolve(client, unresolved_lens)
|
||||||
local options = {} --- @type {client: integer, lens: lsp.CodeLens}[]
|
---@param resolved_lens lsp.CodeLens
|
||||||
local lenses_by_client = lens_cache_by_buf[bufnr] or {}
|
client:request('codeLens/resolve', unresolved_lens, function(err, resolved_lens, ctx)
|
||||||
for client, lenses in pairs(lenses_by_client) do
|
local state = self.client_state[client.id]
|
||||||
for _, lens in pairs(lenses) do
|
if not state then
|
||||||
if
|
return
|
||||||
lens.command
|
end
|
||||||
and lens.command.command ~= ''
|
|
||||||
and lens.range.start.line <= line
|
if err then
|
||||||
and lens.range['end'].line >= line
|
log.error('codelens/resolve', err)
|
||||||
then
|
return
|
||||||
table.insert(options, { client = client, lens = lens })
|
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 namespace = state.namespace
|
||||||
|
|
||||||
|
api.nvim_buf_clear_namespace(self.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 [string, string][]
|
||||||
|
local virt_text = {}
|
||||||
|
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))
|
||||||
|
self:resolve(client, lens)
|
||||||
|
else
|
||||||
|
vim.list_extend(virt_text, {
|
||||||
|
{ lens.command.title, 'LspCodeLens' },
|
||||||
|
{ ' | ', 'LspCodeLensSeparator' },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- Remove trailing separator.
|
||||||
|
table.remove(virt_text)
|
||||||
|
|
||||||
|
api.nvim_buf_set_extmark(self.bufnr, namespace, row, 0, {
|
||||||
|
virt_text = virt_text,
|
||||||
|
hl_mode = 'combine',
|
||||||
|
})
|
||||||
|
end
|
||||||
|
self.row_version[row] = self.version
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if #options == 0 then
|
end
|
||||||
vim.notify('No executable codelens found at current line')
|
|
||||||
elseif #options == 1 then
|
local namespace = api.nvim_create_namespace('nvim.lsp.codelens')
|
||||||
local option = options[1]
|
api.nvim_set_decoration_provider(namespace, {
|
||||||
execute_lens(option.lens, bufnr, option.client)
|
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
|
||||||
|
|
||||||
|
--- 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 above 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 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
|
else
|
||||||
vim.ui.select(options, {
|
vim.ui.select(items, {
|
||||||
prompt = 'Code lenses:',
|
prompt = 'Code Lens',
|
||||||
kind = 'codelens',
|
---@param item [integer, lsp.CodeLens]
|
||||||
format_item = function(option)
|
format_item = function(item)
|
||||||
return option.lens.command.title
|
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,
|
end,
|
||||||
}, function(option)
|
}, function(item)
|
||||||
if option then
|
if item then
|
||||||
execute_lens(option.lens, bufnr, option.client)
|
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)
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Clear the lenses
|
---@deprecated
|
||||||
---
|
---@param client_id? integer
|
||||||
---@param client_id integer|nil filter by client_id. All clients if nil
|
---@param bufnr? integer
|
||||||
---@param bufnr integer|nil filter by buffer. All buffers if nil, 0 for current buffer
|
|
||||||
function M.clear(client_id, bufnr)
|
function M.clear(client_id, bufnr)
|
||||||
bufnr = bufnr and vim._resolve_bufnr(bufnr)
|
vim.deprecate(
|
||||||
local buffers = bufnr and { bufnr }
|
'vim.lsp.codelens.clear(client_id, bufnr)',
|
||||||
or vim.tbl_filter(api.nvim_buf_is_loaded, api.nvim_list_bufs())
|
'vim.lsp.codelens.enable(false, { bufnr = bufnr, client_id = client_id })',
|
||||||
for _, iter_bufnr in pairs(buffers) do
|
'0.13.0'
|
||||||
local client_ids = client_id and { client_id } or vim.tbl_keys(namespaces)
|
)
|
||||||
for _, iter_client_id in pairs(client_ids) do
|
M.enable(false, { bufnr = bufnr, client_id = client_id })
|
||||||
local ns = namespaces[iter_client_id]
|
|
||||||
-- there can be display()ed lenses, which are not stored in cache
|
|
||||||
if lens_cache_by_buf[iter_bufnr] then
|
|
||||||
lens_cache_by_buf[iter_bufnr][iter_client_id] = {}
|
|
||||||
end
|
|
||||||
api.nvim_buf_clear_namespace(iter_bufnr, ns, 0, -1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param lenses lsp.CodeLens[]
|
---@deprecated
|
||||||
---@return table<integer, lsp.CodeLens[]>
|
|
||||||
local function group_lenses_by_start_line(lenses)
|
|
||||||
local lenses_by_lnum = {} ---@type table<integer, lsp.CodeLens[]>
|
|
||||||
for _, lens in pairs(lenses) do
|
|
||||||
local line_lenses = lenses_by_lnum[lens.range.start.line]
|
|
||||||
if not line_lenses then
|
|
||||||
line_lenses = {}
|
|
||||||
lenses_by_lnum[lens.range.start.line] = line_lenses
|
|
||||||
end
|
|
||||||
table.insert(line_lenses, lens)
|
|
||||||
end
|
|
||||||
return lenses_by_lnum
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param bufnr integer
|
|
||||||
---@param ns integer
|
|
||||||
---@param line integer
|
|
||||||
---@param lenses lsp.CodeLens[] Lenses that start at `line`
|
|
||||||
local function display_line_lenses(bufnr, ns, line, lenses)
|
|
||||||
local chunks = {}
|
|
||||||
local num_lenses = #lenses
|
|
||||||
table.sort(lenses, function(a, b)
|
|
||||||
return a.range.start.character < b.range.start.character
|
|
||||||
end)
|
|
||||||
|
|
||||||
local has_unresolved = false
|
|
||||||
for i, lens in ipairs(lenses) do
|
|
||||||
if lens.command then
|
|
||||||
local text = lens.command.title:gsub('%s+', ' ')
|
|
||||||
table.insert(chunks, { text, 'LspCodeLens' })
|
|
||||||
if i < num_lenses then
|
|
||||||
table.insert(chunks, { ' | ', 'LspCodeLensSeparator' })
|
|
||||||
end
|
|
||||||
else
|
|
||||||
has_unresolved = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
-- If some lenses are not resolved yet, don't update the line's virtual text. Due to this, user
|
|
||||||
-- may see outdated lenses or not see already resolved lenses. However, showing outdated lenses
|
|
||||||
-- for short period of time is better than spamming user with virtual text updates.
|
|
||||||
if has_unresolved then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
api.nvim_buf_clear_namespace(bufnr, ns, line, line + 1)
|
|
||||||
if #chunks > 0 then
|
|
||||||
api.nvim_buf_set_extmark(bufnr, ns, line, 0, {
|
|
||||||
virt_text = chunks,
|
|
||||||
hl_mode = 'combine',
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Display the lenses using virtual text
|
|
||||||
---
|
|
||||||
---@param lenses? lsp.CodeLens[] lenses to display
|
---@param lenses? lsp.CodeLens[] lenses to display
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@param client_id integer
|
---@param client_id integer
|
||||||
function M.display(lenses, bufnr, client_id)
|
function M.display(lenses, bufnr, client_id)
|
||||||
if not api.nvim_buf_is_loaded(bufnr) then
|
vim.deprecate('vim.lsp.codelens.display()', nil, '0.13.0')
|
||||||
return
|
local _, _, _ = lenses, bufnr, client_id
|
||||||
end
|
|
||||||
|
|
||||||
local ns = namespaces[client_id]
|
|
||||||
if not lenses or not next(lenses) then
|
|
||||||
api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local lenses_by_lnum = group_lenses_by_start_line(lenses)
|
|
||||||
local num_lines = api.nvim_buf_line_count(bufnr)
|
|
||||||
for i = 0, num_lines do
|
|
||||||
display_line_lenses(bufnr, ns, i, lenses_by_lnum[i] or {})
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Store lenses for a specific buffer and client
|
---@deprecated
|
||||||
---
|
|
||||||
---@param lenses? lsp.CodeLens[] lenses to store
|
---@param lenses? lsp.CodeLens[] lenses to store
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
---@param client_id integer
|
---@param client_id integer
|
||||||
function M.save(lenses, bufnr, client_id)
|
function M.save(lenses, bufnr, client_id)
|
||||||
if not api.nvim_buf_is_loaded(bufnr) then
|
vim.deprecate('vim.lsp.codelens.save()', nil, '0.13.0')
|
||||||
return
|
local _, _, _ = lenses, bufnr, client_id
|
||||||
end
|
|
||||||
|
|
||||||
local lenses_by_client = lens_cache_by_buf[bufnr]
|
|
||||||
if not lenses_by_client then
|
|
||||||
lenses_by_client = {}
|
|
||||||
lens_cache_by_buf[bufnr] = lenses_by_client
|
|
||||||
local ns = namespaces[client_id]
|
|
||||||
api.nvim_buf_attach(bufnr, false, {
|
|
||||||
on_detach = function(_, b)
|
|
||||||
lens_cache_by_buf[b] = nil
|
|
||||||
end,
|
|
||||||
on_lines = function(_, b, _, first_lnum, last_lnum)
|
|
||||||
api.nvim_buf_clear_namespace(b, ns, first_lnum, last_lnum)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
lenses_by_client[client_id] = lenses
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param lenses? lsp.CodeLens[]
|
---@deprecated
|
||||||
---@param bufnr integer
|
---@param err? lsp.ResponseError
|
||||||
---@param client_id integer
|
|
||||||
---@param callback fun()
|
|
||||||
local function resolve_lenses(lenses, bufnr, client_id, callback)
|
|
||||||
lenses = lenses or {}
|
|
||||||
local num_lens = vim.tbl_count(lenses)
|
|
||||||
if num_lens == 0 then
|
|
||||||
callback()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param n integer
|
|
||||||
local function countdown(n)
|
|
||||||
num_lens = num_lens - n
|
|
||||||
if num_lens == 0 then
|
|
||||||
callback()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local ns = namespaces[client_id]
|
|
||||||
local client = vim.lsp.get_client_by_id(client_id)
|
|
||||||
|
|
||||||
-- Resolve all lenses in a line, then display them.
|
|
||||||
local lenses_by_lnum = group_lenses_by_start_line(lenses)
|
|
||||||
for line, line_lenses in pairs(lenses_by_lnum) do
|
|
||||||
local num_resolved_line_lenses = 0
|
|
||||||
local function display_line_countdown()
|
|
||||||
num_resolved_line_lenses = num_resolved_line_lenses + 1
|
|
||||||
if num_resolved_line_lenses == #line_lenses then
|
|
||||||
if api.nvim_buf_is_valid(bufnr) and line <= api.nvim_buf_line_count(bufnr) then
|
|
||||||
display_line_lenses(bufnr, ns, line, line_lenses)
|
|
||||||
end
|
|
||||||
countdown(#line_lenses)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, lens in pairs(line_lenses) do
|
|
||||||
if lens.command then
|
|
||||||
display_line_countdown()
|
|
||||||
else
|
|
||||||
assert(client)
|
|
||||||
client:request('codeLens/resolve', lens, function(_, result)
|
|
||||||
if api.nvim_buf_is_loaded(bufnr) and result and result.command then
|
|
||||||
lens.command = result.command
|
|
||||||
end
|
|
||||||
display_line_countdown()
|
|
||||||
end, bufnr)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
--- |lsp-handler| for the method `textDocument/codeLens`
|
|
||||||
---
|
|
||||||
---@param err lsp.ResponseError?
|
|
||||||
---@param result lsp.CodeLens[]
|
---@param result lsp.CodeLens[]
|
||||||
---@param ctx lsp.HandlerContext
|
---@param ctx lsp.HandlerContext
|
||||||
function M.on_codelens(err, result, ctx)
|
function M.on_codelens(err, result, ctx)
|
||||||
local bufnr = assert(ctx.bufnr)
|
vim.deprecate('vim.lsp.codelens.on_codelens()', nil, '0.13.0')
|
||||||
|
local _, _, _ = err, result, ctx
|
||||||
if err then
|
|
||||||
active_refreshes[bufnr] = nil
|
|
||||||
log.error('codelens', err)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
M.save(result, bufnr, ctx.client_id)
|
|
||||||
|
|
||||||
-- Eager display for any resolved lenses and refresh them once resolved.
|
|
||||||
M.display(result, bufnr, ctx.client_id)
|
|
||||||
resolve_lenses(result, bufnr, ctx.client_id, function()
|
|
||||||
active_refreshes[bufnr] = nil
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @class vim.lsp.codelens.refresh.Opts
|
---@class vim.lsp.codelens.refresh.Opts
|
||||||
--- @inlinedoc
|
---@inlinedoc
|
||||||
--- @field bufnr integer? filter by buffer. All buffers if nil, 0 for current buffer
|
---@field bufnr? integer
|
||||||
|
|
||||||
--- Refresh the lenses.
|
---@deprecated
|
||||||
---
|
---@param opts? vim.lsp.codelens.refresh.Opts Optional fields
|
||||||
--- It is recommended to trigger this using an autocmd or via keymap.
|
|
||||||
---
|
|
||||||
--- Example:
|
|
||||||
---
|
|
||||||
--- ```vim
|
|
||||||
--- autocmd BufEnter,CursorHold,InsertLeave <buffer> lua vim.lsp.codelens.refresh({ bufnr = 0 })
|
|
||||||
--- ```
|
|
||||||
---
|
|
||||||
--- @param opts? vim.lsp.codelens.refresh.Opts Optional fields
|
|
||||||
function M.refresh(opts)
|
function M.refresh(opts)
|
||||||
opts = opts or {}
|
vim.deprecate(
|
||||||
local bufnr = opts.bufnr and vim._resolve_bufnr(opts.bufnr)
|
'vim.lsp.codelens.refresh({ bufnr = bufnr})',
|
||||||
local buffers = bufnr and { bufnr }
|
'vim.lsp.codelens.enable(true, { bufnr = bufnr })',
|
||||||
or vim.tbl_filter(api.nvim_buf_is_loaded, api.nvim_list_bufs())
|
'0.13.0'
|
||||||
|
)
|
||||||
|
|
||||||
for _, buf in ipairs(buffers) do
|
vim.validate('opts', opts, 'table', true)
|
||||||
if not active_refreshes[buf] then
|
M.enable(true, { bufnr = opts and opts.bufnr })
|
||||||
local params = {
|
|
||||||
textDocument = util.make_text_document_params(buf),
|
|
||||||
}
|
|
||||||
active_refreshes[buf] = true
|
|
||||||
|
|
||||||
local request_ids = vim.lsp.buf_request(
|
|
||||||
buf,
|
|
||||||
'textDocument/codeLens',
|
|
||||||
params,
|
|
||||||
M.on_codelens,
|
|
||||||
function() end
|
|
||||||
)
|
|
||||||
if vim.tbl_isempty(request_ids) then
|
|
||||||
active_refreshes[buf] = nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -254,11 +254,6 @@ RCS['textDocument/diagnostic'] = function(...)
|
|||||||
return vim.lsp.diagnostic.on_diagnostic(...)
|
return vim.lsp.diagnostic.on_diagnostic(...)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @private
|
|
||||||
RCS['textDocument/codeLens'] = function(...)
|
|
||||||
return vim.lsp.codelens.on_codelens(...)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- @private
|
--- @private
|
||||||
RCS['textDocument/inlayHint'] = function(...)
|
RCS['textDocument/inlayHint'] = function(...)
|
||||||
return vim.lsp.inlay_hint.on_inlayhint(...)
|
return vim.lsp.inlay_hint.on_inlayhint(...)
|
||||||
|
|||||||
@@ -1,107 +1,248 @@
|
|||||||
local t = require('test.testutil')
|
local t = require('test.testutil')
|
||||||
local n = require('test.functional.testnvim')()
|
local n = require('test.functional.testnvim')()
|
||||||
|
local t_lsp = require('test.functional.plugin.lsp.testutil')
|
||||||
|
local Screen = require('test.functional.ui.screen')
|
||||||
|
|
||||||
local exec_lua = n.exec_lua
|
local dedent = t.dedent
|
||||||
local eq = t.eq
|
local eq = t.eq
|
||||||
|
|
||||||
describe('vim.lsp.codelens', function()
|
local api = n.api
|
||||||
before_each(function()
|
local exec_lua = n.exec_lua
|
||||||
n.clear()
|
local insert = n.insert
|
||||||
exec_lua('require("vim.lsp")')
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('on_codelens_stores_and_displays_lenses', function()
|
local clear_notrace = t_lsp.clear_notrace
|
||||||
local fake_uri = 'file:///fake/uri'
|
local create_server_definition = t_lsp.create_server_definition
|
||||||
local bufnr = exec_lua(function()
|
|
||||||
local bufnr = vim.uri_to_bufnr(fake_uri)
|
describe('vim.lsp.codelens', function()
|
||||||
local lines = { 'So', 'many', 'lines' }
|
local text = dedent([[
|
||||||
vim.fn.bufload(bufnr)
|
struct S {
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
a: i32,
|
||||||
return bufnr
|
b: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl S {
|
||||||
|
fn new(a: i32, b: String) -> Self {
|
||||||
|
S { a, b }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let s = S::new(42, String::from("Hello, world!"));
|
||||||
|
println!("S.a: {}, S.b: {}", s.a, s.b);
|
||||||
|
}
|
||||||
|
]])
|
||||||
|
|
||||||
|
local grid_with_lenses = dedent([[
|
||||||
|
struct S { {1:1 implementation} |
|
||||||
|
a: i32, |
|
||||||
|
b: String, |
|
||||||
|
} |
|
||||||
|
|
|
||||||
|
impl S { |
|
||||||
|
fn new(a: i32, b: String) -> Self { |
|
||||||
|
S { a, b } |
|
||||||
|
} |
|
||||||
|
} |
|
||||||
|
|
|
||||||
|
fn main() { {1:▶︎ Run } |
|
||||||
|
let s = S::new(42, String::from("Hello, world!"))|
|
||||||
|
; |
|
||||||
|
println!("S.a: {}, S.b: {}", s.a, s.b); |
|
||||||
|
} |
|
||||||
|
^ |
|
||||||
|
{1:~ }|*2
|
||||||
|
|
|
||||||
|
]])
|
||||||
|
|
||||||
|
local grid_without_lenses = dedent([[
|
||||||
|
struct S { |
|
||||||
|
a: i32, |
|
||||||
|
b: String, |
|
||||||
|
} |
|
||||||
|
|
|
||||||
|
impl S { |
|
||||||
|
fn new(a: i32, b: String) -> Self { |
|
||||||
|
S { a, b } |
|
||||||
|
} |
|
||||||
|
} |
|
||||||
|
|
|
||||||
|
fn main() { |
|
||||||
|
let s = S::new(42, String::from("Hello, world!"))|
|
||||||
|
; |
|
||||||
|
println!("S.a: {}, S.b: {}", s.a, s.b); |
|
||||||
|
} |
|
||||||
|
^ |
|
||||||
|
{1:~ }|*2
|
||||||
|
|
|
||||||
|
]])
|
||||||
|
|
||||||
|
--- @type test.functional.ui.screen
|
||||||
|
local screen
|
||||||
|
|
||||||
|
--- @type integer
|
||||||
|
local client_id
|
||||||
|
|
||||||
|
before_each(function()
|
||||||
|
clear_notrace()
|
||||||
|
exec_lua(create_server_definition)
|
||||||
|
|
||||||
|
screen = Screen.new(nil, 20)
|
||||||
|
|
||||||
|
client_id = exec_lua(function()
|
||||||
|
_G.server = _G._create_server({
|
||||||
|
capabilities = {
|
||||||
|
codeLensProvider = {
|
||||||
|
resolveProvider = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handlers = {
|
||||||
|
['textDocument/codeLens'] = function(_, _, callback)
|
||||||
|
callback(nil, {
|
||||||
|
{
|
||||||
|
data = {
|
||||||
|
kind = {
|
||||||
|
impls = {
|
||||||
|
position = {
|
||||||
|
character = 7,
|
||||||
|
line = 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
version = 0,
|
||||||
|
},
|
||||||
|
range = {
|
||||||
|
['end'] = {
|
||||||
|
character = 8,
|
||||||
|
line = 0,
|
||||||
|
},
|
||||||
|
start = {
|
||||||
|
character = 7,
|
||||||
|
line = 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
command = {
|
||||||
|
arguments = {},
|
||||||
|
command = 'rust-analyzer.runSingle',
|
||||||
|
title = '▶︎ Run ',
|
||||||
|
},
|
||||||
|
range = {
|
||||||
|
['end'] = {
|
||||||
|
character = 7,
|
||||||
|
line = 11,
|
||||||
|
},
|
||||||
|
start = {
|
||||||
|
character = 3,
|
||||||
|
line = 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end,
|
||||||
|
['codeLens/resolve'] = function(_, _, callback)
|
||||||
|
vim.schedule(function()
|
||||||
|
callback(nil, {
|
||||||
|
command = {
|
||||||
|
arguments = {},
|
||||||
|
command = 'rust-analyzer.showReferences',
|
||||||
|
title = '1 implementation',
|
||||||
|
},
|
||||||
|
range = {
|
||||||
|
['end'] = {
|
||||||
|
character = 8,
|
||||||
|
line = 0,
|
||||||
|
},
|
||||||
|
start = {
|
||||||
|
character = 7,
|
||||||
|
line = 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
insert(text)
|
||||||
|
|
||||||
exec_lua(function()
|
exec_lua(function()
|
||||||
local lenses = {
|
vim.lsp.codelens.enable()
|
||||||
{
|
|
||||||
range = {
|
|
||||||
start = { line = 0, character = 0 },
|
|
||||||
['end'] = { line = 0, character = 0 },
|
|
||||||
},
|
|
||||||
command = { title = 'Lens1', command = 'Dummy' },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
vim.lsp.codelens.on_codelens(
|
|
||||||
nil,
|
|
||||||
lenses,
|
|
||||||
{ method = 'textDocument/codeLens', client_id = 1, bufnr = bufnr }
|
|
||||||
)
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
local stored_lenses = exec_lua(function()
|
screen:expect({ grid = grid_with_lenses })
|
||||||
return vim.lsp.codelens.get(bufnr)
|
end)
|
||||||
|
|
||||||
|
it('clears code lenses when disabled', function()
|
||||||
|
exec_lua(function()
|
||||||
|
vim.lsp.codelens.enable(false)
|
||||||
end)
|
end)
|
||||||
local expected = {
|
|
||||||
|
screen:expect({ grid = grid_without_lenses })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('clears code lenses when sole client detaches', function()
|
||||||
|
exec_lua(function()
|
||||||
|
vim.lsp.get_client_by_id(client_id):stop()
|
||||||
|
end)
|
||||||
|
|
||||||
|
screen:expect({ grid = grid_without_lenses })
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('get code lenses in the current buffer', function()
|
||||||
|
local result = exec_lua(function()
|
||||||
|
vim.api.nvim_win_set_cursor(0, { 12, 3 })
|
||||||
|
return vim.lsp.codelens.get()
|
||||||
|
end)
|
||||||
|
|
||||||
|
eq({
|
||||||
{
|
{
|
||||||
range = {
|
client_id = 1,
|
||||||
start = { line = 0, character = 0 },
|
lens = {
|
||||||
['end'] = { line = 0, character = 0 },
|
command = {
|
||||||
},
|
arguments = {},
|
||||||
command = {
|
command = 'rust-analyzer.showReferences',
|
||||||
title = 'Lens1',
|
title = '1 implementation',
|
||||||
command = 'Dummy',
|
},
|
||||||
|
range = {
|
||||||
|
['end'] = {
|
||||||
|
character = 8,
|
||||||
|
line = 0,
|
||||||
|
},
|
||||||
|
start = {
|
||||||
|
character = 7,
|
||||||
|
line = 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
eq(expected, stored_lenses)
|
client_id = 1,
|
||||||
|
lens = {
|
||||||
local virtual_text_chunks = exec_lua(function()
|
command = {
|
||||||
local ns = vim.lsp.codelens.__namespaces[1]
|
arguments = {},
|
||||||
local extmarks = vim.api.nvim_buf_get_extmarks(bufnr, ns, 0, -1, {})
|
command = 'rust-analyzer.runSingle',
|
||||||
return vim.api.nvim_buf_get_extmark_by_id(bufnr, ns, extmarks[1][1], { details = true })[3].virt_text
|
title = '▶︎ Run ',
|
||||||
end)
|
},
|
||||||
|
range = {
|
||||||
eq({ [1] = { 'Lens1', 'LspCodeLens' } }, virtual_text_chunks)
|
['end'] = {
|
||||||
|
character = 7,
|
||||||
|
line = 11,
|
||||||
|
},
|
||||||
|
start = {
|
||||||
|
character = 3,
|
||||||
|
line = 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, result)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('can clear all lens', function()
|
after_each(function()
|
||||||
local fake_uri = 'file:///fake/uri'
|
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
|
||||||
local bufnr = exec_lua(function()
|
|
||||||
local bufnr = vim.uri_to_bufnr(fake_uri)
|
|
||||||
local lines = { 'So', 'many', 'lines' }
|
|
||||||
vim.fn.bufload(bufnr)
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
|
||||||
return bufnr
|
|
||||||
end)
|
|
||||||
|
|
||||||
exec_lua(function()
|
|
||||||
local lenses = {
|
|
||||||
{
|
|
||||||
range = {
|
|
||||||
start = { line = 0, character = 0 },
|
|
||||||
['end'] = { line = 0, character = 0 },
|
|
||||||
},
|
|
||||||
command = { title = 'Lens1', command = 'Dummy' },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
vim.lsp.codelens.on_codelens(
|
|
||||||
nil,
|
|
||||||
lenses,
|
|
||||||
{ method = 'textDocument/codeLens', client_id = 1, bufnr = bufnr }
|
|
||||||
)
|
|
||||||
end)
|
|
||||||
|
|
||||||
local stored_lenses = exec_lua(function()
|
|
||||||
return vim.lsp.codelens.get(bufnr)
|
|
||||||
end)
|
|
||||||
eq(1, #stored_lenses)
|
|
||||||
|
|
||||||
exec_lua(function()
|
|
||||||
vim.lsp.codelens.clear()
|
|
||||||
end)
|
|
||||||
|
|
||||||
stored_lenses = exec_lua(function()
|
|
||||||
return vim.lsp.codelens.get(bufnr)
|
|
||||||
end)
|
|
||||||
eq(0, #stored_lenses)
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -4944,230 +4944,6 @@ describe('LSP', function()
|
|||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
describe('vim.lsp.codelens', function()
|
|
||||||
it('uses client commands', function()
|
|
||||||
local client --- @type vim.lsp.Client
|
|
||||||
local expected_handlers = {
|
|
||||||
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
||||||
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
||||||
}
|
|
||||||
test_rpc_server {
|
|
||||||
test_name = 'clientside_commands',
|
|
||||||
on_init = function(client_)
|
|
||||||
client = client_
|
|
||||||
end,
|
|
||||||
on_setup = function() end,
|
|
||||||
on_exit = function(code, signal)
|
|
||||||
eq(0, code, 'exit code')
|
|
||||||
eq(0, signal, 'exit signal')
|
|
||||||
end,
|
|
||||||
on_handler = function(err, result, ctx)
|
|
||||||
eq(table.remove(expected_handlers), { err, result, ctx })
|
|
||||||
if ctx.method == 'start' then
|
|
||||||
local fake_uri = 'file:///fake/uri'
|
|
||||||
local cmd = exec_lua(function()
|
|
||||||
local bufnr = vim.uri_to_bufnr(fake_uri)
|
|
||||||
vim.fn.bufload(bufnr)
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'One line' })
|
|
||||||
local lenses = {
|
|
||||||
{
|
|
||||||
range = {
|
|
||||||
start = { line = 0, character = 0 },
|
|
||||||
['end'] = { line = 0, character = 8 },
|
|
||||||
},
|
|
||||||
command = { title = 'Lens1', command = 'Dummy' },
|
|
||||||
},
|
|
||||||
}
|
|
||||||
vim.lsp.codelens.on_codelens(
|
|
||||||
nil,
|
|
||||||
lenses,
|
|
||||||
{ method = 'textDocument/codeLens', client_id = 1, bufnr = bufnr }
|
|
||||||
)
|
|
||||||
local cmd_called = nil
|
|
||||||
vim.lsp.commands['Dummy'] = function(command0)
|
|
||||||
cmd_called = command0
|
|
||||||
end
|
|
||||||
vim.api.nvim_set_current_buf(bufnr)
|
|
||||||
vim.lsp.codelens.run()
|
|
||||||
return cmd_called
|
|
||||||
end)
|
|
||||||
eq({ command = 'Dummy', title = 'Lens1' }, cmd)
|
|
||||||
elseif ctx.method == 'shutdown' then
|
|
||||||
client:stop()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('releases buffer refresh lock', function()
|
|
||||||
local client --- @type vim.lsp.Client
|
|
||||||
local expected_handlers = {
|
|
||||||
{ NIL, {}, { method = 'shutdown', client_id = 1 } },
|
|
||||||
{ NIL, {}, { method = 'start', client_id = 1 } },
|
|
||||||
}
|
|
||||||
test_rpc_server {
|
|
||||||
test_name = 'codelens_refresh_lock',
|
|
||||||
on_init = function(client_)
|
|
||||||
client = client_
|
|
||||||
end,
|
|
||||||
on_setup = function()
|
|
||||||
exec_lua(function()
|
|
||||||
local bufnr = vim.api.nvim_get_current_buf()
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'One line' })
|
|
||||||
vim.lsp.buf_attach_client(bufnr, _G.TEST_RPC_CLIENT_ID)
|
|
||||||
|
|
||||||
_G.CALLED = false
|
|
||||||
_G.RESPONSE = nil
|
|
||||||
local on_codelens = vim.lsp.codelens.on_codelens
|
|
||||||
vim.lsp.codelens.on_codelens = function(err, result, ...)
|
|
||||||
_G.CALLED = true
|
|
||||||
_G.RESPONSE = { err = err, result = result }
|
|
||||||
return on_codelens(err, result, ...)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end,
|
|
||||||
on_exit = function(code, signal)
|
|
||||||
eq(0, code, 'exit code')
|
|
||||||
eq(0, signal, 'exit signal')
|
|
||||||
end,
|
|
||||||
on_handler = function(err, result, ctx)
|
|
||||||
eq(table.remove(expected_handlers), { err, result, ctx })
|
|
||||||
if ctx.method == 'start' then
|
|
||||||
-- 1. first codelens request errors
|
|
||||||
local response = exec_lua(function()
|
|
||||||
_G.CALLED = false
|
|
||||||
vim.lsp.codelens.refresh()
|
|
||||||
vim.wait(100, function()
|
|
||||||
return _G.CALLED
|
|
||||||
end)
|
|
||||||
return _G.RESPONSE
|
|
||||||
end)
|
|
||||||
eq({ err = { code = -32002, message = 'ServerNotInitialized' } }, response)
|
|
||||||
|
|
||||||
-- 2. second codelens request runs
|
|
||||||
response = exec_lua(function()
|
|
||||||
_G.CALLED = false
|
|
||||||
local cmd_called --- @type string?
|
|
||||||
vim.lsp.commands['Dummy'] = function(command0)
|
|
||||||
cmd_called = command0
|
|
||||||
end
|
|
||||||
vim.lsp.codelens.refresh()
|
|
||||||
vim.wait(100, function()
|
|
||||||
return _G.CALLED
|
|
||||||
end)
|
|
||||||
vim.lsp.codelens.run()
|
|
||||||
vim.wait(100, function()
|
|
||||||
return cmd_called ~= nil
|
|
||||||
end)
|
|
||||||
return cmd_called
|
|
||||||
end)
|
|
||||||
eq({ command = 'Dummy', title = 'Lens1' }, response)
|
|
||||||
|
|
||||||
-- 3. third codelens request runs
|
|
||||||
response = exec_lua(function()
|
|
||||||
_G.CALLED = false
|
|
||||||
local cmd_called --- @type string?
|
|
||||||
vim.lsp.commands['Dummy'] = function(command0)
|
|
||||||
cmd_called = command0
|
|
||||||
end
|
|
||||||
vim.lsp.codelens.refresh()
|
|
||||||
vim.wait(100, function()
|
|
||||||
return _G.CALLED
|
|
||||||
end)
|
|
||||||
vim.lsp.codelens.run()
|
|
||||||
vim.wait(100, function()
|
|
||||||
return cmd_called ~= nil
|
|
||||||
end)
|
|
||||||
return cmd_called
|
|
||||||
end)
|
|
||||||
eq({ command = 'Dummy', title = 'Lens2' }, response)
|
|
||||||
elseif ctx.method == 'shutdown' then
|
|
||||||
client:stop()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
}
|
|
||||||
end)
|
|
||||||
|
|
||||||
it('refresh multiple buffers', function()
|
|
||||||
local lens_title_per_fake_uri = {
|
|
||||||
['file:///fake/uri1'] = 'Lens1',
|
|
||||||
['file:///fake/uri2'] = 'Lens2',
|
|
||||||
}
|
|
||||||
exec_lua(create_server_definition)
|
|
||||||
|
|
||||||
-- setup lsp
|
|
||||||
exec_lua(function()
|
|
||||||
local server = _G._create_server({
|
|
||||||
capabilities = {
|
|
||||||
codeLensProvider = {
|
|
||||||
resolveProvider = true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
handlers = {
|
|
||||||
['textDocument/codeLens'] = function(_, params, callback)
|
|
||||||
local lenses = {
|
|
||||||
{
|
|
||||||
range = {
|
|
||||||
start = { line = 0, character = 0 },
|
|
||||||
['end'] = { line = 0, character = 0 },
|
|
||||||
},
|
|
||||||
command = {
|
|
||||||
title = lens_title_per_fake_uri[params.textDocument.uri],
|
|
||||||
command = 'Dummy',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
callback(nil, lenses)
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
_G.CLIENT_ID = vim.lsp.start({
|
|
||||||
name = 'dummy',
|
|
||||||
cmd = server.cmd,
|
|
||||||
})
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- create buffers and setup handler
|
|
||||||
exec_lua(function()
|
|
||||||
local default_buf = vim.api.nvim_get_current_buf()
|
|
||||||
for fake_uri in pairs(lens_title_per_fake_uri) do
|
|
||||||
local bufnr = vim.uri_to_bufnr(fake_uri)
|
|
||||||
vim.api.nvim_set_current_buf(bufnr)
|
|
||||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'Some contents' })
|
|
||||||
vim.lsp.buf_attach_client(bufnr, _G.CLIENT_ID)
|
|
||||||
end
|
|
||||||
vim.api.nvim_buf_delete(default_buf, { force = true })
|
|
||||||
|
|
||||||
_G.REQUEST_COUNT = vim.tbl_count(lens_title_per_fake_uri)
|
|
||||||
_G.RESPONSES = {}
|
|
||||||
local on_codelens = vim.lsp.codelens.on_codelens
|
|
||||||
vim.lsp.codelens.on_codelens = function(err, result, ctx, ...)
|
|
||||||
table.insert(_G.RESPONSES, { err = err, result = result, ctx = ctx })
|
|
||||||
return on_codelens(err, result, ctx, ...)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
-- call codelens refresh
|
|
||||||
local cmds = exec_lua(function()
|
|
||||||
_G.RESPONSES = {}
|
|
||||||
vim.lsp.codelens.refresh()
|
|
||||||
vim.wait(100, function()
|
|
||||||
return #_G.RESPONSES >= _G.REQUEST_COUNT
|
|
||||||
end)
|
|
||||||
|
|
||||||
local cmds = {}
|
|
||||||
for _, resp in ipairs(_G.RESPONSES) do
|
|
||||||
local uri = resp.ctx.params.textDocument.uri
|
|
||||||
cmds[uri] = resp.result[1].command
|
|
||||||
end
|
|
||||||
return cmds
|
|
||||||
end)
|
|
||||||
eq({ command = 'Dummy', title = 'Lens1' }, cmds['file:///fake/uri1'])
|
|
||||||
eq({ command = 'Dummy', title = 'Lens2' }, cmds['file:///fake/uri2'])
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
|
|
||||||
describe('vim.lsp.buf.format', function()
|
describe('vim.lsp.buf.format', function()
|
||||||
it('aborts with notify if no client matches filter', function()
|
it('aborts with notify if no client matches filter', function()
|
||||||
local client --- @type vim.lsp.Client
|
local client --- @type vim.lsp.Client
|
||||||
|
|||||||
Reference in New Issue
Block a user