mirror of
https://github.com/neovim/neovim.git
synced 2026-07-24 18:00:46 +00:00
Problem: Inlay hints used separate global and per-buffer bufstates tables and bespoke global autocmds for managing the inlay hint state across buffers and clients, duplicating the lifecycle logic already provided by the Capability framework. This caused inconsistencies in how client state was handled and inlay hint state lifecycle was managed compared to other LSP features. Solution: Replace the ad-hoc bufstate tracking and global autocmds in vim.lsp.inlay_hint with a proper InlayHint subclass of Capability. This also refactors the way inlay hint state is managed and fixes bugs I found while doing this: 1. For each line with inlay hints, the list of the hints along with whether they have been applied is stored in a current result on the client state. This allows the on_win decorator to clear all inlay hints for an old document version once, and then re-add the new version's hints line-by-line as they are drawn to the screen, modeling the semantic tokens module. 2. It fixes problems with mixing results from multiple clients attached to the buffer by fully moving each client's state to its own table. Previously, only the most recent document version used to populate a line's inlay hints was stored, but there was no distinction for which client the hints may have come from. (Fixes #36318) 3. It fixes the workspace/inlayHint/refresh server->client notification behavior. Previously it would only re-request inlay hints for buffers currently displayed in a window but would not invalidate them in non-displayed buffers (or provide any mechanism for those buffers to re-request at a later time). Model semantic token module here again by invalidating all buffers, and adding a BufWinEnter autocmd to refresh hints. 4. Add a mechanism to cancel in-flight requests if a new request for a newer document version is made before the last one returned 5. Handle stale results by simply dropping them.
457 lines
13 KiB
Lua
457 lines
13 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 pcall_err = t.pcall_err
|
|
local dedent = t.dedent
|
|
local exec_lua = n.exec_lua
|
|
local insert = n.insert
|
|
local feed = n.feed
|
|
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);
|
|
}
|
|
}]])
|
|
|
|
---@type lsp.InlayHint[]
|
|
local response = {
|
|
{
|
|
kind = 1,
|
|
paddingLeft = false,
|
|
paddingRight = false,
|
|
label = '-> int',
|
|
position = { character = 22, line = 0 },
|
|
},
|
|
{
|
|
kind = 2,
|
|
paddingLeft = false,
|
|
paddingRight = true,
|
|
label = 'a:',
|
|
position = { character = 15, line = 5 },
|
|
},
|
|
{
|
|
kind = 2,
|
|
paddingLeft = false,
|
|
paddingRight = true,
|
|
label = 'b:',
|
|
position = { character = 17, line = 5 },
|
|
},
|
|
}
|
|
|
|
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
|
|
|
|
--- @type integer
|
|
local client_id
|
|
|
|
--- @type integer
|
|
local bufnr
|
|
|
|
before_each(function()
|
|
clear_notrace()
|
|
screen = Screen.new(50, 9)
|
|
|
|
bufnr = n.api.nvim_get_current_buf()
|
|
exec_lua(create_server_definition)
|
|
client_id = exec_lua(function()
|
|
_G.server = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
|
|
inlayHintProvider = true,
|
|
},
|
|
handlers = {
|
|
['textDocument/inlayHint'] = function(_, _, callback)
|
|
callback(nil, response)
|
|
end,
|
|
},
|
|
})
|
|
|
|
return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })
|
|
end)
|
|
|
|
insert(text)
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
|
|
end)
|
|
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(function()
|
|
vim.lsp.get_client_by_id(client_id):stop()
|
|
end)
|
|
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
|
|
end)
|
|
|
|
it('does not clear inlay hints when one of several clients detaches', function()
|
|
local client_id2 = exec_lua(function()
|
|
_G.server2 = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
|
|
inlayHintProvider = true,
|
|
},
|
|
handlers = {
|
|
['textDocument/inlayHint'] = function(_, _, callback)
|
|
callback(nil, {})
|
|
end,
|
|
},
|
|
})
|
|
return vim.lsp.start({ name = 'dummy2', cmd = _G.server2.cmd })
|
|
end)
|
|
|
|
exec_lua(function()
|
|
vim.lsp.get_client_by_id(client_id2):stop()
|
|
end)
|
|
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, function()
|
|
--- @diagnostic disable-next-line:param-type-mismatch
|
|
vim.lsp.inlay_hint.enable({}, { bufnr = bufnr })
|
|
end)
|
|
)
|
|
t.matches(
|
|
'enable: expected boolean, got number',
|
|
t.pcall_err(exec_lua, function()
|
|
--- @diagnostic disable-next-line:param-type-mismatch
|
|
vim.lsp.inlay_hint.enable(42)
|
|
end)
|
|
)
|
|
t.matches(
|
|
'filter: expected table, got number',
|
|
t.pcall_err(exec_lua, function()
|
|
--- @diagnostic disable-next-line:param-type-mismatch
|
|
vim.lsp.inlay_hint.enable(true, 42)
|
|
end)
|
|
)
|
|
end)
|
|
end)
|
|
|
|
describe('clears/applies inlay hints when passed false/true/nil', function()
|
|
local bufnr2 --- @type integer
|
|
before_each(function()
|
|
bufnr2 = exec_lua(function()
|
|
local bufnr2_0 = vim.api.nvim_create_buf(true, false)
|
|
vim.lsp.buf_attach_client(bufnr2_0, client_id)
|
|
vim.api.nvim_win_set_buf(0, bufnr2_0)
|
|
return bufnr2_0
|
|
end)
|
|
insert(text)
|
|
screen:expect({ grid = grid_without_inlay_hints })
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr2 })
|
|
end)
|
|
screen:expect({ grid = grid_with_inlay_hints })
|
|
end)
|
|
|
|
it('for one single buffer', function()
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(false, { bufnr = bufnr })
|
|
vim.api.nvim_win_set_buf(0, bufnr2)
|
|
end)
|
|
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
|
|
n.api.nvim_win_set_buf(0, bufnr)
|
|
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
|
|
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
|
|
end)
|
|
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
|
|
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(
|
|
not vim.lsp.inlay_hint.is_enabled({ bufnr = bufnr }),
|
|
{ bufnr = bufnr }
|
|
)
|
|
end)
|
|
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
|
|
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
|
|
end)
|
|
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
|
|
end)
|
|
|
|
it('for all buffers', function()
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(false)
|
|
end)
|
|
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
|
|
n.api.nvim_win_set_buf(0, bufnr2)
|
|
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
|
|
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(true)
|
|
end)
|
|
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
|
|
n.api.nvim_win_set_buf(0, bufnr)
|
|
screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
|
|
end)
|
|
end)
|
|
|
|
describe('get()', function()
|
|
it('returns filtered inlay hints', function()
|
|
local expected2 = {
|
|
kind = 1,
|
|
paddingLeft = false,
|
|
label = ': int',
|
|
position = {
|
|
character = 10,
|
|
line = 2,
|
|
},
|
|
paddingRight = false,
|
|
}
|
|
|
|
exec_lua(function()
|
|
_G.server2 = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
|
|
inlayHintProvider = true,
|
|
},
|
|
handlers = {
|
|
['textDocument/inlayHint'] = function(_, _, callback)
|
|
callback(nil, { expected2 })
|
|
end,
|
|
},
|
|
})
|
|
_G.client2 = vim.lsp.start({ name = 'dummy2', cmd = _G.server2.cmd })
|
|
end)
|
|
|
|
--- @type vim.lsp.inlay_hint.get.ret
|
|
eq(
|
|
{
|
|
{ bufnr = 1, client_id = 1, inlay_hint = response[1] },
|
|
{ bufnr = 1, client_id = 1, inlay_hint = response[2] },
|
|
{ bufnr = 1, client_id = 1, inlay_hint = response[3] },
|
|
{ bufnr = 1, client_id = 2, inlay_hint = expected2 },
|
|
},
|
|
exec_lua(function()
|
|
return vim.lsp.inlay_hint.get()
|
|
end)
|
|
)
|
|
|
|
eq(
|
|
{
|
|
{ bufnr = 1, client_id = 2, inlay_hint = expected2 },
|
|
},
|
|
exec_lua(function()
|
|
return vim.lsp.inlay_hint.get({
|
|
range = {
|
|
start = { line = 2, character = 10 },
|
|
['end'] = { line = 2, character = 10 },
|
|
},
|
|
})
|
|
end)
|
|
)
|
|
|
|
eq(
|
|
{
|
|
{ bufnr = 1, client_id = 1, inlay_hint = response[2] },
|
|
{ bufnr = 1, client_id = 1, inlay_hint = response[3] },
|
|
},
|
|
exec_lua(function()
|
|
return vim.lsp.inlay_hint.get({
|
|
bufnr = vim.api.nvim_get_current_buf(),
|
|
range = {
|
|
start = { line = 4, character = 18 },
|
|
['end'] = { line = 5, character = 17 },
|
|
},
|
|
})
|
|
end)
|
|
)
|
|
|
|
eq(
|
|
{},
|
|
exec_lua(function()
|
|
return vim.lsp.inlay_hint.get({
|
|
bufnr = vim.api.nvim_get_current_buf() + 1,
|
|
})
|
|
end)
|
|
)
|
|
end)
|
|
end)
|
|
|
|
it('does not request hints from lsp when disabled', function()
|
|
local client_id2 = exec_lua(function()
|
|
_G.server2 = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
|
|
inlayHintProvider = true,
|
|
},
|
|
handlers = {
|
|
['textDocument/inlayHint'] = function(_, _, callback)
|
|
_G.got_inlay_hint_request = true
|
|
callback(nil, {})
|
|
end,
|
|
},
|
|
})
|
|
return vim.lsp.start({
|
|
name = 'dummy2',
|
|
cmd = _G.server2.cmd,
|
|
on_attach = function(client, _)
|
|
vim.lsp.inlay_hint.enable(false, { client_id = client.id })
|
|
end,
|
|
})
|
|
end)
|
|
|
|
local function was_request_sent()
|
|
return exec_lua(function()
|
|
return _G.got_inlay_hint_request or false
|
|
end)
|
|
end
|
|
|
|
eq(false, was_request_sent())
|
|
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.get()
|
|
end)
|
|
|
|
eq(false, was_request_sent())
|
|
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.enable(true, { client_id = client_id2 })
|
|
end)
|
|
|
|
eq(true, was_request_sent())
|
|
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
|
|
|
|
--- @type integer
|
|
local client_id
|
|
|
|
--- @type integer
|
|
local bufnr
|
|
|
|
before_each(function()
|
|
clear_notrace()
|
|
screen = Screen.new(50, 3)
|
|
|
|
exec_lua(create_server_definition)
|
|
bufnr = n.api.nvim_get_current_buf()
|
|
client_id = exec_lua(function()
|
|
_G.server = _G._create_server({
|
|
capabilities = {
|
|
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
|
|
inlayHintProvider = true,
|
|
},
|
|
handlers = {
|
|
['textDocument/inlayHint'] = function(_, _, callback)
|
|
callback(nil, response)
|
|
end,
|
|
},
|
|
})
|
|
|
|
vim.api.nvim_win_set_buf(0, bufnr)
|
|
|
|
return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd })
|
|
end)
|
|
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(function()
|
|
vim.lsp.get_client_by_id(client_id):stop()
|
|
end)
|
|
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
|
|
end)
|
|
|
|
it('refreshes hints on request', function()
|
|
exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]])
|
|
screen:expect({ grid = grid_with_inlay_hints })
|
|
feed('kibefore <Esc>')
|
|
screen:expect([[
|
|
before^ {1:01234}test text |
|
|
|*2
|
|
]])
|
|
exec_lua(function()
|
|
vim.lsp.inlay_hint.on_refresh(
|
|
nil,
|
|
nil,
|
|
{ method = 'workspace/inlayHint/refresh', client_id = client_id }
|
|
)
|
|
end)
|
|
screen:expect([[
|
|
{1:01234}before^ test text |
|
|
|*2
|
|
]])
|
|
end)
|
|
|
|
after_each(function()
|
|
api.nvim_exec_autocmds('VimLeavePre', { modeline = false })
|
|
end)
|
|
end)
|