mirror of
https://github.com/neovim/neovim.git
synced 2026-07-09 10:59:38 +00:00
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:
@@ -2388,9 +2388,11 @@ enable({enable}, {filter}) *vim.lsp.inlay_hint.enable()*
|
||||
|
||||
Parameters: ~
|
||||
• {enable} (`boolean?`) true/nil to enable, false to disable
|
||||
• {filter} (`table?`) Optional filters |kwargs|, or `nil` for all.
|
||||
• {bufnr} (`integer?`) Buffer number, or 0 for current
|
||||
buffer, or nil for all.
|
||||
• {filter} (`table?`) Optional filters |kwargs|,
|
||||
• {bufnr}? (`integer`, default: all) Buffer number, or 0 for
|
||||
current buffer, or nil for all.
|
||||
• {client_id}? (`integer`, default: all) Client ID, or nil
|
||||
for all.
|
||||
|
||||
get({filter}) *vim.lsp.inlay_hint.get()*
|
||||
Get the list of inlay hints, (optionally) restricted by buffer or range.
|
||||
@@ -2434,9 +2436,11 @@ is_enabled({filter}) *vim.lsp.inlay_hint.is_enabled()*
|
||||
Since: 0.10.0
|
||||
|
||||
Parameters: ~
|
||||
• {filter} (`table?`) Optional filters |kwargs|, or `nil` for all.
|
||||
• {bufnr} (`integer?`) Buffer number, or 0 for current
|
||||
buffer, or nil for all.
|
||||
• {filter} (`table?`) Optional filters |kwargs|,
|
||||
• {bufnr}? (`integer`, default: all) Buffer number, or 0 for
|
||||
current buffer, or nil for all.
|
||||
• {client_id}? (`integer`, default: all) Client ID, or nil
|
||||
for all.
|
||||
|
||||
Return: ~
|
||||
(`boolean`)
|
||||
|
||||
@@ -5,6 +5,7 @@ local api = vim.api
|
||||
---| 'diagnostics'
|
||||
---| 'document_color'
|
||||
---| 'folding_range'
|
||||
---| 'inlay_hint'
|
||||
---| 'inline_completion'
|
||||
---| 'linked_editing_range'
|
||||
---| 'semantic_tokens'
|
||||
|
||||
@@ -557,6 +557,7 @@ function Client:initialize()
|
||||
require('vim.lsp._folding_range')
|
||||
require('vim.lsp.diagnostic')
|
||||
require('vim.lsp.document_color')
|
||||
require('vim.lsp.inlay_hint')
|
||||
require('vim.lsp.inline_completion')
|
||||
require('vim.lsp.linked_editing_range')
|
||||
require('vim.lsp.semantic_tokens')
|
||||
|
||||
@@ -1,37 +1,143 @@
|
||||
local util = require('vim.lsp.util')
|
||||
local api = vim.api
|
||||
local log = require('vim.lsp.log')
|
||||
local nvim_on = require('vim._core.util').nvim_on
|
||||
local api = vim.api
|
||||
local util = require('vim.lsp.util')
|
||||
|
||||
local Capability = require('vim.lsp._capability')
|
||||
|
||||
local M = {}
|
||||
|
||||
---@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.LineHints
|
||||
---@field hints lsp.InlayHint[]
|
||||
---@field applied boolean whether this line's hints have had extmarks applied
|
||||
|
||||
---@class (private) vim.lsp.inlay_hint.CurrentResult Info for current result
|
||||
---@field version? integer document version associated with this result
|
||||
---@field namespace_cleared? boolean whether the namespace was cleared for this result yet
|
||||
---@field hints? table<integer, vim.lsp.inlay_hint.LineHints> lnum -> hints
|
||||
|
||||
---@class (private) vim.lsp.inlay_hint.ActiveRequest
|
||||
---@field request_id? integer the LSP request ID of the most recent request sent to the server
|
||||
---@field version? integer the document version associated with the most recent request
|
||||
|
||||
---@class (private) vim.lsp.inlay_hint.ClientState Buffer local state for inlay hints
|
||||
---@field namespace integer
|
||||
---@field active_request vim.lsp.inlay_hint.ActiveRequest
|
||||
---@field current_result vim.lsp.inlay_hint.CurrentResult
|
||||
|
||||
---@class (private) InlayHints : vim.lsp.Capability
|
||||
---@field active table<integer, InlayHints>
|
||||
---@field client_state table<integer, vim.lsp.inlay_hint.ClientState>
|
||||
local InlayHint = {
|
||||
name = 'inlay_hint',
|
||||
method = 'textDocument/inlayHint',
|
||||
active = {},
|
||||
}
|
||||
InlayHint.__index = InlayHint
|
||||
setmetatable(InlayHint, Capability)
|
||||
Capability.all[InlayHint.name] = InlayHint
|
||||
|
||||
---@class (private) vim.lsp.inlay_hint.bufstate: vim.lsp.inlay_hint.globalstate Buffer local state for inlay hints
|
||||
---@field version? integer
|
||||
---@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
|
||||
---@package
|
||||
function InlayHint:new(bufnr)
|
||||
self = Capability.new(self, bufnr)
|
||||
|
||||
---@type table<integer, vim.lsp.inlay_hint.bufstate>
|
||||
local bufstates = vim.defaulttable(function(_)
|
||||
return setmetatable({ applied = {} }, {
|
||||
__index = globalstate,
|
||||
__newindex = function(state, key, value)
|
||||
if globalstate[key] == value then
|
||||
rawset(state, key, nil)
|
||||
else
|
||||
rawset(state, key, value)
|
||||
end
|
||||
end,
|
||||
})
|
||||
end)
|
||||
nvim_on('LspNotify', self.augroup, { buf = self.bufnr }, function(ev)
|
||||
local client_id = ev.data.client_id ---@type integer
|
||||
|
||||
local namespace = api.nvim_create_namespace('nvim.lsp.inlayhint')
|
||||
local augroup = api.nvim_create_augroup('nvim.lsp.inlayhint', {})
|
||||
if not self.client_state[client_id] then
|
||||
return
|
||||
end
|
||||
|
||||
if ev.data.method == 'textDocument/didClose' then
|
||||
self:reset(client_id)
|
||||
end
|
||||
|
||||
if ev.data.method == 'textDocument/didChange' or ev.data.method == 'textDocument/didOpen' then
|
||||
self:refresh(client_id)
|
||||
end
|
||||
end)
|
||||
|
||||
nvim_on('BufWinEnter', self.augroup, { buf = self.bufnr }, function()
|
||||
for client_id, _ in pairs(self.client_state) do
|
||||
self:refresh(client_id)
|
||||
end
|
||||
end)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
---@package
|
||||
function InlayHint:on_attach(client_id)
|
||||
if not self.client_state[client_id] then
|
||||
self.client_state[client_id] = {
|
||||
namespace = api.nvim_create_namespace('nvim.lsp.inlay_hint:' .. client_id),
|
||||
active_request = {},
|
||||
current_result = {},
|
||||
}
|
||||
end
|
||||
self:refresh(client_id)
|
||||
end
|
||||
|
||||
---@package
|
||||
function InlayHint:on_detach(client_id)
|
||||
local state = self.client_state[client_id]
|
||||
if state then
|
||||
self:reset(client_id)
|
||||
self.client_state[client_id] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Reset the buffer's inlay hint state and clear the extmarks
|
||||
---@package
|
||||
---@param client_id integer
|
||||
function InlayHint:reset(client_id)
|
||||
local state = assert(self.client_state[client_id])
|
||||
self:cancel_active_request(client_id)
|
||||
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
|
||||
state.current_result = {}
|
||||
end
|
||||
|
||||
--- Refresh inlay hints by requesting them from the server
|
||||
---
|
||||
--- Only sends a request if there is no active request in flight for the current document version.
|
||||
--- Otherwise, it cancels any previous in-progress request before sending a new one.
|
||||
---
|
||||
---@package
|
||||
---@param client_id integer
|
||||
function InlayHint:refresh(client_id)
|
||||
local version = util.buf_versions[self.bufnr]
|
||||
local state = self.client_state[client_id]
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
|
||||
if state and client then
|
||||
local current_result = state.current_result
|
||||
local active_request = state.active_request
|
||||
|
||||
-- Only send a request for this client if the current result is out of date and
|
||||
-- there isn't a current a request in flight for this version
|
||||
if current_result.version == version or active_request.version == version then
|
||||
return
|
||||
end
|
||||
|
||||
-- cancel stale in-flight request
|
||||
self:cancel_active_request(client_id)
|
||||
|
||||
---@type lsp.InlayHintParams
|
||||
local params = {
|
||||
textDocument = util.make_text_document_params(self.bufnr),
|
||||
range = vim
|
||||
.range(self.bufnr, 0, 0, api.nvim_buf_line_count(self.bufnr), 0)
|
||||
:to_lsp(client.offset_encoding),
|
||||
}
|
||||
|
||||
local success, request_id = client:request('textDocument/inlayHint', params, nil, self.bufnr)
|
||||
|
||||
if success then
|
||||
active_request.request_id = request_id
|
||||
active_request.version = version
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- |lsp-handler| for the method `textDocument/inlayHint`
|
||||
--- Store hints for a specific buffer and client
|
||||
@@ -39,72 +145,85 @@ local augroup = api.nvim_create_augroup('nvim.lsp.inlayhint', {})
|
||||
---@param ctx lsp.HandlerContext
|
||||
---@private
|
||||
function M.on_inlayhint(err, result, ctx)
|
||||
local bufnr = assert(ctx.bufnr)
|
||||
local provider = InlayHint.active[bufnr]
|
||||
if not provider then
|
||||
return
|
||||
end
|
||||
|
||||
local state = provider.client_state[ctx.client_id]
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
|
||||
if err then
|
||||
log.error('inlay_hint', err)
|
||||
state.active_request = {}
|
||||
return
|
||||
end
|
||||
local bufnr = assert(ctx.bufnr)
|
||||
|
||||
if
|
||||
util.buf_versions[bufnr] ~= ctx.version
|
||||
or not api.nvim_buf_is_loaded(bufnr)
|
||||
or not bufstates[bufnr].enabled
|
||||
then
|
||||
if util.buf_versions[bufnr] ~= ctx.version or not api.nvim_buf_is_loaded(bufnr) then
|
||||
return
|
||||
end
|
||||
local client_id = ctx.client_id
|
||||
local bufstate = bufstates[bufnr]
|
||||
if not (bufstate.client_hints and bufstate.version) then
|
||||
bufstate.client_hints = vim.defaulttable()
|
||||
bufstate.version = ctx.version
|
||||
|
||||
-- ignore stale responses
|
||||
if state.active_request.request_id and ctx.request_id ~= state.active_request.request_id then
|
||||
return
|
||||
end
|
||||
local client_hints = bufstate.client_hints
|
||||
local client = assert(vim.lsp.get_client_by_id(client_id))
|
||||
|
||||
-- If there's no error but the result is nil, clear existing hints.
|
||||
result = result or {}
|
||||
|
||||
local new_lnum_hints = vim.defaulttable()
|
||||
local new_lnum_hints = {} ---@type table<integer, vim.lsp.inlay_hint.LineHints>
|
||||
local num_unprocessed = #result
|
||||
if num_unprocessed == 0 then
|
||||
client_hints[client_id] = {}
|
||||
bufstate.version = ctx.version
|
||||
api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
|
||||
state.active_request = {}
|
||||
state.current_result = {}
|
||||
if vim.fn.win_gettype(vim.fn.bufwinid(bufnr)) == '' then
|
||||
api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local client = assert(vim.lsp.get_client_by_id(ctx.client_id))
|
||||
|
||||
for _, hint in ipairs(result) do
|
||||
local lnum = hint.position.line
|
||||
local line = lines and lines[lnum + 1] or ''
|
||||
hint.position.character =
|
||||
vim.str_byteindex(line, client.offset_encoding, hint.position.character, false)
|
||||
table.insert(new_lnum_hints[lnum], hint)
|
||||
if not new_lnum_hints[lnum] then
|
||||
new_lnum_hints[lnum] = {
|
||||
hints = {},
|
||||
applied = false,
|
||||
}
|
||||
end
|
||||
table.insert(new_lnum_hints[lnum].hints, hint)
|
||||
end
|
||||
|
||||
client_hints[client_id] = new_lnum_hints
|
||||
bufstate.version = ctx.version
|
||||
api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
|
||||
state.active_request = {}
|
||||
state.current_result = {
|
||||
hints = new_lnum_hints,
|
||||
version = ctx.version,
|
||||
namespace_cleared = false,
|
||||
}
|
||||
|
||||
if vim.fn.win_gettype(vim.fn.bufwinid(bufnr)) == '' then
|
||||
api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
|
||||
end
|
||||
end
|
||||
|
||||
--- Refresh inlay hints, only if we have attached clients that support it
|
||||
---@param bufnr (integer) Buffer handle, or 0 for current
|
||||
---@param client_id? (integer) Client ID, or nil for all
|
||||
local function refresh(bufnr, client_id)
|
||||
for _, client in
|
||||
ipairs(vim.lsp.get_clients({
|
||||
bufnr = bufnr,
|
||||
id = client_id,
|
||||
method = 'textDocument/inlayHint',
|
||||
}))
|
||||
do
|
||||
client:request('textDocument/inlayHint', {
|
||||
textDocument = util.make_text_document_params(bufnr),
|
||||
range = vim
|
||||
.range(bufnr, 0, 0, api.nvim_buf_line_count(bufnr), 0)
|
||||
:to_lsp(client.offset_encoding),
|
||||
}, nil, bufnr)
|
||||
---@private
|
||||
function InlayHint:cancel_active_request(client_id)
|
||||
local state = assert(self.client_state[client_id])
|
||||
local client = vim.lsp.get_client_by_id(client_id)
|
||||
local active_request = state.active_request
|
||||
|
||||
if client and active_request.request_id then
|
||||
client:cancel_request(active_request.request_id)
|
||||
active_request.request_id = nil
|
||||
active_request.version = nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,13 +234,14 @@ function M.on_refresh(err, _, ctx)
|
||||
if err then
|
||||
return vim.NIL
|
||||
end
|
||||
|
||||
for bufnr in pairs(vim.lsp.get_client_by_id(ctx.client_id).attached_buffers or {}) do
|
||||
for _, winid in ipairs(api.nvim_list_wins()) do
|
||||
if api.nvim_win_get_buf(winid) == bufnr then
|
||||
if bufstates[bufnr] and bufstates[bufnr].enabled then
|
||||
bufstates[bufnr].applied = {}
|
||||
refresh(bufnr)
|
||||
end
|
||||
local provider = InlayHint.active[bufnr]
|
||||
if provider and provider.client_state[ctx.client_id] then
|
||||
provider:reset(ctx.client_id)
|
||||
|
||||
if not vim.tbl_isempty(vim.fn.win_findbuf(bufnr)) then
|
||||
provider:refresh(ctx.client_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -172,27 +292,19 @@ function M.get(filter)
|
||||
|
||||
local bufnr = filter.bufnr
|
||||
if not bufnr then
|
||||
--- @type vim.lsp.inlay_hint.get.ret[]
|
||||
local hints = {}
|
||||
--- @param buf integer
|
||||
vim.tbl_map(function(buf)
|
||||
vim.list_extend(hints, M.get(vim.tbl_extend('keep', { bufnr = buf }, filter)))
|
||||
end, api.nvim_list_bufs())
|
||||
return hints
|
||||
return vim
|
||||
.iter(api.nvim_list_bufs())
|
||||
:map(function(buf)
|
||||
return M.get(vim.tbl_extend('keep', { bufnr = buf }, filter))
|
||||
end)
|
||||
:flatten()
|
||||
:totable()
|
||||
else
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
end
|
||||
|
||||
local bufstate = bufstates[bufnr]
|
||||
if not bufstate.client_hints then
|
||||
return {}
|
||||
end
|
||||
|
||||
local clients = vim.lsp.get_clients({
|
||||
bufnr = bufnr,
|
||||
method = 'textDocument/inlayHint',
|
||||
})
|
||||
if #clients == 0 then
|
||||
local provider = InlayHint.active[bufnr]
|
||||
if not provider then
|
||||
return {}
|
||||
end
|
||||
|
||||
@@ -206,12 +318,12 @@ function M.get(filter)
|
||||
|
||||
--- @type vim.lsp.inlay_hint.get.ret[]
|
||||
local result = {}
|
||||
for _, client in pairs(clients) do
|
||||
local lnum_hints = bufstate.client_hints[client.id]
|
||||
for client_id, state in pairs(provider.client_state) do
|
||||
local lnum_hints = state.current_result.hints
|
||||
if lnum_hints then
|
||||
for lnum = range.start.line, range['end'].line do
|
||||
local hints = lnum_hints[lnum] or {}
|
||||
for _, hint in pairs(hints) do
|
||||
local line_hints = lnum_hints[lnum] or { hints = {}, applied = false }
|
||||
for _, hint in pairs(line_hints.hints) do
|
||||
local line, char = hint.position.line, hint.position.character
|
||||
if
|
||||
(line > range.start.line or char >= range.start.character)
|
||||
@@ -219,7 +331,7 @@ function M.get(filter)
|
||||
then
|
||||
table.insert(result, {
|
||||
bufnr = bufnr,
|
||||
client_id = client.id,
|
||||
client_id = client_id,
|
||||
inlay_hint = hint,
|
||||
})
|
||||
end
|
||||
@@ -230,105 +342,27 @@ function M.get(filter)
|
||||
return result
|
||||
end
|
||||
|
||||
--- Clear inlay hints
|
||||
---@param bufnr (integer) Buffer handle, or 0 for current
|
||||
local function clear(bufnr)
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
local bufstate = bufstates[bufnr]
|
||||
local client_lens = (bufstate or {}).client_hints or {}
|
||||
local client_ids = vim.tbl_keys(client_lens) --- @type integer[]
|
||||
for _, iter_client_id in ipairs(client_ids) do
|
||||
if bufstate then
|
||||
bufstate.client_hints[iter_client_id] = {}
|
||||
end
|
||||
end
|
||||
api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
|
||||
api.nvim__redraw({ buf = bufnr, valid = true, flush = false })
|
||||
end
|
||||
|
||||
--- Disable inlay hints for a buffer
|
||||
---@param bufnr (integer) Buffer handle, or 0 for current
|
||||
local function _disable(bufnr)
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
clear(bufnr)
|
||||
bufstates[bufnr] = nil
|
||||
bufstates[bufnr].enabled = false
|
||||
end
|
||||
|
||||
--- Enable inlay hints for a buffer
|
||||
---@param bufnr (integer) Buffer handle, or 0 for current
|
||||
local function _enable(bufnr)
|
||||
bufnr = vim._resolve_bufnr(bufnr)
|
||||
bufstates[bufnr] = nil
|
||||
bufstates[bufnr].enabled = true
|
||||
refresh(bufnr)
|
||||
end
|
||||
|
||||
nvim_on('LspNotify', augroup, function(ev)
|
||||
---@type integer
|
||||
local bufnr = ev.buf
|
||||
|
||||
if ev.data.method ~= 'textDocument/didChange' and ev.data.method ~= 'textDocument/didOpen' then
|
||||
return
|
||||
end
|
||||
if bufstates[bufnr].enabled then
|
||||
refresh(bufnr, ev.data.client_id)
|
||||
end
|
||||
end)
|
||||
nvim_on('LspAttach', augroup, function(ev)
|
||||
---@type integer
|
||||
local bufnr = ev.buf
|
||||
|
||||
api.nvim_buf_attach(bufnr, false, {
|
||||
on_reload = function(_, cb_bufnr)
|
||||
clear(cb_bufnr)
|
||||
if bufstates[cb_bufnr] and bufstates[cb_bufnr].enabled then
|
||||
bufstates[cb_bufnr].applied = {}
|
||||
refresh(cb_bufnr)
|
||||
--- on_win handler for the decoration provider (see |nvim_set_decoration_provider|)
|
||||
---@package
|
||||
---@param topline integer
|
||||
---@param botline integer
|
||||
function InlayHint:on_win(topline, botline)
|
||||
for _, state in pairs(self.client_state) do
|
||||
local current_result = state.current_result
|
||||
if current_result.version == util.buf_versions[self.bufnr] then
|
||||
if not current_result.namespace_cleared then
|
||||
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
|
||||
current_result.namespace_cleared = true
|
||||
end
|
||||
end,
|
||||
on_detach = function(_, cb_bufnr)
|
||||
_disable(cb_bufnr)
|
||||
bufstates[cb_bufnr] = nil
|
||||
end,
|
||||
})
|
||||
end)
|
||||
nvim_on('LspDetach', augroup, function(ev)
|
||||
---@type integer
|
||||
local bufnr = ev.buf
|
||||
local clients = vim.lsp.get_clients({ bufnr = bufnr, method = 'textDocument/inlayHint' })
|
||||
|
||||
if not vim.iter(clients):any(function(c)
|
||||
return c.id ~= ev.data.client_id
|
||||
end) then
|
||||
_disable(bufnr)
|
||||
end
|
||||
end)
|
||||
api.nvim_set_decoration_provider(namespace, {
|
||||
on_win = function(_, _, bufnr, topline, botline)
|
||||
---@type vim.lsp.inlay_hint.bufstate
|
||||
local bufstate = rawget(bufstates, bufnr)
|
||||
if not bufstate then
|
||||
return
|
||||
end
|
||||
|
||||
if bufstate.version ~= util.buf_versions[bufnr] then
|
||||
return
|
||||
end
|
||||
|
||||
if not bufstate.client_hints then
|
||||
return
|
||||
end
|
||||
local client_hints = assert(bufstate.client_hints)
|
||||
|
||||
for lnum = topline, botline do
|
||||
if bufstate.applied[lnum] ~= bufstate.version then
|
||||
api.nvim_buf_clear_namespace(bufnr, namespace, lnum, lnum + 1)
|
||||
local hints = assert(current_result.hints)
|
||||
|
||||
for lnum = topline, botline do
|
||||
local hint_virtual_texts = {} --- @type table<integer, [string, string?][]>
|
||||
for _, lnum_hints in pairs(client_hints) do
|
||||
local hints = lnum_hints[lnum] or {}
|
||||
for _, hint in pairs(hints) do
|
||||
local line_hints = hints[lnum]
|
||||
if line_hints and not line_hints.applied then
|
||||
line_hints.applied = true
|
||||
for _, hint in pairs(line_hints.hints) do
|
||||
local text = ''
|
||||
local label = hint.label
|
||||
if type(label) == 'string' then
|
||||
@@ -351,40 +385,25 @@ api.nvim_set_decoration_provider(namespace, {
|
||||
end
|
||||
|
||||
for pos, vt in pairs(hint_virtual_texts) do
|
||||
api.nvim_buf_set_extmark(bufnr, namespace, lnum, pos, {
|
||||
api.nvim_buf_set_extmark(self.bufnr, state.namespace, lnum, pos, {
|
||||
virt_text_pos = 'inline',
|
||||
ephemeral = false,
|
||||
virt_text = vt,
|
||||
})
|
||||
end
|
||||
|
||||
bufstate.applied[lnum] = bufstate.version
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
--- Query whether inlay hint is enabled in the {filter}ed scope
|
||||
--- @param filter? vim.lsp.inlay_hint.enable.Filter
|
||||
--- @param filter? vim.lsp.capability.enable.Filter
|
||||
--- @return boolean
|
||||
--- @since 12
|
||||
function M.is_enabled(filter)
|
||||
vim.validate('filter', filter, 'table', true)
|
||||
filter = filter or {}
|
||||
local bufnr = filter.bufnr
|
||||
|
||||
if bufnr == nil then
|
||||
return globalstate.enabled
|
||||
end
|
||||
return bufstates[vim._resolve_bufnr(bufnr)].enabled
|
||||
return Capability.is_enabled('inlay_hint', filter)
|
||||
end
|
||||
|
||||
--- Optional filters |kwargs|, or `nil` for all.
|
||||
--- @class vim.lsp.inlay_hint.enable.Filter
|
||||
--- @inlinedoc
|
||||
--- Buffer number, or 0 for current buffer, or nil for all.
|
||||
--- @field bufnr integer?
|
||||
|
||||
--- Enables or disables inlay hints for the {filter}ed scope.
|
||||
---
|
||||
--- To "toggle", pass the inverse of `is_enabled()`:
|
||||
@@ -393,35 +412,21 @@ end
|
||||
--- vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled())
|
||||
--- ```
|
||||
---
|
||||
--- @param enable (boolean|nil) true/nil to enable, false to disable
|
||||
--- @param filter vim.lsp.inlay_hint.enable.Filter?
|
||||
--- @param enable boolean? true/nil to enable, false to disable
|
||||
--- @param filter? vim.lsp.capability.enable.Filter
|
||||
--- @since 12
|
||||
function M.enable(enable, filter)
|
||||
vim.validate('enable', enable, 'boolean', true)
|
||||
vim.validate('filter', filter, 'table', true)
|
||||
enable = enable == nil or enable
|
||||
filter = filter or {}
|
||||
|
||||
if filter.bufnr == nil then
|
||||
globalstate.enabled = enable
|
||||
for _, bufnr in ipairs(api.nvim_list_bufs()) 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
|
||||
if enable == false then
|
||||
_disable(filter.bufnr)
|
||||
else
|
||||
_enable(filter.bufnr)
|
||||
end
|
||||
end
|
||||
Capability.enable('inlay_hint', enable, filter)
|
||||
end
|
||||
|
||||
local namespace = api.nvim_create_namespace('nvim.lsp.inlay_hint')
|
||||
api.nvim_set_decoration_provider(namespace, {
|
||||
on_win = function(_, _, bufnr, topline, botline)
|
||||
local provider = InlayHint.active[bufnr]
|
||||
if provider then
|
||||
provider:on_win(topline, botline)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
return M
|
||||
|
||||
@@ -22,7 +22,7 @@ local M = {}
|
||||
--- @field version? integer document version associated with this result
|
||||
--- @field result_id? string resultId from the server; used with delta requests
|
||||
--- @field highlights? STTokenRange[] cache of highlight ranges for this document version
|
||||
--- @field tokens? integer[] raw token array as received by the server. used for calculating delta responses
|
||||
--- @field tokens? uinteger[] raw token array as received by the server. used for calculating delta responses
|
||||
--- @field namespace_cleared? boolean whether the namespace was cleared for this result yet
|
||||
|
||||
--- @class (private) STActiveRequest
|
||||
@@ -283,12 +283,11 @@ end
|
||||
|
||||
--- This is the entry point for getting all the tokens in a buffer.
|
||||
---
|
||||
--- For the given clients (or all attached, if not provided), this sends semantic token requests to
|
||||
--- ask for semantic tokens. If the server supports range requests and a full result has not been
|
||||
--- processed yet, it will send a range request for the current visible range. Additionally, if a
|
||||
--- result for the current document version hasn't been processed yet, it sends either a full or
|
||||
--- delta request, depending on what the server supports and whether there's a current full result
|
||||
--- for the previous document version.
|
||||
--- For the given client, this sends semantic token requests to ask for semantic tokens. If the
|
||||
--- server supports range requests and a full result has not been processed yet, it will send a
|
||||
--- range request for the current visible range. Additionally, if a result for the current document
|
||||
--- version hasn't been processed yet, it sends either a full or delta request, depending on what
|
||||
--- the server supports and whether there's a current full result for the previous document version.
|
||||
---
|
||||
--- This function will skip full/delta requests on servers where there is an already an active
|
||||
--- full/delta request in flight for the same version. If there is a stale request in flight, that
|
||||
@@ -738,10 +737,10 @@ end
|
||||
---@param client_id integer
|
||||
function STHighlighter:reset(client_id)
|
||||
local state = assert(self.client_state[client_id])
|
||||
self:cancel_active_request(client_id)
|
||||
api.nvim_buf_clear_namespace(self.bufnr, state.namespace, 0, -1)
|
||||
state.current_result = {}
|
||||
state.has_full_result = false
|
||||
self:cancel_active_request(client_id)
|
||||
end
|
||||
|
||||
--- Mark a client's results as dirty. This method will cancel any active
|
||||
@@ -987,8 +986,9 @@ end
|
||||
--- Refresh requests are sent by the server to indicate a project-wide change
|
||||
--- that requires all tokens to be re-requested by the client. This handler will
|
||||
--- invalidate the current results of all buffers and automatically kick off a
|
||||
--- new request for buffers that are displayed in a window. For those that aren't, a
|
||||
--- new request for buffers that are displayed in a window. For those that aren't,
|
||||
--- the BufWinEnter event should take care of it next time it's displayed.
|
||||
---@param ctx lsp.HandlerContext
|
||||
function M._refresh(err, _, ctx)
|
||||
if err then
|
||||
return vim.NIL
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user