feat(lsp)!: reimplement textDocument/codeLens as decoration provider

This commit is contained in:
Yi Ming
2026-01-30 15:12:33 +08:00
parent af4115ce2b
commit fe23168e2b
8 changed files with 697 additions and 674 deletions

View File

@@ -4944,230 +4944,6 @@ describe('LSP', function()
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()
it('aborts with notify if no client matches filter', function()
local client --- @type vim.lsp.Client