neovim/test/functional/plugin/lsp/inlay_hint_spec.lua
Amit Singh e29f245a10
fix(lsp): inlay hints are rendered in the correct order (#29707)
Problem:
When there are multiple inlay hints present at the same position, they
should be rendered in the order they are received in the response from
LSP as per the LSP spec. Currently, this is not respected.

Solution:
Gather all hints for a given position, and then set it in a single
extmark call instead of multiple set_extmark calls. This leads to fewer
extmark calls and correct inlay hints being rendered.
2024-07-17 16:44:53 +02:00

332 lines
10 KiB
Lua

local t = require('test.testutil')
local n = require('test.functional.testnvim')()
local Screen = require('test.functional.ui.screen')
local t_lsp = require('test.functional.plugin.lsp.testutil')
local eq = t.eq
local dedent = t.dedent
local exec_lua = n.exec_lua
local insert = n.insert
local api = n.api
local clear_notrace = t_lsp.clear_notrace
local create_server_definition = t_lsp.create_server_definition
describe('vim.lsp.inlay_hint', function()
local text = dedent([[
auto add(int a, int b) { return a + b; }
int main() {
int x = 1;
int y = 2;
return add(x,y);
}
}]])
local response = [==[
[
{"kind":1,"paddingLeft":false,"label":"-> int","position":{"character":22,"line":0},"paddingRight":false},
{"kind":2,"paddingLeft":false,"label":"a:","position":{"character":15,"line":5},"paddingRight":true},
{"kind":2,"paddingLeft":false,"label":"b:","position":{"character":17,"line":5},"paddingRight":true}
]
]==]
local grid_without_inlay_hints = [[
auto add(int a, int b) { return a + b; } |
|
int main() { |
int x = 1; |
int y = 2; |
return add(x,y); |
} |
^} |
|
]]
local grid_with_inlay_hints = [[
auto add(int a, int b){1:-> int} { return a + b; } |
|
int main() { |
int x = 1; |
int y = 2; |
return add({1:a:} x,{1:b:} y); |
} |
^} |
|
]]
--- @type test.functional.ui.screen
local screen
before_each(function()
clear_notrace()
screen = Screen.new(50, 9)
screen:attach()
exec_lua(create_server_definition)
exec_lua(
[[
local response = ...
server = _create_server({
capabilities = {
inlayHintProvider = true,
},
handlers = {
['textDocument/inlayHint'] = function(_, _, callback)
callback(nil, vim.json.decode(response))
end,
}
})
bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_win_set_buf(0, bufnr)
client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
]],
response
)
insert(text)
exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]])
screen:expect({ grid = grid_with_inlay_hints })
end)
after_each(function()
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
end)
it('clears inlay hints when sole client detaches', function()
exec_lua([[vim.lsp.stop_client(client_id)]])
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
end)
it('does not clear inlay hints when one of several clients detaches', function()
exec_lua([[
server2 = _create_server({
capabilities = {
inlayHintProvider = true,
},
handlers = {
['textDocument/inlayHint'] = function(_, _, callback)
callback(nil, {})
end,
}
})
client2 = vim.lsp.start({ name = 'dummy2', cmd = server2.cmd })
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
]])
exec_lua([[ vim.lsp.stop_client(client2) ]])
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
end)
describe('enable()', function()
it('validation', function()
t.matches(
'enable: expected boolean, got table',
t.pcall_err(exec_lua, [[vim.lsp.inlay_hint.enable({}, { bufnr = bufnr })]])
)
t.matches(
'enable: expected boolean, got number',
t.pcall_err(exec_lua, [[vim.lsp.inlay_hint.enable(42)]])
)
t.matches(
'filter: expected table, got number',
t.pcall_err(exec_lua, [[vim.lsp.inlay_hint.enable(true, 42)]])
)
end)
describe('clears/applies inlay hints when passed false/true/nil', function()
before_each(function()
exec_lua([[
bufnr2 = vim.api.nvim_create_buf(true, false)
vim.lsp.buf_attach_client(bufnr2, client_id)
vim.api.nvim_win_set_buf(0, bufnr2)
]])
insert(text)
exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr2 })]])
exec_lua([[vim.api.nvim_win_set_buf(0, bufnr)]])
screen:expect({ grid = grid_with_inlay_hints })
end)
it('for one single buffer', function()
exec_lua([[
vim.lsp.inlay_hint.enable(false, { bufnr = bufnr })
vim.api.nvim_win_set_buf(0, bufnr2)
]])
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
exec_lua([[vim.api.nvim_win_set_buf(0, bufnr)]])
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]])
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
exec_lua(
[[vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }), { bufnr = bufnr })]]
)
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]])
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
end)
it('for all buffers', function()
exec_lua([[vim.lsp.inlay_hint.enable(false)]])
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
exec_lua([[vim.api.nvim_win_set_buf(0, bufnr2)]])
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
exec_lua([[vim.lsp.inlay_hint.enable(true)]])
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
exec_lua([[vim.api.nvim_win_set_buf(0, bufnr)]])
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
end)
end)
end)
describe('get()', function()
it('returns filtered inlay hints', function()
--- @type lsp.InlayHint[]
local expected = vim.json.decode(response)
local expected2 = {
kind = 1,
paddingLeft = false,
label = ': int',
position = {
character = 10,
line = 2,
},
paddingRight = false,
}
exec_lua(
[[
local expected2 = ...
server2 = _create_server({
capabilities = {
inlayHintProvider = true,
},
handlers = {
['textDocument/inlayHint'] = function(_, _, callback)
callback(nil, { expected2 })
end,
}
})
client2 = vim.lsp.start({ name = 'dummy2', cmd = server2.cmd })
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
]],
expected2
)
--- @type vim.lsp.inlay_hint.get.ret
local res = exec_lua([[return vim.lsp.inlay_hint.get()]])
eq({
{ bufnr = 1, client_id = 1, inlay_hint = expected[1] },
{ bufnr = 1, client_id = 1, inlay_hint = expected[2] },
{ bufnr = 1, client_id = 1, inlay_hint = expected[3] },
{ bufnr = 1, client_id = 2, inlay_hint = expected2 },
}, res)
--- @type vim.lsp.inlay_hint.get.ret
res = exec_lua([[return vim.lsp.inlay_hint.get({
range = {
start = { line = 2, character = 10 },
["end"] = { line = 2, character = 10 },
},
})]])
eq({
{ bufnr = 1, client_id = 2, inlay_hint = expected2 },
}, res)
--- @type vim.lsp.inlay_hint.get.ret
res = exec_lua([[return vim.lsp.inlay_hint.get({
bufnr = vim.api.nvim_get_current_buf(),
range = {
start = { line = 4, character = 18 },
["end"] = { line = 5, character = 17 },
},
})]])
eq({
{ bufnr = 1, client_id = 1, inlay_hint = expected[2] },
{ bufnr = 1, client_id = 1, inlay_hint = expected[3] },
}, res)
--- @type vim.lsp.inlay_hint.get.ret
res = exec_lua([[return vim.lsp.inlay_hint.get({
bufnr = vim.api.nvim_get_current_buf() + 1,
})]])
eq({}, res)
end)
end)
end)
describe('Inlay hints handler', function()
local text = dedent([[
test text
]])
local response = [==[
[
{ "position": { "line": 0, "character": 0 }, "label": "0" },
{ "position": { "line": 0, "character": 0 }, "label": "1" },
{ "position": { "line": 0, "character": 0 }, "label": "2" },
{ "position": { "line": 0, "character": 0 }, "label": "3" },
{ "position": { "line": 0, "character": 0 }, "label": "4" }
]
]==]
local grid_without_inlay_hints = [[
test text |
^ |
|
]]
local grid_with_inlay_hints = [[
{1:01234}test text |
^ |
|
]]
--- @type test.functional.ui.screen
local screen
before_each(function()
clear_notrace()
screen = Screen.new(50, 3)
screen:attach()
exec_lua(create_server_definition)
exec_lua(
[[
local response = ...
server = _create_server({
capabilities = {
inlayHintProvider = true,
},
handlers = {
['textDocument/inlayHint'] = function(_, _, callback)
callback(nil, vim.json.decode(response))
end,
}
})
bufnr = vim.api.nvim_get_current_buf()
vim.api.nvim_win_set_buf(0, bufnr)
client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
]],
response
)
insert(text)
end)
it('renders hints with same position in received order', function()
exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]])
screen:expect({ grid = grid_with_inlay_hints })
exec_lua([[vim.lsp.stop_client(client_id)]])
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
end)
after_each(function()
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
end)
end)