mirror of
https://github.com/neovim/neovim.git
synced 2026-07-23 17:32:51 +00:00
Problem: The codelens LSP module was using its own raw buffer events and its own debounce mechanism for refreshing code lens in attached buffers. Solution: Switch the module to using the LspNotify autocmd events. LspNotify fires just after document versions are synced with the server and provides a built in debounce mechanism for changes. Additionally, this fixes some bugs with the previous implementation: 1. The workspace/codeLens/refresh handler re-requested codelens for all buffers but when the response came back, it forced an extra redraw after clearing the work the handler had just done. 2. Document synchronization was reworked to be more resilient to multiple clients providing codelens for a single buffer. The latest document version is now separately tracked per client (and per client's lenses per row) instead of for the buffer as a whole. This allows the on_win() function to properly redraw all codelens even when different clients' responses for a particular document version come back at different times.
544 lines
17 KiB
Lua
544 lines
17 KiB
Lua
local t = require('test.testutil')
|
|
local n = require('test.functional.testnvim')()
|
|
local t_lsp = require('test.functional.plugin.lsp.testutil')
|
|
local Screen = require('test.functional.ui.screen')
|
|
|
|
local dedent = t.dedent
|
|
local eq = t.eq
|
|
|
|
local api = n.api
|
|
local exec_lua = n.exec_lua
|
|
local insert = n.insert
|
|
local feed = n.feed
|
|
|
|
local clear_notrace = t_lsp.clear_notrace
|
|
local create_server_definition = t_lsp.create_server_definition
|
|
|
|
describe('vim.lsp.codelens', function()
|
|
local text = 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);
|
|
}
|
|
]])
|
|
|
|
local grid_with_lenses = dedent([[
|
|
{1: 1 implementation} |
|
|
struct S { |
|
|
a: i32, |
|
|
b: String, |
|
|
} |
|
|
|
|
|
impl S { |
|
|
fn new(a: i32, b: String) -> Self { |
|
|
S { a, b } |
|
|
} |
|
|
} |
|
|
|
|
|
{1: ▶︎ Run } |
|
|
fn main() { |
|
|
let s = S::new(42, String::from("Hello, world!"))|
|
|
; |
|
|
println!("S.a: {}, S.b: {}", s.a, s.b); |
|
|
} |
|
|
^ |
|
|
|
|
|
]])
|
|
|
|
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)
|
|
exec_lua(function()
|
|
vim.lsp.config('*', {
|
|
flags = { debounce_text_changes = 0 },
|
|
})
|
|
end)
|
|
|
|
screen = Screen.new(nil, 20)
|
|
|
|
client_id = exec_lua(function()
|
|
_G.server = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
|
|
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)
|
|
|
|
insert(text)
|
|
|
|
exec_lua(function()
|
|
vim.lsp.codelens.enable()
|
|
end)
|
|
|
|
screen:expect({ grid = grid_with_lenses })
|
|
end)
|
|
|
|
it('clears/shows code lenses when disabled/enabled', function()
|
|
exec_lua(function()
|
|
vim.lsp.codelens.enable(false)
|
|
end)
|
|
|
|
screen:expect({ grid = grid_without_lenses })
|
|
|
|
exec_lua(function()
|
|
vim.lsp.codelens.enable(true)
|
|
end)
|
|
|
|
screen:expect({ grid = grid_with_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({
|
|
{
|
|
client_id = 1,
|
|
lens = {
|
|
command = {
|
|
arguments = {},
|
|
command = 'rust-analyzer.showReferences',
|
|
title = '1 implementation',
|
|
},
|
|
range = {
|
|
['end'] = {
|
|
character = 8,
|
|
line = 0,
|
|
},
|
|
start = {
|
|
character = 7,
|
|
line = 0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
client_id = 1,
|
|
lens = {
|
|
command = {
|
|
arguments = {},
|
|
command = 'rust-analyzer.runSingle',
|
|
title = '▶︎ Run ',
|
|
},
|
|
range = {
|
|
['end'] = {
|
|
character = 7,
|
|
line = 11,
|
|
},
|
|
start = {
|
|
character = 3,
|
|
line = 11,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}, result)
|
|
end)
|
|
|
|
it('refreshes code lenses on request', function()
|
|
local get_message_count = function()
|
|
local messages = exec_lua('return _G.server.messages')
|
|
local count = 0
|
|
for _, m in ipairs(messages) do
|
|
if m.method == 'textDocument/codeLens' then
|
|
count = count + 1
|
|
end
|
|
end
|
|
return count
|
|
end
|
|
|
|
eq(2, get_message_count())
|
|
|
|
feed('ggdd')
|
|
|
|
-- Second codelens (▶︎ Run) is not resolved immediately, so its previous extmark (same row) isn't
|
|
-- cleared and it stays anchored to where it was. The new one (which would effectively look like
|
|
-- it moved a row down) needs to be resolved first, which goes through another screen update.
|
|
-- This is a quirk of the test since it returns the exact same result no matter what the text in
|
|
-- the buffer actually is
|
|
|
|
screen:expect([[
|
|
{1: 1 implementation} |
|
|
^a: i32, |
|
|
b: String, |
|
|
} |
|
|
|
|
|
impl S { |
|
|
fn new(a: i32, b: String) -> Self { |
|
|
S { a, b } |
|
|
} |
|
|
} |
|
|
|
|
|
{1: ▶︎ Run } |
|
|
fn main() { |
|
|
let s = S::new(42, String::from("Hello, world!"))|
|
|
; |
|
|
println!("S.a: {}, S.b: {}", s.a, s.b); |
|
|
} |
|
|
|
|
|
{1:~ }|
|
|
|
|
|
]])
|
|
|
|
eq(2, get_message_count())
|
|
|
|
screen:expect([[
|
|
{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:~ }|
|
|
|
|
|
]])
|
|
|
|
eq(3, get_message_count())
|
|
|
|
exec_lua(function()
|
|
vim.lsp.codelens.on_refresh(
|
|
nil,
|
|
nil,
|
|
{ method = 'workspace/codeLens/refresh', client_id = client_id }
|
|
)
|
|
end)
|
|
|
|
eq(4, get_message_count())
|
|
end)
|
|
|
|
it('ignores stale codeLens/resolve responses', function()
|
|
clear_notrace()
|
|
exec_lua(create_server_definition)
|
|
|
|
insert('line1\nline2\n')
|
|
|
|
exec_lua(function()
|
|
local codelens_request_count = 0
|
|
_G.stale_resolve_sent = false
|
|
_G.server = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
|
|
codeLensProvider = {
|
|
resolveProvider = true,
|
|
},
|
|
},
|
|
handlers = {
|
|
['textDocument/codeLens'] = function(_, _, callback)
|
|
codelens_request_count = codelens_request_count + 1
|
|
if codelens_request_count == 1 then
|
|
callback(nil, {
|
|
{
|
|
range = {
|
|
['end'] = {
|
|
character = 1,
|
|
line = 0,
|
|
},
|
|
start = {
|
|
character = 0,
|
|
line = 0,
|
|
},
|
|
},
|
|
},
|
|
})
|
|
else
|
|
callback(nil, {})
|
|
end
|
|
end,
|
|
['codeLens/resolve'] = function(_, lens, callback)
|
|
vim.defer_fn(function()
|
|
_G.stale_resolve_sent = true
|
|
callback(nil, {
|
|
command = {
|
|
arguments = {},
|
|
command = 'dummy.command',
|
|
title = 'resolved',
|
|
},
|
|
range = lens.range,
|
|
})
|
|
end, 100)
|
|
end,
|
|
},
|
|
})
|
|
|
|
local stale_client_id = vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })
|
|
vim.lsp.codelens.enable()
|
|
vim.wait(1000, function()
|
|
return #vim.lsp.codelens.get() > 0
|
|
end)
|
|
|
|
vim.api.nvim__redraw({ flush = true })
|
|
|
|
vim.lsp.codelens.on_refresh(nil, nil, {
|
|
method = 'workspace/codeLens/refresh',
|
|
client_id = stale_client_id,
|
|
})
|
|
|
|
assert(
|
|
vim.wait(1000, function()
|
|
return _G.stale_resolve_sent
|
|
end),
|
|
'timed out waiting for stale resolve response'
|
|
)
|
|
end)
|
|
|
|
eq('', api.nvim_get_vvar('errmsg'))
|
|
end)
|
|
|
|
it('ignores refresh responses for deleted buffer', function()
|
|
clear_notrace()
|
|
exec_lua(create_server_definition)
|
|
|
|
insert('line1\n')
|
|
|
|
exec_lua(function()
|
|
local codelens_request_count = 0
|
|
_G.refresh_response_sent = false
|
|
_G.server = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
|
|
codeLensProvider = {
|
|
resolveProvider = true,
|
|
},
|
|
},
|
|
handlers = {
|
|
['textDocument/codeLens'] = function(_, _, callback)
|
|
codelens_request_count = codelens_request_count + 1
|
|
|
|
local lenses = {
|
|
{
|
|
command = {
|
|
arguments = {},
|
|
command = 'dummy.command',
|
|
title = 'Lens',
|
|
},
|
|
range = {
|
|
['end'] = {
|
|
character = 1,
|
|
line = 0,
|
|
},
|
|
start = {
|
|
character = 0,
|
|
line = 0,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
if codelens_request_count == 1 then
|
|
callback(nil, lenses)
|
|
else
|
|
-- Delay the refresh response so the buffer is wiped before it arrives.
|
|
vim.schedule(function()
|
|
_G.refresh_response_sent = true
|
|
callback(nil, lenses)
|
|
end)
|
|
end
|
|
end,
|
|
},
|
|
})
|
|
|
|
local client_id = vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })
|
|
vim.lsp.codelens.enable()
|
|
|
|
assert(
|
|
vim.wait(1000, function()
|
|
return #vim.lsp.codelens.get() > 0
|
|
end),
|
|
'timed out waiting for initial codelens response'
|
|
)
|
|
|
|
vim.lsp.codelens.on_refresh(nil, nil, {
|
|
method = 'workspace/codeLens/refresh',
|
|
client_id = client_id,
|
|
})
|
|
vim.cmd.bwipeout({ bang = true })
|
|
|
|
assert(
|
|
vim.wait(1000, function()
|
|
return _G.refresh_response_sent
|
|
end),
|
|
'timed out waiting for refresh response'
|
|
)
|
|
end)
|
|
|
|
eq('', api.nvim_get_vvar('errmsg'))
|
|
end)
|
|
|
|
it('clears extmarks beyond the bottom of the buffer', function()
|
|
feed('12G4dd')
|
|
screen:expect([[
|
|
{1: 1 implementation} |
|
|
struct S { |
|
|
a: i32, |
|
|
b: String, |
|
|
} |
|
|
|
|
|
impl S { |
|
|
fn new(a: i32, b: String) -> Self { |
|
|
S { a, b } |
|
|
} |
|
|
} |
|
|
|
|
|
{1:▶︎ Run } |
|
|
^ |
|
|
{1:~ }|*5
|
|
4 fewer lines |
|
|
]])
|
|
|
|
feed('dd')
|
|
screen:expect([[
|
|
{1: 1 implementation} |
|
|
struct S { |
|
|
a: i32, |
|
|
b: String, |
|
|
} |
|
|
|
|
|
impl S { |
|
|
fn new(a: i32, b: String) -> Self { |
|
|
S { a, b } |
|
|
} |
|
|
} |
|
|
^ |
|
|
{1:~ }|*7
|
|
4 fewer lines |
|
|
]])
|
|
end)
|
|
|
|
after_each(function()
|
|
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
|
|
end)
|
|
end)
|