feat(lsp): vim.lsp.inlay_hint.enable(nil) applies to all buffers #28543

Problem:
Inlay hints `enable()` does not fully implement the `:help dev-lua` guidelines:

    Interface conventions ~
    - When accepting a buffer id, etc., 0 means "current buffer", nil means "all
      buffers".  Likewise for window id, tabpage id, etc.
      - Examples: |vim.lsp.codelens.clear()| |vim.diagnostic.enable()|

Solution:
Implement globally enabling inlay hints.
* refactor(lsp): do not rely on `enable` to create autocmds
* refactor(lsp): make `bufstates` a defaulttable
* refactor(lsp): make `bufstate` inherit values from `globalstate`
* feat(lsp): `vim.lsp.inlay_hints` now take effect on all buffers by default
* test(lsp): add basic tests for enable inlay hints for all buffers
* test(lsp): add test cases cover more than one buffer
This commit is contained in:
Yi Ming 2024-05-02 21:16:20 +08:00 committed by GitHub
parent 2becec289c
commit d5063f4b29
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 140 additions and 79 deletions

View File

@ -1611,8 +1611,8 @@ enable({enable}, {filter}) *vim.lsp.inlay_hint.enable()*
Parameters: ~ Parameters: ~
• {enable} (`boolean?`) true/nil to enable, false to disable • {enable} (`boolean?`) true/nil to enable, false to disable
• {filter} (`table?`) Optional filters |kwargs|, or `nil` for all. • {filter} (`table?`) Optional filters |kwargs|, or `nil` for all.
• {bufnr} (`integer?`) Buffer number, or 0/nil for current • {bufnr} (`integer?`) Buffer number, or 0 for current
buffer. buffer, or nil for all.
get({filter}) *vim.lsp.inlay_hint.get()* get({filter}) *vim.lsp.inlay_hint.get()*
Get the list of inlay hints, (optionally) restricted by buffer or range. Get the list of inlay hints, (optionally) restricted by buffer or range.

View File

@ -148,6 +148,8 @@ BREAKING CHANGES IN HEAD *news-breaking-dev*
The following changes to UNRELEASED features were made during the development The following changes to UNRELEASED features were made during the development
cycle (Nvim HEAD, the "master" branch). cycle (Nvim HEAD, the "master" branch).
• `vim.lsp.inlay_hint.enable()` now take effect on all buffers by default.
• Removed `vim.treesitter.foldtext` as transparent foldtext is now supported • Removed `vim.treesitter.foldtext` as transparent foldtext is now supported
https://github.com/neovim/neovim/pull/20750 https://github.com/neovim/neovim/pull/20750

View File

@ -4,13 +4,26 @@ local ms = require('vim.lsp.protocol').Methods
local api = vim.api local api = vim.api
local M = {} local M = {}
---@class (private) vim.lsp.inlay_hint.bufstate ---@class (private) vim.lsp.inlay_hint.globalstate Global state for inlay hints
---@field enabled boolean Whether inlay hints are enabled for this scope
---@type vim.lsp.inlay_hint.globalstate
local globalstate = {
enabled = false,
}
---@class (private) vim.lsp.inlay_hint.bufstate: vim.lsp.inlay_hint.globalstate Buffer local state for inlay hints
---@field version? integer ---@field version? integer
---@field client_hints? table<integer, table<integer, lsp.InlayHint[]>> client_id -> (lnum -> hints) ---@field client_hints? table<integer, table<integer, lsp.InlayHint[]>> client_id -> (lnum -> hints)
---@field applied table<integer, integer> Last version of hints applied to this line ---@field applied table<integer, integer> Last version of hints applied to this line
---@field enabled boolean Whether inlay hints are enabled for this buffer
---@type table<integer, vim.lsp.inlay_hint.bufstate> ---@type table<integer, vim.lsp.inlay_hint.bufstate>
local bufstates = {} local bufstates = vim.defaulttable(function(_)
return setmetatable({ applied = {} }, {
__index = globalstate,
__newindex = function(state, key, value)
rawset(state, key, (globalstate[key] ~= value) and value or nil)
end,
})
end)
local namespace = api.nvim_create_namespace('vim_lsp_inlayhint') local namespace = api.nvim_create_namespace('vim_lsp_inlayhint')
local augroup = api.nvim_create_augroup('vim_lsp_inlayhint', {}) local augroup = api.nvim_create_augroup('vim_lsp_inlayhint', {})
@ -34,7 +47,7 @@ function M.on_inlayhint(err, result, ctx, _)
return return
end end
local bufstate = bufstates[bufnr] local bufstate = bufstates[bufnr]
if not bufstate or not bufstate.enabled then if not bufstate.enabled then
return return
end end
if not (bufstate.client_hints and bufstate.version) then if not (bufstate.client_hints and bufstate.version) then
@ -91,11 +104,7 @@ function M.on_refresh(err, _, ctx, _)
for _, bufnr in ipairs(vim.lsp.get_buffers_by_client_id(ctx.client_id)) do for _, bufnr in ipairs(vim.lsp.get_buffers_by_client_id(ctx.client_id)) do
for _, winid in ipairs(api.nvim_list_wins()) do for _, winid in ipairs(api.nvim_list_wins()) do
if api.nvim_win_get_buf(winid) == bufnr then if api.nvim_win_get_buf(winid) == bufnr then
local bufstate = bufstates[bufnr] util._refresh(ms.textDocument_inlayHint, { bufnr = bufnr })
if bufstate then
util._refresh(ms.textDocument_inlayHint, { bufnr = bufnr })
break
end
end end
end end
end end
@ -154,7 +163,7 @@ function M.get(filter)
end end
local bufstate = bufstates[bufnr] local bufstate = bufstates[bufnr]
if not (bufstate and bufstate.client_hints) then if not bufstate.client_hints then
return {} return {}
end end
@ -203,12 +212,9 @@ end
--- Clear inlay hints --- Clear inlay hints
---@param bufnr (integer) Buffer handle, or 0 for current ---@param bufnr (integer) Buffer handle, or 0 for current
local function clear(bufnr) local function clear(bufnr)
if bufnr == nil or bufnr == 0 then if bufnr == 0 then
bufnr = api.nvim_get_current_buf() bufnr = api.nvim_get_current_buf()
end end
if not bufstates[bufnr] then
return
end
local bufstate = bufstates[bufnr] local bufstate = bufstates[bufnr]
local client_lens = (bufstate or {}).client_hints or {} local client_lens = (bufstate or {}).client_hints or {}
local client_ids = vim.tbl_keys(client_lens) --- @type integer[] local client_ids = vim.tbl_keys(client_lens) --- @type integer[]
@ -222,15 +228,14 @@ local function clear(bufnr)
end end
--- Disable inlay hints for a buffer --- Disable inlay hints for a buffer
---@param bufnr (integer|nil) Buffer handle, or 0 or nil for current ---@param bufnr (integer) Buffer handle, or 0 for current
local function _disable(bufnr) local function _disable(bufnr)
if bufnr == nil or bufnr == 0 then if bufnr == 0 then
bufnr = api.nvim_get_current_buf() bufnr = api.nvim_get_current_buf()
end end
clear(bufnr) clear(bufnr)
if bufstates[bufnr] then bufstates[bufnr] = nil
bufstates[bufnr] = { enabled = false, applied = {} } bufstates[bufnr].enabled = false
end
end end
--- Refresh inlay hints, only if we have attached clients that support it --- Refresh inlay hints, only if we have attached clients that support it
@ -244,30 +249,38 @@ local function _refresh(bufnr, opts)
end end
--- Enable inlay hints for a buffer --- Enable inlay hints for a buffer
---@param bufnr (integer|nil) Buffer handle, or 0 or nil for current ---@param bufnr (integer) Buffer handle, or 0 for current
local function _enable(bufnr) local function _enable(bufnr)
if bufnr == nil or bufnr == 0 then if bufnr == 0 then
bufnr = api.nvim_get_current_buf() bufnr = api.nvim_get_current_buf()
end end
local bufstate = bufstates[bufnr] bufstates[bufnr] = nil
if not bufstate then bufstates[bufnr].enabled = true
bufstates[bufnr] = { applied = {}, enabled = true } _refresh(bufnr)
api.nvim_create_autocmd('LspNotify', { end
buffer = bufnr,
callback = function(opts) api.nvim_create_autocmd('LspNotify', {
if callback = function(args)
opts.data.method ~= ms.textDocument_didChange ---@type integer
and opts.data.method ~= ms.textDocument_didOpen local bufnr = args.buf
then
return if
end args.data.method ~= ms.textDocument_didChange
if bufstates[bufnr] and bufstates[bufnr].enabled then and args.data.method ~= ms.textDocument_didOpen
_refresh(bufnr, { client_id = opts.data.client_id }) then
end return
end, end
group = augroup, if bufstates[bufnr].enabled then
}) _refresh(bufnr, { client_id = args.data.client_id })
_refresh(bufnr) end
end,
group = augroup,
})
api.nvim_create_autocmd('LspAttach', {
callback = function(args)
---@type integer
local bufnr = args.buf
api.nvim_buf_attach(bufnr, false, { api.nvim_buf_attach(bufnr, false, {
on_reload = function(_, cb_bufnr) on_reload = function(_, cb_bufnr)
clear(cb_bufnr) clear(cb_bufnr)
@ -278,32 +291,30 @@ local function _enable(bufnr)
end, end,
on_detach = function(_, cb_bufnr) on_detach = function(_, cb_bufnr)
_disable(cb_bufnr) _disable(cb_bufnr)
bufstates[cb_bufnr] = nil
end, end,
}) })
api.nvim_create_autocmd('LspDetach', { end,
buffer = bufnr, group = augroup,
callback = function(args) })
local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_inlayHint }) api.nvim_create_autocmd('LspDetach', {
callback = function(args)
if ---@type integer
not vim.iter(clients):any(function(c) local bufnr = args.buf
return c.id ~= args.data.client_id local clients = vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_inlayHint })
end)
then
_disable(bufnr)
end
end,
group = augroup,
})
else
bufstate.enabled = true
_refresh(bufnr)
end
end
if not vim.iter(clients):any(function(c)
return c.id ~= args.data.client_id
end) then
_disable(bufnr)
end
end,
group = augroup,
})
api.nvim_set_decoration_provider(namespace, { api.nvim_set_decoration_provider(namespace, {
on_win = function(_, _, bufnr, topline, botline) on_win = function(_, _, bufnr, topline, botline)
local bufstate = bufstates[bufnr] ---@type vim.lsp.inlay_hint.bufstate
local bufstate = rawget(bufstates, bufnr)
if not bufstate then if not bufstate then
return return
end end
@ -361,13 +372,13 @@ function M.is_enabled(bufnr)
if bufnr == nil or bufnr == 0 then if bufnr == nil or bufnr == 0 then
bufnr = api.nvim_get_current_buf() bufnr = api.nvim_get_current_buf()
end end
return bufstates[bufnr] and bufstates[bufnr].enabled or false return bufstates[bufnr].enabled
end end
--- Optional filters |kwargs|, or `nil` for all. --- Optional filters |kwargs|, or `nil` for all.
--- @class vim.lsp.inlay_hint.enable.Filter --- @class vim.lsp.inlay_hint.enable.Filter
--- @inlinedoc --- @inlinedoc
--- Buffer number, or 0/nil for current buffer. --- Buffer number, or 0 for current buffer, or nil for all.
--- @field bufnr integer? --- @field bufnr integer?
--- Enables or disables inlay hints for a buffer. --- Enables or disables inlay hints for a buffer.
@ -392,11 +403,28 @@ function M.enable(enable, filter)
end end
vim.validate({ enable = { enable, 'boolean', true }, filter = { filter, 'table', true } }) vim.validate({ enable = { enable, 'boolean', true }, filter = { filter, 'table', true } })
enable = enable == nil or enable
filter = filter or {} filter = filter or {}
if enable == false then
_disable(filter.bufnr) if filter.bufnr == nil then
globalstate.enabled = enable
for bufnr, _ in pairs(bufstates) do
if api.nvim_buf_is_loaded(bufnr) then
if enable == false then
_disable(bufnr)
else
_enable(bufnr)
end
else
bufstates[bufnr] = nil
end
end
else else
_enable(filter.bufnr) if enable == false then
_disable(filter.bufnr)
else
_enable(filter.bufnr)
end
end end
end end

View File

@ -137,20 +137,51 @@ describe('vim.lsp.inlay_hint', function()
) )
end) end)
it('clears/applies inlay hints when passed false/true/nil', function() describe('clears/applies inlay hints when passed false/true/nil', function()
exec_lua([[vim.lsp.inlay_hint.enable(false, { bufnr = bufnr })]]) before_each(function()
screen:expect({ grid = grid_without_inlay_hints, unchanged = true }) 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)
exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]]) it('for one single buffer', function()
screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) 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( exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]])
[[vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled(bufnr), { bufnr = bufnr })]] screen:expect({ grid = grid_with_inlay_hints, unchanged = true })
)
screen:expect({ grid = grid_without_inlay_hints, unchanged = true })
exec_lua([[vim.lsp.inlay_hint.enable(true, { bufnr = bufnr })]]) exec_lua(
screen:expect({ grid = grid_with_inlay_hints, unchanged = true }) [[vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled(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)
end) end)