From 01c4d37959227ccb7a7fce1a8d63616ef2ed7479 Mon Sep 17 00:00:00 2001 From: Rocco Vaccone <25404382+rvaccone@users.noreply.github.com> Date: Sun, 19 Jul 2026 20:16:56 -0400 Subject: [PATCH] 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 --- runtime/doc/news.txt | 3 +++ runtime/lua/vim/lsp/util.lua | 27 +++++++++++++++++++--- test/functional/plugin/lsp/util_spec.lua | 29 ++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index ab2b6f27e4..0faa65d23c 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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* diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 44a6880727..c181f4c2e0 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -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) diff --git a/test/functional/plugin/lsp/util_spec.lua b/test/functional/plugin/lsp/util_spec.lua index 9195f45d8c..cfac7f698d 100644 --- a/test/functional/plugin/lsp/util_spec.lua +++ b/test/functional/plugin/lsp/util_spec.lua @@ -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()