Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions lua/overlook/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,37 @@ local function setup_autocmd()
state.cleanup_touched_buffer(args.buf)
end,
})

-- Snapshot the popup's live buffer+cursor into opts whenever focus leaves
-- it, so a later restore reopens what the user was last viewing (after gd,
-- gf, manual cursor moves, etc.). close_all suppresses WinLeave via
-- eventignore so this autocmd doesn't fire during bulk close -- Popup:close
-- snapshots there directly. This autocmd covers the other paths: user
-- switches focus to host (popup still open, will be restored later) and
-- user runs `:close` from inside a popup (WinLeave fires before WinClosed).
vim.api.nvim_create_autocmd("WinLeave", {
group = augroup,
callback = function()
local winid = vim.api.nvim_get_current_win()
-- Read via the API rather than vim.w because tests reassign
-- `vim.w = {}` which detaches the proxy from real window-locals; the
-- API call still reaches the underlying var.
local ok, is_popup = pcall(vim.api.nvim_win_get_var, winid, "is_overlook_popup")
if not ok or not is_popup then
return
end
local w = require("overlook.window").find_by_popup_winid(winid)
if not w then
return
end
for _, popup in ipairs(w.stack.items) do
if popup.winid == winid then
popup:snapshot_state()
return
end
end
end,
})
end

--- Initialize and configure overlook.nvim with user-provided options.
Expand Down
21 changes: 21 additions & 0 deletions lua/overlook/popup.lua
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,29 @@ function Popup:open(enter)
return true
end

---Capture the popup's live buffer + cursor into self.opts so restore reopens
---what the user was last looking at, not the original peek target. Called
---from two places: (1) Popup:close, which catches the close_all path
---(eventignore suppresses WinLeave there); (2) the WinLeave autocmd in
---init.lua, which catches focus-leaves and external `:close` paths (where
---Popup:close is not invoked because the popup closes via nvim's own
---WinClosed -> on_popup_closed route).
function Popup:snapshot_state()
if not self:is_valid() then
return
end
self.opts.target_bufnr = api.nvim_win_get_buf(self.winid)
local cursor = api.nvim_win_get_cursor(self.winid)
self.opts.lnum = cursor[1]
-- opts.col is 1-indexed (the adapters use vim.fn.getpos which returns
-- 1-indexed); nvim_win_get_cursor returns 0-indexed. Convert.
-- set_cursor_position converts back via math.max(0, col - 1) on restore.
self.opts.col = cursor[2] + 1
end

function Popup:close()
if self:is_valid() then
self:snapshot_state()
pcall(api.nvim_win_close, self.winid, false)
end
end
Expand Down
81 changes: 81 additions & 0 deletions tests/spec/window_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,87 @@ describe("Window:restore_all rollback when a trashed popup is unrestorable", fun
end)
end)

describe("Window: restore preserves live buffer and cursor", function()
local Window = require("overlook.window")

before_each(function()
Window.instances = {}
vim.w = {}
-- Register the WinLeave snapshot autocmd (idempotent: the augroup uses
-- clear = true).
require("overlook").setup {}
end)

-- When the user navigates inside a popup (gd, gf, :edit, etc.) the popup's
-- window buffer changes but popup.opts.target_bufnr is captured once at
-- peek time and never refreshed -- restore would reopen the ORIGINAL peek
-- target instead of what the user was last viewing. Same applies to
-- cursor position. Two snapshot paths cover this:
-- (1) WinLeave autocmd in init.lua -- focus leaves the popup
-- (2) Popup:close -- close_all suppresses WinLeave
it("snapshots via WinLeave when user switches focus before close_all", function()
local host = api.nvim_get_current_win()
local buf_a = make_buf()
local buf_b = make_buf()

local w = Window.current()
local p = w:open_popup { target_bufnr = buf_a, lnum = 1, col = 1 }
assert.is_not_nil(p)

-- Mimic gd: switch popup's buffer and move the cursor.
api.nvim_win_set_buf(p.winid, buf_b)
api.nvim_win_set_cursor(p.winid, { 3, 0 }) -- 0-indexed col 0 == 1-indexed col 1

-- User switches focus -> WinLeave fires on the popup -> snapshot.
api.nvim_set_current_win(host)

-- Live state was written back to opts.
assert.are.equal(buf_b, p.opts.target_bufnr)
assert.are.equal(3, p.opts.lnum)
assert.are.equal(1, p.opts.col) -- opts.col is 1-indexed

-- Close + restore should reopen buf_b at the snapshotted cursor.
w:close_all()
w:restore()
local restored = w.stack:top()
assert.are.equal(buf_b, api.nvim_win_get_buf(restored.winid))
local cursor = api.nvim_win_get_cursor(restored.winid)
assert.are.equal(3, cursor[1])
assert.are.equal(0, cursor[2])

w:close_all()
end)

it("snapshots via Popup:close when user is inside the popup at close_all time", function()
local buf_a = make_buf()
local buf_b = make_buf()

local w = Window.current()
local p = w:open_popup { target_bufnr = buf_a, lnum = 1, col = 1 }
assert.is_not_nil(p)

api.nvim_win_set_buf(p.winid, buf_b)
api.nvim_win_set_cursor(p.winid, { 2, 0 })

-- Do NOT leave focus first. close_all wraps the loop in eventignore so
-- WinLeave is suppressed; Popup:close's snapshot must catch the live state.
w:close_all()

assert.are.equal(buf_b, w.stack.trash[1].opts.target_bufnr)
assert.are.equal(2, w.stack.trash[1].opts.lnum)
assert.are.equal(1, w.stack.trash[1].opts.col)

w:restore()
local restored = w.stack:top()
assert.are.equal(buf_b, api.nvim_win_get_buf(restored.winid))
local cursor = api.nvim_win_get_cursor(restored.winid)
assert.are.equal(2, cursor[1])
assert.are.equal(0, cursor[2])

w:close_all()
end)
end)

describe("Popup:open(false) opens without taking focus", function()
local Popup = require("overlook.popup")

Expand Down
Loading