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.
This commit is contained in:
Amit Singh 2024-07-17 20:14:53 +05:30 committed by GitHub
parent 0500804df5
commit e29f245a10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 107 additions and 29 deletions

View File

@ -336,6 +336,8 @@ api.nvim_set_decoration_provider(namespace, {
for lnum = topline, botline do for lnum = topline, botline do
if bufstate.applied[lnum] ~= bufstate.version then if bufstate.applied[lnum] ~= bufstate.version then
api.nvim_buf_clear_namespace(bufnr, namespace, lnum, lnum + 1) api.nvim_buf_clear_namespace(bufnr, namespace, lnum, lnum + 1)
local hint_virtual_texts = {} --- @type table<integer, [string, string?][]>
for _, lnum_hints in pairs(client_hints) do for _, lnum_hints in pairs(client_hints) do
local hints = lnum_hints[lnum] or {} local hints = lnum_hints[lnum] or {}
for _, hint in pairs(hints) do for _, hint in pairs(hints) do
@ -348,7 +350,7 @@ api.nvim_set_decoration_provider(namespace, {
text = text .. part.value text = text .. part.value
end end
end end
local vt = {} --- @type [string, string?][] local vt = hint_virtual_texts[hint.position.character] or {}
if hint.paddingLeft then if hint.paddingLeft then
vt[#vt + 1] = { ' ' } vt[#vt + 1] = { ' ' }
end end
@ -356,13 +358,18 @@ api.nvim_set_decoration_provider(namespace, {
if hint.paddingRight then if hint.paddingRight then
vt[#vt + 1] = { ' ' } vt[#vt + 1] = { ' ' }
end end
api.nvim_buf_set_extmark(bufnr, namespace, lnum, hint.position.character, { hint_virtual_texts[hint.position.character] = vt
virt_text_pos = 'inline',
ephemeral = false,
virt_text = vt,
})
end end
end end
for pos, vt in pairs(hint_virtual_texts) do
api.nvim_buf_set_extmark(bufnr, namespace, lnum, pos, {
virt_text_pos = 'inline',
ephemeral = false,
virt_text = vt,
})
end
bufstate.applied[lnum] = bufstate.version bufstate.applied[lnum] = bufstate.version
end end
end end

View File

@ -12,7 +12,8 @@ local api = n.api
local clear_notrace = t_lsp.clear_notrace local clear_notrace = t_lsp.clear_notrace
local create_server_definition = t_lsp.create_server_definition local create_server_definition = t_lsp.create_server_definition
local text = dedent([[ describe('vim.lsp.inlay_hint', function()
local text = dedent([[
auto add(int a, int b) { return a + b; } auto add(int a, int b) { return a + b; }
int main() { int main() {
@ -22,7 +23,7 @@ int main() {
} }
}]]) }]])
local response = [==[ local response = [==[
[ [
{"kind":1,"paddingLeft":false,"label":"-> int","position":{"character":22,"line":0},"paddingRight":false}, {"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":"a:","position":{"character":15,"line":5},"paddingRight":true},
@ -30,7 +31,7 @@ local response = [==[
] ]
]==] ]==]
local grid_without_inlay_hints = [[ local grid_without_inlay_hints = [[
auto add(int a, int b) { return a + b; } | auto add(int a, int b) { return a + b; } |
| |
int main() { | int main() { |
@ -42,7 +43,7 @@ local grid_without_inlay_hints = [[
| |
]] ]]
local grid_with_inlay_hints = [[ local grid_with_inlay_hints = [[
auto add(int a, int b){1:-> int} { return a + b; } | auto add(int a, int b){1:-> int} { return a + b; } |
| |
int main() { | int main() { |
@ -54,16 +55,16 @@ local grid_with_inlay_hints = [[
| |
]] ]]
--- @type test.functional.ui.screen --- @type test.functional.ui.screen
local screen local screen
before_each(function() before_each(function()
clear_notrace() clear_notrace()
screen = Screen.new(50, 9) screen = Screen.new(50, 9)
screen:attach() screen:attach()
exec_lua(create_server_definition) exec_lua(create_server_definition)
exec_lua( exec_lua(
[[ [[
local response = ... local response = ...
server = _create_server({ server = _create_server({
capabilities = { capabilities = {
@ -81,19 +82,18 @@ before_each(function()
client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd }) client_id = vim.lsp.start({ name = 'dummy', cmd = server.cmd })
]], ]],
response response
) )
insert(text) insert(text)
exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]]) exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]])
screen:expect({ grid = grid_with_inlay_hints }) screen:expect({ grid = grid_with_inlay_hints })
end) end)
after_each(function() after_each(function()
api.nvim_exec_autocmds('VimLeavePre', { modeline = false }) api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
end) end)
describe('vim.lsp.inlay_hint', function()
it('clears inlay hints when sole client detaches', function() it('clears inlay hints when sole client detaches', function()
exec_lua([[vim.lsp.stop_client(client_id)]]) exec_lua([[vim.lsp.stop_client(client_id)]])
screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
@ -258,3 +258,74 @@ describe('vim.lsp.inlay_hint', function()
end) end)
end) 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)