fix(lsp): preview window converted to normal window is auto-closed (#40770)

Problem:
A floating preview window (hover, signature help) converted to a normal
window (e.g. with CTRL-W_H or nvim_win_set_config()) is still closed by
the open_floating_preview() autocommands, and a later preview may focus
or close it as if it were still a float.

Solution:
When the preview window is no longer floating, remove its auto-close
autocommands and stop tracking it as the buffer's floating preview.

Closes #36659
This commit is contained in:
Rocco Vaccone
2026-07-19 20:16:56 -04:00
committed by GitHub
parent 1dffa35d46
commit 01c4d37959
3 changed files with 56 additions and 3 deletions

View File

@@ -417,6 +417,9 @@ These existing features changed their behavior.
• Markdown inline highlighting now conceals the backslash in backslash escapes.
• Markdown inline backslash escapes and hard line breaks no longer use the
`@string.escape` capture.
• |vim.lsp.util.open_floating_preview()| windows (e.g. |vim.lsp.buf.hover()|)
converted to normal windows (e.g. with |CTRL-W_H|) are no longer closed
automatically.
==============================================================================
REMOVED FEATURES *news-removed*

View File

@@ -950,6 +950,12 @@ local function find_window_by_var(name, value)
end
end
---@param winnr integer
---@return boolean `true` if `winnr` is a valid floating window
local function is_float(winnr)
return api.nvim_win_is_valid(winnr) and vim.fn.win_gettype(winnr) == 'popup'
end
---Returns true if the line is empty or only contains whitespace.
---@param line string
---@return boolean
@@ -1305,6 +1311,21 @@ local function close_preview_window(winnr, bufnrs)
local augroup = 'nvim.preview_window_' .. winnr
pcall(api.nvim_del_augroup_by_name, augroup)
-- Preview window was converted to a normal window (e.g. |CTRL-W_H|):
-- keep it open and stop managing it.
if api.nvim_win_is_valid(winnr) and not is_float(winnr) then
local srcbuf = vim.w[winnr].lsp_floating_bufnr
if
srcbuf
and api.nvim_buf_is_valid(srcbuf)
and vim.b[srcbuf].lsp_floating_preview == winnr
then
vim.b[srcbuf].lsp_floating_preview = nil
end
return
end
pcall(api.nvim_win_close, winnr, true)
end)
end
@@ -1508,13 +1529,13 @@ function M.open_floating_preview(contents, syntax, opts)
if opts.focus_id and opts.focusable ~= false and opts.focus then
-- Go back to previous window if we are in a focusable one
local current_winnr = api.nvim_get_current_win()
if vim.w[current_winnr][opts.focus_id] then
if vim.w[current_winnr][opts.focus_id] and is_float(current_winnr) then
api.nvim_command('wincmd p')
return bufnr, current_winnr
end
do
local win = find_window_by_var(opts.focus_id, bufnr)
if win and api.nvim_win_is_valid(win) and vim.fn.pumvisible() == 0 then
if win and is_float(win) and vim.fn.pumvisible() == 0 then
-- focus and return the existing buf, win
api.nvim_set_current_win(win)
api.nvim_command('stopinsert')
@@ -1526,7 +1547,7 @@ function M.open_floating_preview(contents, syntax, opts)
-- check if another floating preview already exists for this buffer
-- and close it if needed
local existing_float = vim.b[bufnr].lsp_floating_preview
if existing_float and api.nvim_win_is_valid(existing_float) then
if existing_float and is_float(existing_float) then
api.nvim_win_close(existing_float, true)
end
floating_bufnr = api.nvim_create_buf(false, true)

View File

@@ -1385,6 +1385,35 @@ describe('vim.lsp.util', function()
-- b:lsp_floating_preview should be cleared.
eq('Key not found: lsp_floating_preview', pcall_err(api.nvim_buf_get_var, 0, var_name))
end)
it('converted to a normal window is unmanaged, not closed #36659', function()
local converted = exec_lua(function()
local opts = { focus_id = 'focus_test' }
local cur_winnr = vim.api.nvim_get_current_win()
vim.lsp.util.open_floating_preview({ 'test' }, '', opts)
local converted = vim.b.lsp_floating_preview
vim.api.nvim_win_set_config(converted, { win = cur_winnr, split = 'right' })
-- close events unmanage the converted window instead of closing it
vim.api.nvim_exec_autocmds('CursorMoved', { buffer = 0 })
assert(
vim.wait(1000, function()
return vim.b.lsp_floating_preview == nil
end),
'preview should be untracked after close event'
)
-- reusing the focus_id opens a new float instead of refocusing the converted window
local _, new_win = vim.lsp.util.open_floating_preview({ 'test' }, '', opts)
assert(new_win ~= converted, 'expected a new float')
-- also when triggered from inside the converted window
vim.api.nvim_set_current_win(converted)
local _, newer_win = vim.lsp.util.open_floating_preview({ 'test' }, '', opts)
assert(newer_win ~= converted, 'expected a new float')
assert(vim.api.nvim_get_current_win() == converted, 'focus changed')
return converted
end)
eq(true, api.nvim_win_is_valid(converted))
eq('', api.nvim_win_get_config(converted).relative)
end)
end)
it('open_floating_preview zindex greater than current window', function()