mirror of
https://github.com/neovim/neovim.git
synced 2026-04-19 22:10:45 +00:00
fix(lsp): check stale context in hover/signature callback #38724
Problem: hover/signature callback lacked consistency checks, so slow LSP servers could open a float after the cursor had already moved away. Solution: guard the callback with buf validity, buf version, and cursor position checks before opening the float. Also fix table capacity calculation.
This commit is contained in:
@@ -25,6 +25,32 @@ end
|
||||
local hover_ns = api.nvim_create_namespace('nvim.lsp.hover_range')
|
||||
local rename_ns = api.nvim_create_namespace('nvim.lsp.rename_range')
|
||||
|
||||
--- Returns false if the LSP response is stale and should be discarded.
|
||||
--- @param ctx lsp.HandlerContext
|
||||
--- @return boolean
|
||||
local function ctx_is_valid(ctx)
|
||||
local bufnr = ctx.bufnr
|
||||
if
|
||||
not bufnr
|
||||
or not api.nvim_buf_is_valid(bufnr)
|
||||
or api.nvim_get_current_buf() ~= bufnr
|
||||
or vim.lsp.util.buf_versions[bufnr] ~= ctx.version
|
||||
then
|
||||
return false
|
||||
end
|
||||
local p = ctx.params and ctx.params.position
|
||||
if not p then
|
||||
return true
|
||||
end
|
||||
|
||||
local cur = api.nvim_win_get_cursor(0)
|
||||
local c = lsp.get_client_by_id(ctx.client_id)
|
||||
local enc = c and c.offset_encoding
|
||||
|
||||
return cur[1] - 1 == p.line and enc and cur[2] == util._get_line_byte_from_position(bufnr, p, enc)
|
||||
or false
|
||||
end
|
||||
|
||||
--- @class vim.lsp.buf.hover.Opts : vim.lsp.util.open_floating_preview.Opts
|
||||
--- @field silent? boolean
|
||||
|
||||
@@ -53,14 +79,14 @@ function M.hover(config)
|
||||
config.focus_id = 'textDocument/hover'
|
||||
|
||||
lsp.buf_request_all(0, 'textDocument/hover', client_positional_params(), function(results, ctx)
|
||||
local bufnr = assert(ctx.bufnr)
|
||||
if api.nvim_get_current_buf() ~= bufnr then
|
||||
-- Ignore result since buffer changed. This happens for slow language servers.
|
||||
return
|
||||
local bufnr = ctx.bufnr
|
||||
if not bufnr or not ctx_is_valid(ctx) then
|
||||
return -- Ignore result if context changed. Can happen for slow LS.
|
||||
end
|
||||
|
||||
-- Filter errors from results
|
||||
local results1 = {} --- @type table<integer,lsp.Hover>
|
||||
local nresults = 0
|
||||
local empty_response = false
|
||||
|
||||
for client_id, resp in pairs(results) do
|
||||
@@ -75,29 +101,29 @@ function M.hover(config)
|
||||
-- - MarkedString-pair: { language="c", value="doc" }
|
||||
-- - MarkedString[]-string: { "doc1", ... }
|
||||
-- - MarkedString[]-pair: { { language="c", value="doc1" }, ... }
|
||||
if
|
||||
(
|
||||
type(result.contents) == 'table'
|
||||
and #(
|
||||
vim.tbl_get(result.contents, 'value') -- MarkupContent or MarkedString-pair
|
||||
or vim.tbl_get(result.contents, 1, 'value') -- MarkedString[]-pair
|
||||
or result.contents[1] -- MarkedString[]-string
|
||||
or ''
|
||||
)
|
||||
> 0
|
||||
local valid = false
|
||||
if type(result.contents) == 'table' then
|
||||
local value_len = #(
|
||||
vim.tbl_get(result.contents, 'value') -- MarkupContent or MarkedString-pair
|
||||
or vim.tbl_get(result.contents, 1, 'value') -- MarkedString[]-pair
|
||||
or result.contents[1] -- MarkedString[]-string
|
||||
or ''
|
||||
)
|
||||
or (
|
||||
type(result.contents) == 'string' and #result.contents > 0 -- MarkedString-string
|
||||
)
|
||||
then
|
||||
valid = value_len > 0
|
||||
elseif type(result.contents) == 'string' then
|
||||
valid = #result.contents > 0
|
||||
end
|
||||
|
||||
if valid then
|
||||
results1[client_id] = result
|
||||
nresults = nresults + 1
|
||||
else
|
||||
empty_response = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if vim.tbl_isempty(results1) then
|
||||
if nresults == 0 then
|
||||
if config.silent ~= true then
|
||||
if empty_response then
|
||||
vim.notify('Empty hover response', vim.log.levels.INFO)
|
||||
@@ -109,10 +135,8 @@ function M.hover(config)
|
||||
end
|
||||
|
||||
local contents = {} --- @type string[]
|
||||
|
||||
local nresults = #vim.tbl_keys(results1)
|
||||
|
||||
local format = 'markdown'
|
||||
local MarkupKind = lsp.protocol.MarkupKind
|
||||
local format = MarkupKind.Markdown
|
||||
|
||||
for client_id, result in pairs(results1) do
|
||||
local client = assert(lsp.get_client_by_id(client_id))
|
||||
@@ -120,12 +144,14 @@ function M.hover(config)
|
||||
-- Show client name if there are multiple clients
|
||||
contents[#contents + 1] = string.format('# %s', client.name)
|
||||
end
|
||||
if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then
|
||||
if #results1 == 1 then
|
||||
format = 'plaintext'
|
||||
|
||||
if type(result.contents) == 'table' and result.contents.kind == MarkupKind.PlainText then
|
||||
if nresults == 1 then
|
||||
-- Only one client: use PlainText format
|
||||
format = MarkupKind.PlainText
|
||||
contents = vim.split(result.contents.value or '', '\n', { trimempty = true })
|
||||
else
|
||||
-- Surround plaintext with ``` to get correct formatting
|
||||
-- Multiple clients: surround plaintext with ``` to get correct formatting
|
||||
contents[#contents + 1] = '```'
|
||||
vim.list_extend(
|
||||
contents,
|
||||
@@ -155,8 +181,10 @@ function M.hover(config)
|
||||
contents[#contents + 1] = '---'
|
||||
end
|
||||
|
||||
-- Remove last linebreak ('---')
|
||||
contents[#contents] = nil
|
||||
-- Remove last linebreak ('---') if contents is not empty
|
||||
if #contents > 0 then
|
||||
contents[#contents] = nil
|
||||
end
|
||||
|
||||
local _, winid = lsp.util.open_floating_preview(contents, format, config)
|
||||
|
||||
@@ -387,9 +415,8 @@ function M.signature_help(config)
|
||||
local user_title = config.title
|
||||
|
||||
lsp.buf_request_all(0, method, client_positional_params(), function(results, ctx)
|
||||
if api.nvim_get_current_buf() ~= ctx.bufnr then
|
||||
-- Ignore result since buffer changed. This happens for slow language servers.
|
||||
return
|
||||
if not ctx_is_valid(ctx) then
|
||||
return -- Ignore result if context changed. Can happen for slow LS.
|
||||
end
|
||||
|
||||
local signatures, active_signature = process_signature_help_results(results)
|
||||
|
||||
Reference in New Issue
Block a user