Skip to content

Commit e875c6d

Browse files
committed
fix(stack): scope quick peek closes and preserve wipeout history
1 parent 091924d commit e875c6d

5 files changed

Lines changed: 207 additions & 30 deletions

File tree

lua/peekstack/core/events.lua

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,9 @@ local function ensure_popup_cursor_tracking(group, bufnr)
5555
})
5656
end
5757

58-
---Close all ephemeral popups across all stacks and reflow
58+
---Close ephemeral popups that belong to the current root window.
5959
local function close_ephemeral_popups()
60-
stack.close_ephemerals()
60+
stack.close_ephemerals(vim.api.nvim_get_current_win())
6161
end
6262

6363
---Check if a window is a floating popup

lua/peekstack/core/stack/events.lua

Lines changed: 75 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,47 @@ local function deps()
1414
end
1515
end
1616

17+
---@param stack PeekstackStackModel
18+
---@param idx integer
19+
---@param item PeekstackPopupModel
20+
---@param opts? { close_window?: boolean, highlight_origin?: boolean }
21+
local function remove_stack_popup(stack, idx, item, opts)
22+
opts = opts or {}
23+
if stack.zoomed_id == item.id then
24+
stack.zoomed_id = nil
25+
end
26+
if opts.highlight_origin ~= false then
27+
feedback.highlight_origin(item.origin)
28+
end
29+
common.emit_popup_event("PeekstackClose", item, stack.root_winid)
30+
history.push_entry(stack, history.build_entry(item, idx))
31+
user_events.emit("PeekstackHistoryPush", {
32+
popup_id = item.id,
33+
location = item.location,
34+
root_winid = stack.root_winid,
35+
})
36+
state.unindex_popup(item)
37+
table.remove(stack.popups, idx)
38+
if opts.close_window ~= false and item.winid and vim.api.nvim_win_is_valid(item.winid) then
39+
popup.close(item)
40+
end
41+
end
42+
43+
---@param id integer
44+
---@param item PeekstackPopupModel
45+
---@param opts? { close_window?: boolean }
46+
local function remove_ephemeral(id, item, opts)
47+
opts = opts or {}
48+
if opts.close_window ~= false and item.winid and vim.api.nvim_win_is_valid(item.winid) then
49+
popup.close(item)
50+
end
51+
state.unregister_ephemeral(id)
52+
user_events.emit(
53+
"PeekstackClose",
54+
user_events.build_popup_data(item, item.origin and item.origin.winid or 0, { ephemeral = true })
55+
)
56+
end
57+
1758
---@param winid integer
1859
function M.handle_win_closed(winid)
1960
deps()
@@ -83,21 +124,32 @@ function M.handle_buf_wipeout(bufnr)
83124
end
84125
for id, item in pairs(state.ephemerals) do
85126
if item.bufnr == bufnr then
86-
state.unregister_ephemeral(id)
127+
remove_ephemeral(id, item, { close_window = false })
87128
end
88129
end
89130
for _, stack in pairs(state.stacks) do
131+
local removed = false
132+
local focused_removed = false
90133
for idx = #stack.popups, 1, -1 do
91134
local item = stack.popups[idx]
92135
if item.bufnr == bufnr then
93-
if stack.zoomed_id == item.id then
94-
stack.zoomed_id = nil
136+
if stack.focused_id == item.id then
137+
focused_removed = true
95138
end
96-
state.unindex_popup(item)
97-
table.remove(stack.popups, idx)
139+
remove_stack_popup(stack, idx, item, { close_window = false })
140+
removed = true
98141
end
99142
end
100-
layout.reflow(stack)
143+
if removed then
144+
if focused_removed then
145+
if #stack.popups > 0 then
146+
stack.focused_id = stack.popups[#stack.popups].id
147+
else
148+
stack.focused_id = nil
149+
end
150+
end
151+
layout.reflow(stack)
152+
end
101153
end
102154
end
103155

@@ -121,23 +173,32 @@ function M.handle_origin_wipeout(bufnr)
121173
end
122174
for id, item in pairs(state.ephemerals) do
123175
if should_close_for_origin(item) then
124-
popup.close(item)
125-
state.unregister_ephemeral(id)
176+
remove_ephemeral(id, item)
126177
end
127178
end
128179
for _, stack in pairs(state.stacks) do
180+
local removed = false
181+
local focused_removed = false
129182
for idx = #stack.popups, 1, -1 do
130183
local item = stack.popups[idx]
131184
if should_close_for_origin(item) then
132-
if stack.zoomed_id == item.id then
133-
stack.zoomed_id = nil
185+
if stack.focused_id == item.id then
186+
focused_removed = true
187+
end
188+
remove_stack_popup(stack, idx, item)
189+
removed = true
190+
end
191+
end
192+
if removed then
193+
if focused_removed then
194+
if #stack.popups > 0 then
195+
stack.focused_id = stack.popups[#stack.popups].id
196+
else
197+
stack.focused_id = nil
134198
end
135-
popup.close(item)
136-
state.unindex_popup(item)
137-
table.remove(stack.popups, idx)
138199
end
200+
layout.reflow(stack)
139201
end
140-
layout.reflow(stack)
141202
end
142203
end
143204

lua/peekstack/core/stack/operations.lua

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -432,27 +432,43 @@ function M.close_stale(now_ms, opts)
432432
end
433433
end
434434

435-
function M.close_ephemerals()
435+
---@param winid? integer
436+
function M.close_ephemerals(winid)
436437
deps()
438+
local target_root_winid = nil
439+
if winid ~= nil or vim.api.nvim_get_current_win() ~= nil then
440+
target_root_winid = state.get_root_winid(winid)
441+
end
442+
437443
for _, stack in pairs(state.stacks) do
438-
local removed = false
439-
for idx = #stack.popups, 1, -1 do
440-
local item = stack.popups[idx]
441-
if item.ephemeral then
442-
popup.close(item)
443-
state.unindex_popup(item)
444-
table.remove(stack.popups, idx)
445-
removed = true
444+
if target_root_winid == nil or stack.root_winid == target_root_winid then
445+
local removed = false
446+
for idx = #stack.popups, 1, -1 do
447+
local item = stack.popups[idx]
448+
if item.ephemeral then
449+
popup.close(item)
450+
state.unindex_popup(item)
451+
table.remove(stack.popups, idx)
452+
removed = true
453+
end
454+
end
455+
if removed then
456+
layout.reflow(stack)
446457
end
447-
end
448-
if removed then
449-
layout.reflow(stack)
450458
end
451459
end
452460

453461
for id, item in pairs(state.ephemerals) do
454-
popup.close(item)
455-
state.unregister_ephemeral(id)
462+
local entry = state.lookup_by_id(item.id)
463+
local root_winid = entry and entry.root_winid or nil
464+
if target_root_winid == nil or root_winid == target_root_winid then
465+
popup.close(item)
466+
state.unregister_ephemeral(id)
467+
user_events.emit(
468+
"PeekstackClose",
469+
user_events.build_popup_data(item, item.origin and item.origin.winid or 0, { ephemeral = true })
470+
)
471+
end
456472
end
457473
end
458474

tests/events_spec.lua

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,41 @@ describe("peekstack.core.events", function()
133133
assert.equals(1, #buf_leave)
134134
assert.equals(2, #win_leave)
135135
end)
136+
137+
it("closes quick peek popups only for the current root window", function()
138+
local location = {
139+
uri = vim.uri_from_bufnr(0),
140+
range = { start = { line = 0, character = 0 }, ["end"] = { line = 0, character = 10 } },
141+
provider = "test",
142+
}
143+
144+
config.setup({
145+
ui = {
146+
quick_peek = { close_events = { "CursorMoved" } },
147+
popup = { auto_close = { enabled = false } },
148+
},
149+
})
150+
events.setup()
151+
152+
local left_win = vim.api.nvim_get_current_win()
153+
vim.api.nvim_cmd({ cmd = "vsplit" }, {})
154+
local right_win = vim.api.nvim_get_current_win()
155+
156+
vim.api.nvim_set_current_win(left_win)
157+
local left_popup = stack.push(location, { stack = false })
158+
assert.is_not_nil(left_popup)
159+
160+
vim.api.nvim_set_current_win(right_win)
161+
local right_popup = stack.push(location, { stack = false })
162+
assert.is_not_nil(right_popup)
163+
164+
vim.api.nvim_set_current_win(left_win)
165+
vim.api.nvim_exec_autocmds("CursorMoved", { modeline = false })
166+
167+
assert.is_nil(stack._ephemerals()[left_popup.id])
168+
assert.is_not_nil(stack._ephemerals()[right_popup.id])
169+
170+
stack.close(right_popup.id)
171+
vim.api.nvim_cmd({ cmd = "only" }, {})
172+
end)
136173
end)

tests/stack_spec.lua

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,13 @@ describe("stack.handle_origin_wipeout", function()
280280
id = 1,
281281
origin = { bufnr = 10 },
282282
origin_is_popup = true,
283+
location = helpers.make_location(),
283284
},
284285
{
285286
id = 2,
286287
origin = { bufnr = 10 },
287288
origin_is_popup = false,
289+
location = helpers.make_location(),
288290
},
289291
}
290292

@@ -293,6 +295,67 @@ describe("stack.handle_origin_wipeout", function()
293295
assert.equals(1, #s.popups)
294296
assert.equals(1, s.popups[1].id)
295297
end)
298+
299+
it("pushes history and emits events when the origin buffer is wiped", function()
300+
local received = {}
301+
local group = vim.api.nvim_create_augroup("PeekstackOriginWipeoutEvents", { clear = true })
302+
vim.api.nvim_create_autocmd("User", {
303+
group = group,
304+
pattern = { "PeekstackClose", "PeekstackHistoryPush" },
305+
callback = function(args)
306+
table.insert(received, args.match)
307+
end,
308+
})
309+
310+
local model = stack.push(helpers.make_location())
311+
assert.is_not_nil(model)
312+
313+
stack.handle_origin_wipeout(model.origin.bufnr)
314+
315+
local history = stack.history_list()
316+
assert.equals(1, #history)
317+
assert.equals(model.id, history[1].popup_id)
318+
assert.is_nil(stack.find_by_id(model.id))
319+
assert.same({ "PeekstackClose", "PeekstackHistoryPush" }, received)
320+
321+
pcall(vim.api.nvim_del_augroup_by_id, group)
322+
end)
323+
end)
324+
325+
describe("stack.handle_buf_wipeout", function()
326+
before_each(function()
327+
stack._reset()
328+
config.setup({})
329+
end)
330+
331+
after_each(function()
332+
stack._reset()
333+
end)
334+
335+
it("pushes history and emits events when the popup buffer is wiped", function()
336+
local received = {}
337+
local group = vim.api.nvim_create_augroup("PeekstackBufWipeoutEvents", { clear = true })
338+
vim.api.nvim_create_autocmd("User", {
339+
group = group,
340+
pattern = { "PeekstackClose", "PeekstackHistoryPush" },
341+
callback = function(args)
342+
table.insert(received, args.match)
343+
end,
344+
})
345+
346+
local model = stack.push(helpers.make_location())
347+
assert.is_not_nil(model)
348+
349+
stack.handle_buf_wipeout(model.bufnr)
350+
351+
local history = stack.history_list()
352+
assert.equals(1, #history)
353+
assert.equals(model.id, history[1].popup_id)
354+
assert.is_nil(stack.find_by_id(model.id))
355+
assert.same({ "PeekstackClose", "PeekstackHistoryPush" }, received)
356+
357+
pcall(vim.api.nvim_del_augroup_by_id, group)
358+
end)
296359
end)
297360

298361
describe("stack.close_by_id", function()

0 commit comments

Comments
 (0)