diff --git a/README.md b/README.md index 017c17f..8498111 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ Built-in provider names: - `:PeekstackRestoreAllPopups` — restore all closed popups - `:PeekstackCloseAll` — close all popups in the current stack - `:PeekstackToggle` — temporarily hide/show all popups in the current stack +- `:PeekstackZoom` — toggle zoom (maximize the top popup to fill the editor) - `:PeekstackHistory` — show popup history and select to restore - `:PeekstackQuickPeek [provider]` — quick peek without stacking (default: `lsp.definition`, accepts any registered provider) @@ -127,6 +128,7 @@ Defaults inside popup windows: - `` — promote to vertical split - `` — promote to new tab - `os` — open stack view +- `` — toggle zoom (maximize top popup) Defaults in stack view: @@ -140,6 +142,7 @@ Defaults in stack view: - `/` — filter - `gg/G` — jump to first/last stack item - `j/k` — move cursor by stack item (skip header/preview lines) +- `z` — toggle zoom (maximize top popup) - `?` — help - `q` — close @@ -228,6 +231,7 @@ Configure via `require("peekstack").setup({ ... })`. promote_vsplit = "", promote_tab = "", toggle_stack_view = "os", + zoom = "", }, }, picker = { diff --git a/doc/peekstack.txt b/doc/peekstack.txt index b931757..8fe0c06 100644 --- a/doc/peekstack.txt +++ b/doc/peekstack.txt @@ -98,6 +98,7 @@ With options: promote_vsplit = "", promote_tab = "", toggle_stack_view = "os", + zoom = "", }, }, picker = { @@ -297,6 +298,13 @@ Commands are registered after `setup()` is called. again recreates the windows in their original layout. Pushing a new popup while hidden automatically restores visibility. +:PeekstackZoom *:PeekstackZoom* + Toggle zoom on the top popup. When zoomed, the popup fills the entire + editor. Call again to restore the normal layout. The border highlight + changes to `PeekstackPopupBorderZoomed` while zoomed. Zoom is + automatically cleared when the zoomed popup is closed, a new popup + is pushed, or the stack is hidden via |:PeekstackToggle|. + :PeekstackHistory *:PeekstackHistory* Show popup history and select an entry to restore. @@ -496,6 +504,7 @@ Default keymaps inside popup windows: `` Promote to vertical split `` Promote to new tab `os` Toggle the stack view panel + `` Toggle zoom (maximize top popup) Stack view keymaps: @@ -509,6 +518,7 @@ Stack view keymaps: `/` Filter the list `gg/G` Jump to first/last stack item `j/k` Move cursor by stack item (skip header/preview lines) + `z` Toggle zoom (maximize top popup) `?` Toggle help `q` Close the stack view @@ -534,6 +544,7 @@ Title highlights: Popup border highlights: PeekstackPopupBorder Unfocused popup border (links to FloatBorder) PeekstackPopupBorderFocused Focused popup border (links to Function) + PeekstackPopupBorderZoomed Zoomed popup border (links to WarningMsg) Stack view highlights: PeekstackStackViewIndex Entry index number (links to LineNr) diff --git a/lua/peekstack/commands.lua b/lua/peekstack/commands.lua index c9b1f5c..3c1a037 100644 --- a/lua/peekstack/commands.lua +++ b/lua/peekstack/commands.lua @@ -14,6 +14,7 @@ local COMMAND_NAMES = { "PeekstackCloseAll", "PeekstackQuickPeek", "PeekstackToggle", + "PeekstackZoom", } ---@param session PeekstackSession|table @@ -181,6 +182,13 @@ function M.setup() end end, {}) + vim.api.nvim_create_user_command("PeekstackZoom", function() + local toggled = require("peekstack.core.stack").toggle_zoom() + if not toggled then + notify.info("No popups in the current stack") + end + end, {}) + vim.api.nvim_create_user_command("PeekstackQuickPeek", function(opts) local provider = opts.args and opts.args ~= "" and opts.args or "lsp.definition" require("peekstack").peek(provider, { mode = "quick" }) diff --git a/lua/peekstack/config.lua b/lua/peekstack/config.lua index 6679e62..0f1c716 100644 --- a/lua/peekstack/config.lua +++ b/lua/peekstack/config.lua @@ -156,6 +156,7 @@ M.defaults = { promote_vsplit = "", promote_tab = "", toggle_stack_view = "os", + zoom = "", }, }, picker = { diff --git a/lua/peekstack/core/events.lua b/lua/peekstack/core/events.lua index dd92e40..b6c3658 100644 --- a/lua/peekstack/core/events.lua +++ b/lua/peekstack/core/events.lua @@ -125,12 +125,11 @@ function M.setup() local close_events = cfg.ui.quick_peek and cfg.ui.quick_peek.close_events or { "CursorMoved", "InsertEnter", "BufLeave", "WinLeave" } - for _, event in ipairs(close_events) do - vim.api.nvim_create_autocmd(event, { - group = group, - callback = close_ephemeral_popups, - }) - end + -- Merge all close events into a single autocmd for efficiency + vim.api.nvim_create_autocmd(close_events, { + group = group, + callback = close_ephemeral_popups, + }) vim.api.nvim_create_autocmd("User", { group = group, diff --git a/lua/peekstack/core/layout.lua b/lua/peekstack/core/layout.lua index 173756a..4e943bf 100644 --- a/lua/peekstack/core/layout.lua +++ b/lua/peekstack/core/layout.lua @@ -67,6 +67,22 @@ function M.compute(index) } end +---Compute fullscreen layout for zoomed popup. +---@param popup_count integer number of popups in the stack +---@return PeekstackLayoutResult +function M.compute_zoom(popup_count) + local columns = vim.o.columns + local lines = vim.o.lines - vim.o.cmdheight + local base = config.get().ui.layout.zindex_base + return { + width = columns, + height = lines, + row = 0, + col = 0, + zindex = base + popup_count + 1, + } +end + ---@param winid integer ---@param is_focused boolean local function set_popup_winhighlight(winid, is_focused) @@ -89,28 +105,52 @@ local function focused_popup_winid(stack) return nil end +---@param winid integer +---@param is_zoomed boolean +local function set_popup_zoom_winhighlight(winid, is_zoomed) + if not vim.api.nvim_win_is_valid(winid) then + return + end + if is_zoomed then + vim.wo[winid].winhighlight = "FloatBorder:PeekstackPopupBorderZoomed" + end +end + ---@param stack PeekstackStackModel function M.reflow(stack) local focused_winid = focused_popup_winid(stack) local base = config.get().ui.layout.zindex_base local top = base + #stack.popups + local zoomed_id = stack.zoomed_id for idx, popup in ipairs(stack.popups) do if popup.winid and vim.api.nvim_win_is_valid(popup.winid) then local is_focused = focused_winid ~= nil and popup.winid == focused_winid - local layout = M.compute(idx) - local z = layout.zindex - if is_focused then + local is_zoomed = zoomed_id ~= nil and popup.id == zoomed_id + + local lo + if is_zoomed then + lo = M.compute_zoom(#stack.popups) + else + lo = M.compute(idx) + end + + local z = lo.zindex + if not is_zoomed and is_focused then z = top end local win_opts = vim.tbl_extend("force", popup.win_opts or {}, { - row = layout.row, - col = layout.col, - width = layout.width, - height = layout.height, + row = lo.row, + col = lo.col, + width = lo.width, + height = lo.height, zindex = z, }) vim.api.nvim_win_set_config(popup.winid, win_opts) - set_popup_winhighlight(popup.winid, is_focused) + if is_zoomed then + set_popup_zoom_winhighlight(popup.winid, true) + else + set_popup_winhighlight(popup.winid, is_focused) + end end end end @@ -126,12 +166,24 @@ function M.update_focus_zindex(stack, focused_winid) local ui = config.get().ui local base = ui.layout.zindex_base local top = base + #stack.popups + local zoomed_id = stack.zoomed_id for idx, popup in ipairs(stack.popups) do if popup.winid and vim.api.nvim_win_is_valid(popup.winid) then local is_focused = popup.winid == focused_winid - local z = is_focused and top or (base + idx - 1) - local lo = M.compute(idx) + local is_zoomed = zoomed_id ~= nil and popup.id == zoomed_id + + local lo + if is_zoomed then + lo = M.compute_zoom(#stack.popups) + else + lo = M.compute(idx) + end + + local z = lo.zindex + if not is_zoomed and is_focused then + z = top + end local win_opts = vim.tbl_extend("force", popup.win_opts or {}, { row = lo.row, col = lo.col, @@ -140,7 +192,11 @@ function M.update_focus_zindex(stack, focused_winid) zindex = z, }) pcall(vim.api.nvim_win_set_config, popup.winid, win_opts) - set_popup_winhighlight(popup.winid, is_focused) + if is_zoomed then + set_popup_zoom_winhighlight(popup.winid, true) + else + set_popup_winhighlight(popup.winid, is_focused) + end end end end diff --git a/lua/peekstack/core/stack.lua b/lua/peekstack/core/stack.lua index dbcf4cc..f2002ef 100644 --- a/lua/peekstack/core/stack.lua +++ b/lua/peekstack/core/stack.lua @@ -283,6 +283,10 @@ function M.push(location, opts) if stack.hidden then M.toggle(stack.root_winid) end + -- Clear zoom before pushing so the new top popup gets normal layout + if stack.zoomed_id then + stack.zoomed_id = nil + end local model = popup.create(location, create_opts) if not model then return nil @@ -325,6 +329,10 @@ end local function close_stack_item(stack, idx, item) local current_win = vim.api.nvim_get_current_win() local should_restore_focus = item.winid == current_win and vim.w[current_win].peekstack_popup_id ~= nil + -- Clear zoom if the zoomed popup is being closed + if stack.zoomed_id == item.id then + stack.zoomed_id = nil + end -- Remove from popups BEFORE closing the window to prevent -- WinClosed autocmd from re-entering and processing the same popup. table.remove(stack.popups, idx) @@ -650,6 +658,9 @@ function M.handle_win_closed(winid) if stack.focused_id == item.id then focused_removed = true end + if stack.zoomed_id == item.id then + stack.zoomed_id = nil + end emit_popup_event("PeekstackClose", item, root_winid) feedback.highlight_origin(item.origin) table.remove(stack.popups, idx) @@ -719,6 +730,9 @@ function M.handle_buf_wipeout(bufnr) for idx = #stack.popups, 1, -1 do local item = stack.popups[idx] if item.bufnr == bufnr then + if stack.zoomed_id == item.id then + stack.zoomed_id = nil + end unindex_popup(item) table.remove(stack.popups, idx) end @@ -795,6 +809,9 @@ function M.handle_origin_wipeout(bufnr) for idx = #stack.popups, 1, -1 do local item = stack.popups[idx] if should_close_for_origin(item) then + if stack.zoomed_id == item.id then + stack.zoomed_id = nil + end popup.close(item) unindex_popup(item) table.remove(stack.popups, idx) @@ -855,6 +872,8 @@ function M.toggle(winid) end if not stack.hidden then + -- Clear zoom state before hiding + stack.zoomed_id = nil -- Move focus back to root window before hiding if vim.api.nvim_win_is_valid(stack.root_winid) then vim.api.nvim_set_current_win(stack.root_winid) @@ -897,11 +916,41 @@ function M.is_hidden(winid) return stack.hidden == true end +---Toggle zoom on the top popup. When zoomed, the popup fills the +---entire editor. Calling again restores the normal layout. +---@param winid? integer +---@return boolean +function M.toggle_zoom(winid) + deps() + local stack = ensure_stack(winid) + if #stack.popups == 0 or stack.hidden then + return false + end + + local top = stack.popups[#stack.popups] + if stack.zoomed_id == top.id then + stack.zoomed_id = nil + else + stack.zoomed_id = top.id + end + layout.reflow(stack) + return true +end + +---Check whether the current stack has a zoomed popup. +---@param winid? integer +---@return boolean +function M.is_zoomed(winid) + local stack = ensure_stack(winid) + return stack.zoomed_id ~= nil +end + --- Close all popups in the current (or given) window's stack. ---@param winid? integer function M.close_all(winid) deps() local stack = ensure_stack(winid) + stack.zoomed_id = nil -- When hidden, windows are already closed; just clear hidden state -- and record history for each popup. if stack.hidden then diff --git a/lua/peekstack/init.lua b/lua/peekstack/init.lua index 04857c2..77cfb0f 100644 --- a/lua/peekstack/init.lua +++ b/lua/peekstack/init.lua @@ -32,6 +32,7 @@ local function set_hl() vim.api.nvim_set_hl(0, "PeekstackStackViewLine", { default = true, link = "LineNr" }) vim.api.nvim_set_hl(0, "PeekstackPopupBorder", { default = true, link = "FloatBorder" }) vim.api.nvim_set_hl(0, "PeekstackPopupBorderFocused", { default = true, link = "Function" }) + vim.api.nvim_set_hl(0, "PeekstackPopupBorderZoomed", { default = true, link = "WarningMsg" }) vim.api.nvim_set_hl(0, "PeekstackTitleKindError", { default = true, link = "DiagnosticError" }) vim.api.nvim_set_hl(0, "PeekstackTitleKindWarn", { default = true, link = "DiagnosticWarn" }) vim.api.nvim_set_hl(0, "PeekstackTitleKindInfo", { default = true, link = "DiagnosticInfo" }) diff --git a/lua/peekstack/types.lua b/lua/peekstack/types.lua index b2cd982..a352c7f 100644 --- a/lua/peekstack/types.lua +++ b/lua/peekstack/types.lua @@ -63,6 +63,7 @@ ---@field layout_state any ---@field focused_id integer? ---@field hidden boolean? +---@field zoomed_id integer? ---@class PeekstackUserEventData ---@field event string @@ -250,6 +251,7 @@ ---@field promote_vsplit string ---@field promote_tab string ---@field toggle_stack_view string +---@field zoom string ---@class PeekstackConfigUI ---@field layout PeekstackConfigLayout diff --git a/lua/peekstack/ui/diagnostics.lua b/lua/peekstack/ui/diagnostics.lua index ac06baf..dd03384 100644 --- a/lua/peekstack/ui/diagnostics.lua +++ b/lua/peekstack/ui/diagnostics.lua @@ -8,39 +8,16 @@ local NS = vim.api.nvim_create_namespace("peekstack_diagnostics") ---@field ids integer[] ---@param kind? integer +---@param prefix string "DiagnosticVirtualText" or "DiagnosticUnderline" ---@return string -local function virtual_text_hl(kind) - if kind == vim.diagnostic.severity.ERROR then - return "DiagnosticVirtualTextError" - end - if kind == vim.diagnostic.severity.WARN then - return "DiagnosticVirtualTextWarn" - end - if kind == vim.diagnostic.severity.INFO then - return "DiagnosticVirtualTextInfo" - end - if kind == vim.diagnostic.severity.HINT then - return "DiagnosticVirtualTextHint" - end - return "DiagnosticVirtualTextInfo" -end - ----@param kind? integer ----@return string -local function underline_hl(kind) - if kind == vim.diagnostic.severity.ERROR then - return "DiagnosticUnderlineError" - end - if kind == vim.diagnostic.severity.WARN then - return "DiagnosticUnderlineWarn" - end - if kind == vim.diagnostic.severity.INFO then - return "DiagnosticUnderlineInfo" - end - if kind == vim.diagnostic.severity.HINT then - return "DiagnosticUnderlineHint" - end - return "DiagnosticUnderlineInfo" +local function severity_hl(kind, prefix) + local suffix = ({ + [vim.diagnostic.severity.ERROR] = "Error", + [vim.diagnostic.severity.WARN] = "Warn", + [vim.diagnostic.severity.INFO] = "Info", + [vim.diagnostic.severity.HINT] = "Hint", + })[kind] or "Info" + return prefix .. suffix end ---@param text string @@ -103,7 +80,7 @@ function M.decorate(popup) local ids = {} local virt_lines = {} - local virt_hl = virtual_text_hl(location.kind) + local virt_hl = severity_hl(location.kind, "DiagnosticVirtualText") for _, msg in ipairs(split_message(text)) do local msg_text = msg ~= "" and msg or " " table.insert(virt_lines, { { msg_text, virt_hl } }) @@ -117,7 +94,7 @@ function M.decorate(popup) table.insert(ids, id) end - local underline = underline_hl(location.kind) + local underline = severity_hl(location.kind, "DiagnosticUnderline") if underline ~= "" then local id = vim.api.nvim_buf_set_extmark(bufnr, NS, line, col, { end_row = end_line, diff --git a/lua/peekstack/ui/keymaps.lua b/lua/peekstack/ui/keymaps.lua index dc1f584..2ca0eab 100644 --- a/lua/peekstack/ui/keymaps.lua +++ b/lua/peekstack/ui/keymaps.lua @@ -89,6 +89,11 @@ function M.apply_popup(popup) map(popup.bufnr, keys.toggle_stack_view, function() stack_view.toggle() end, "Peekstack stack view") + + map(popup.bufnr, keys.zoom, function() + local stack = require("peekstack.core.stack") + stack.toggle_zoom() + end, "Peekstack zoom") end return M diff --git a/lua/peekstack/ui/stack_view/keymaps.lua b/lua/peekstack/ui/stack_view/keymaps.lua index e69f8f2..d9d9422 100644 --- a/lua/peekstack/ui/stack_view/keymaps.lua +++ b/lua/peekstack/ui/stack_view/keymaps.lua @@ -187,6 +187,7 @@ local function toggle_help(s, deps) "/ Filter list", "gg/G Jump to first/last stack item", "j/k Move cursor by stack item", + "z Toggle zoom (maximize top popup)", "q Close stack view", "? Toggle this help", } @@ -460,6 +461,11 @@ function M.apply(s, deps) end) end, { buffer = s.bufnr, nowait = true, silent = true }) + vim.keymap.set("n", "z", function() + local stack = require("peekstack.core.stack") + stack.toggle_zoom(s.root_winid) + end, { buffer = s.bufnr, nowait = true, silent = true }) + vim.keymap.set("n", "?", function() toggle_help(s, deps) end, { buffer = s.bufnr, nowait = true, silent = true }) diff --git a/lua/peekstack/util/fs.lua b/lua/peekstack/util/fs.lua index 5f11faf..db8a99c 100644 --- a/lua/peekstack/util/fs.lua +++ b/lua/peekstack/util/fs.lua @@ -2,6 +2,20 @@ local M = {} ---@type string? local cached_repo_root = nil + +---Check if a window ID is valid (non-nil and exists) +---@param winid integer? +---@return boolean +function M.win_is_valid(winid) + return winid ~= nil and vim.api.nvim_win_is_valid(winid) +end + +---Check if a buffer number is valid (non-nil and exists) +---@param bufnr integer? +---@return boolean +function M.buf_is_valid(bufnr) + return bufnr ~= nil and vim.api.nvim_buf_is_valid(bufnr) +end ---@type string? local cached_repo_cwd = nil ---@type string? @@ -84,9 +98,7 @@ end ---@param path string ---@return string function M.ensure_dir(path) - if vim.fn.isdirectory(path) == 1 then - return path - end + -- mkdir with "p" flag is atomic and handles existing directories vim.fn.mkdir(path, "p") return path end diff --git a/tests/fs_spec.lua b/tests/fs_spec.lua index 6346ac2..79a6986 100644 --- a/tests/fs_spec.lua +++ b/tests/fs_spec.lua @@ -30,6 +30,54 @@ describe("fs", function() end) end) + describe("win_is_valid", function() + it("returns false for nil input", function() + assert.is_false(fs.win_is_valid(nil)) + end) + + it("returns true for current window", function() + assert.is_true(fs.win_is_valid(vim.api.nvim_get_current_win())) + end) + + it("returns false for closed window", function() + local bufnr = vim.api.nvim_create_buf(false, true) + local winid = vim.api.nvim_open_win(bufnr, false, { + relative = "editor", + row = 0, + col = 0, + width = 1, + height = 1, + style = "minimal", + }) + + assert.is_true(fs.win_is_valid(winid)) + vim.api.nvim_win_close(winid, true) + assert.is_false(fs.win_is_valid(winid)) + + if vim.api.nvim_buf_is_valid(bufnr) then + vim.api.nvim_buf_delete(bufnr, { force = true }) + end + end) + end) + + describe("buf_is_valid", function() + it("returns false for nil input", function() + assert.is_false(fs.buf_is_valid(nil)) + end) + + it("returns true for current buffer", function() + assert.is_true(fs.buf_is_valid(vim.api.nvim_get_current_buf())) + end) + + it("returns false for deleted buffer", function() + local bufnr = vim.api.nvim_create_buf(false, true) + assert.is_true(fs.buf_is_valid(bufnr)) + + vim.api.nvim_buf_delete(bufnr, { force = true }) + assert.is_false(fs.buf_is_valid(bufnr)) + end) + end) + describe("scope_path", function() local original_ensure_dir diff --git a/tests/zoom_spec.lua b/tests/zoom_spec.lua new file mode 100644 index 0000000..105ae60 --- /dev/null +++ b/tests/zoom_spec.lua @@ -0,0 +1,177 @@ +local stack = require("peekstack.core.stack") +local config = require("peekstack.config") +local events = require("peekstack.core.events") +local helpers = require("tests.helpers") + +describe("stack.toggle_zoom", function() + before_each(function() + stack._reset() + config.setup({}) + events.setup() + end) + + after_each(function() + local s = stack.current_stack() + if s.hidden then + stack.toggle() + end + for i = #s.popups, 1, -1 do + stack.close(s.popups[i].id) + end + stack._reset() + end) + + it("returns false on empty stack", function() + assert.is_false(stack.toggle_zoom()) + assert.is_false(stack.is_zoomed()) + end) + + it("zooms the top popup to fullscreen", function() + local loc = helpers.make_location() + stack.push(loc) + local m2 = stack.push(loc) + + assert.is_true(stack.toggle_zoom()) + assert.is_true(stack.is_zoomed()) + + local cfg = vim.api.nvim_win_get_config(m2.winid) + local expected_w = vim.o.columns + local expected_h = vim.o.lines - vim.o.cmdheight + + -- The zoomed popup should be at least close to fullscreen. + -- nvim_win_set_config may adjust for border, so allow tolerance. + assert.is_true(cfg.width >= expected_w - 2) + assert.is_true(cfg.height >= expected_h - 2) + end) + + it("unzooms on second toggle", function() + local loc = helpers.make_location() + stack.push(loc) + local m2 = stack.push(loc) + + stack.toggle_zoom() + assert.is_true(stack.is_zoomed()) + + stack.toggle_zoom() + assert.is_false(stack.is_zoomed()) + + -- After unzoom, size should match normal layout + local layout = require("peekstack.core.layout") + local expected = layout.compute(2) + local cfg = vim.api.nvim_win_get_config(m2.winid) + assert.equals(expected.width, cfg.width) + assert.equals(expected.height, cfg.height) + end) + + it("clears zoom when the zoomed popup is closed", function() + local loc = helpers.make_location() + stack.push(loc) + local m2 = stack.push(loc) + + stack.toggle_zoom() + assert.is_true(stack.is_zoomed()) + + stack.close(m2.id) + assert.is_false(stack.is_zoomed()) + end) + + it("clears zoom on push", function() + local loc = helpers.make_location() + stack.push(loc) + + stack.toggle_zoom() + assert.is_true(stack.is_zoomed()) + + stack.push(loc) + assert.is_false(stack.is_zoomed()) + end) + + it("clears zoom when hiding via toggle visibility", function() + local loc = helpers.make_location() + stack.push(loc) + + stack.toggle_zoom() + assert.is_true(stack.is_zoomed()) + + stack.toggle() -- hide + assert.is_true(stack.is_hidden()) + assert.is_false(stack.is_zoomed()) + end) + + it("returns false when stack is hidden", function() + local loc = helpers.make_location() + stack.push(loc) + + stack.toggle() -- hide + assert.is_false(stack.toggle_zoom()) + assert.is_false(stack.is_zoomed()) + end) + + it("clears zoom on close_all", function() + local loc = helpers.make_location() + stack.push(loc) + stack.push(loc) + + stack.toggle_zoom() + assert.is_true(stack.is_zoomed()) + + stack.close_all() + assert.is_false(stack.is_zoomed()) + end) + + it("sets zoomed border highlight", function() + local loc = helpers.make_location() + stack.push(loc) + local m = stack.push(loc) + + stack.toggle_zoom() + + local whl = vim.wo[m.winid].winhighlight + assert.truthy(whl:find("PeekstackPopupBorderZoomed")) + end) + + it("restores normal border highlight on unzoom", function() + local loc = helpers.make_location() + stack.push(loc) + local m = stack.push(loc) + + stack.toggle_zoom() + stack.toggle_zoom() + + local whl = vim.wo[m.winid].winhighlight + assert.falsy(whl:find("PeekstackPopupBorderZoomed")) + end) + + it("zoomed zindex is above all popups regardless of zindex_base", function() + config.setup({ ui = { layout = { zindex_base = 300 } } }) + events.setup() + local loc = helpers.make_location() + stack.push(loc) + local m2 = stack.push(loc) + + stack.toggle_zoom() + + local s = stack.current_stack() + local zoomed_cfg = vim.api.nvim_win_get_config(m2.winid) + -- Zoomed zindex must be higher than any non-zoomed popup + for _, p in ipairs(s.popups) do + if p.id ~= m2.id and p.winid and vim.api.nvim_win_is_valid(p.winid) then + local other_cfg = vim.api.nvim_win_get_config(p.winid) + assert.is_true(zoomed_cfg.zindex > other_cfg.zindex) + end + end + end) + + it("clears zoom when popup window is closed externally", function() + local loc = helpers.make_location() + stack.push(loc) + local m2 = stack.push(loc) + + stack.toggle_zoom() + assert.is_true(stack.is_zoomed()) + + -- Simulate external close (e.g., nvim_win_close) + vim.api.nvim_win_close(m2.winid, true) + assert.is_false(stack.is_zoomed()) + end) +end)