feat(lsp): convert inlay_hint to capability framework #40569

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.
This commit is contained in:
jdrouhard
2026-07-05 11:47:30 -05:00
committed by GitHub
parent e494c4777b
commit 1f18ea1cf7
6 changed files with 363 additions and 351 deletions

View File

@@ -92,6 +92,7 @@ int main() {
client_id = exec_lua(function()
_G.server = _G._create_server({
capabilities = {
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
inlayHintProvider = true,
},
handlers = {
@@ -126,6 +127,7 @@ int main() {
local client_id2 = exec_lua(function()
_G.server2 = _G._create_server({
capabilities = {
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
inlayHintProvider = true,
},
handlers = {
@@ -134,9 +136,7 @@ int main() {
end,
},
})
local client_id2 = vim.lsp.start({ name = 'dummy2', cmd = _G.server2.cmd })
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
return client_id2
return vim.lsp.start({ name = 'dummy2', cmd = _G.server2.cmd })
end)
exec_lua(function()
@@ -169,67 +169,67 @@ int main() {
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)
exec_lua(function()
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr2 })
end)
n.api.nvim_win_set_buf(0, bufnr)
screen:expect({ grid = grid_with_inlay_hints })
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)
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 })
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 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 })
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)
@@ -249,6 +249,7 @@ int main() {
exec_lua(function()
_G.server2 = _G._create_server({
capabilities = {
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
inlayHintProvider = true,
},
handlers = {
@@ -258,7 +259,6 @@ int main() {
},
})
_G.client2 = vim.lsp.start({ name = 'dummy2', cmd = _G.server2.cmd })
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
end)
--- @type vim.lsp.inlay_hint.get.ret
@@ -313,50 +313,50 @@ int main() {
end)
)
end)
end)
it('does not request hints from lsp when disabled', function()
exec_lua(function()
_G.server2 = _G._create_server({
capabilities = {
inlayHintProvider = true,
},
handlers = {
['textDocument/inlayHint'] = function(_, _, callback)
_G.got_inlay_hint_request = true
callback(nil, {})
end,
},
})
_G.client2 = vim.lsp.start({ name = 'dummy2', cmd = _G.server2.cmd })
end)
local function was_request_sent()
return exec_lua(function()
return _G.got_inlay_hint_request == true
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(false, { bufnr = bufnr })
vim.lsp.inlay_hint.get()
end)
eq(false, was_request_sent())
exec_lua(function()
vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })
end)
eq(true, was_request_sent())
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)
@@ -403,6 +403,7 @@ test text
client_id = exec_lua(function()
_G.server = _G._create_server({
capabilities = {
textDocumentSync = vim.lsp.protocol.TextDocumentSyncKind.Full,
inlayHintProvider = true,
},
handlers = {