Skip to content

Commit ffd1e88

Browse files
committed
perf(stack): add O(1) popup lookup indexes
1 parent 6ba5179 commit ffd1e88

2 files changed

Lines changed: 243 additions & 38 deletions

File tree

lua/peekstack/core/stack.lua

Lines changed: 187 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,128 @@ local stacks = {}
2828
local ephemerals = {}
2929
---@type table<integer, boolean>
3030
local stack_view_wins = {}
31+
---@class PeekstackPopupLookupEntry
32+
---@field popup PeekstackPopupModel
33+
---@field root_winid integer?
34+
---@type table<integer, PeekstackPopupLookupEntry>
35+
local popup_by_id = {}
36+
---@type table<integer, PeekstackPopupLookupEntry>
37+
local popup_by_winid = {}
38+
39+
---@param model PeekstackPopupModel
40+
local function unindex_popup(model)
41+
if not model then
42+
return
43+
end
44+
45+
local removed = false
46+
47+
local id = model.id
48+
if id ~= nil then
49+
local entry_by_id = popup_by_id[id]
50+
if entry_by_id and entry_by_id.popup == model then
51+
popup_by_id[id] = nil
52+
removed = true
53+
end
54+
end
55+
56+
local winid = model.winid
57+
if winid ~= nil then
58+
local entry_by_winid = popup_by_winid[winid]
59+
if entry_by_winid and entry_by_winid.popup == model then
60+
popup_by_winid[winid] = nil
61+
removed = true
62+
end
63+
end
64+
65+
if removed then
66+
return
67+
end
68+
69+
-- Guard against tests mutating id/winid directly.
70+
for id, entry in pairs(popup_by_id) do
71+
if entry.popup == model then
72+
popup_by_id[id] = nil
73+
end
74+
end
75+
for wid, entry in pairs(popup_by_winid) do
76+
if entry.popup == model then
77+
popup_by_winid[wid] = nil
78+
end
79+
end
80+
end
81+
82+
---@param model PeekstackPopupModel
83+
---@param root_winid integer?
84+
local function index_popup(model, root_winid)
85+
unindex_popup(model)
86+
87+
local entry = {
88+
popup = model,
89+
root_winid = root_winid,
90+
}
91+
if model.id ~= nil then
92+
popup_by_id[model.id] = entry
93+
end
94+
if model.winid ~= nil then
95+
popup_by_winid[model.winid] = entry
96+
end
97+
end
98+
99+
---@param id integer
100+
---@return PeekstackPopupLookupEntry?
101+
local function lookup_by_id(id)
102+
local entry = popup_by_id[id]
103+
if entry and entry.popup and entry.popup.id == id then
104+
return entry
105+
end
106+
popup_by_id[id] = nil
107+
108+
for root_winid, stack in pairs(stacks) do
109+
for _, item in ipairs(stack.popups) do
110+
if item.id == id then
111+
index_popup(item, root_winid)
112+
return popup_by_id[id]
113+
end
114+
end
115+
end
116+
117+
local ephemeral = ephemerals[id]
118+
if ephemeral then
119+
index_popup(ephemeral, nil)
120+
return popup_by_id[id]
121+
end
122+
123+
return nil
124+
end
125+
126+
---@param winid integer
127+
---@return PeekstackPopupLookupEntry?
128+
local function lookup_by_winid(winid)
129+
local entry = popup_by_winid[winid]
130+
if entry and entry.popup and entry.popup.winid == winid then
131+
return entry
132+
end
133+
popup_by_winid[winid] = nil
134+
135+
for root_winid, stack in pairs(stacks) do
136+
for _, item in ipairs(stack.popups) do
137+
if item.winid == winid then
138+
index_popup(item, root_winid)
139+
return popup_by_winid[winid]
140+
end
141+
end
142+
end
143+
144+
for _, item in pairs(ephemerals) do
145+
if item.winid == winid then
146+
index_popup(item, nil)
147+
return popup_by_winid[winid]
148+
end
149+
end
150+
151+
return nil
152+
end
31153

32154
---@param winid integer
33155
function M._register_stack_view_win(winid)
@@ -37,10 +159,15 @@ end
37159
---@param model PeekstackPopupModel
38160
local function register_ephemeral(model)
39161
ephemerals[model.id] = model
162+
index_popup(model, nil)
40163
end
41164

42165
---@param id integer
43166
local function unregister_ephemeral(id)
167+
local model = ephemerals[id]
168+
if model then
169+
unindex_popup(model)
170+
end
44171
ephemerals[id] = nil
45172
end
46173

@@ -50,10 +177,9 @@ local function find_ephemeral(id)
50177
if ephemerals[id] then
51178
return id, ephemerals[id]
52179
end
53-
for eid, model in pairs(ephemerals) do
54-
if model.winid == id then
55-
return eid, model
56-
end
180+
local entry = lookup_by_winid(id)
181+
if entry and entry.root_winid == nil then
182+
return entry.popup.id, entry.popup
57183
end
58184
return nil
59185
end
@@ -72,14 +198,10 @@ local function get_root_winid(winid)
72198
if ok_root and type(root_winid) == "number" and vim.api.nvim_win_is_valid(root_winid) then
73199
return root_winid
74200
end
75-
-- Current window is floating – look for the origin window stored in the
76-
-- popup model that owns this float.
77-
for _, stack in pairs(stacks) do
78-
for _, item in ipairs(stack.popups) do
79-
if item.winid == wid then
80-
return stack.root_winid
81-
end
82-
end
201+
-- Current window is floating – resolve the owner stack from the popup index.
202+
local owner = lookup_by_winid(wid)
203+
if owner and owner.root_winid and vim.api.nvim_win_is_valid(owner.root_winid) then
204+
return owner.root_winid
83205
end
84206
-- Fallback: pick the first non-floating window in the current tabpage.
85207
for _, w in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
@@ -158,6 +280,7 @@ function M.push(location, opts)
158280
return nil
159281
end
160282
table.insert(stack.popups, model)
283+
index_popup(model, stack.root_winid)
161284
stack.focused_id = model.id
162285
layout.reflow(stack)
163286

@@ -189,6 +312,7 @@ local function close_stack_item(stack, idx, item)
189312
-- Remove from popups BEFORE closing the window to prevent
190313
-- WinClosed autocmd from re-entering and processing the same popup.
191314
table.remove(stack.popups, idx)
315+
unindex_popup(item)
192316

193317
feedback.highlight_origin(item.origin)
194318
popup.close(item)
@@ -240,6 +364,19 @@ function M.close_by_id(id, winid)
240364
return true
241365
end
242366

367+
local indexed = lookup_by_id(id)
368+
if indexed and indexed.root_winid then
369+
local owner_stack = stacks[indexed.root_winid]
370+
if owner_stack then
371+
for idx, item in ipairs(owner_stack.popups) do
372+
if item.id == id then
373+
close_stack_item(owner_stack, idx, item)
374+
return true
375+
end
376+
end
377+
end
378+
end
379+
243380
local stack = ensure_stack(winid)
244381
for idx, item in ipairs(stack.popups) do
245382
if item.id == id then
@@ -259,6 +396,19 @@ function M.close(id, winid)
259396
return true
260397
end
261398

399+
local indexed = lookup_by_winid(id)
400+
if indexed and indexed.root_winid then
401+
local owner_stack = stacks[indexed.root_winid]
402+
if owner_stack then
403+
for idx, item in ipairs(owner_stack.popups) do
404+
if item.winid == id then
405+
close_stack_item(owner_stack, idx, item)
406+
return true
407+
end
408+
end
409+
end
410+
end
411+
262412
local stack = ensure_stack(winid)
263413
for idx, item in ipairs(stack.popups) do
264414
if item.winid == id then
@@ -320,33 +470,25 @@ end
320470
---@param winid integer
321471
---@return PeekstackStackModel?, PeekstackPopupModel?
322472
function M.find_by_winid(winid)
323-
for _, stack in pairs(stacks) do
324-
for _, item in ipairs(stack.popups) do
325-
if item.winid == winid then
326-
return stack, item
327-
end
328-
end
473+
local entry = lookup_by_winid(winid)
474+
if not entry then
475+
return nil
329476
end
330-
for _, item in pairs(ephemerals) do
331-
if item.winid == winid then
332-
return nil, item
477+
if entry.root_winid then
478+
local stack = stacks[entry.root_winid]
479+
if stack then
480+
return stack, entry.popup
333481
end
334482
end
335-
return nil
483+
return nil, entry.popup
336484
end
337485

338486
---@param id integer
339487
---@return PeekstackPopupModel?
340488
function M.find_by_id(id)
341-
for _, stack in pairs(stacks) do
342-
for _, item in ipairs(stack.popups) do
343-
if item.id == id then
344-
return item
345-
end
346-
end
347-
end
348-
if ephemerals[id] then
349-
return ephemerals[id]
489+
local entry = lookup_by_id(id)
490+
if entry then
491+
return entry.popup
350492
end
351493
return nil
352494
end
@@ -404,7 +546,9 @@ function M.reopen_by_id(id, winid)
404546
model.pinned = item.pinned or false
405547
vim.b[model.bufnr].peekstack_popup_id = model.id
406548
vim.w[model.winid].peekstack_popup_id = model.id
549+
unindex_popup(item)
407550
stack.popups[idx] = model
551+
index_popup(model, stack.root_winid)
408552
layout.reflow(stack)
409553
return model
410554
end
@@ -464,6 +608,7 @@ function M.handle_win_closed(winid)
464608
emit_popup_event("PeekstackClose", item, root_winid)
465609
history.push_entry(stack, history.build_entry(item, idx))
466610
table.remove(stack.popups, idx)
611+
unindex_popup(item)
467612
popup.close(item)
468613
end
469614
stacks[root_winid] = nil
@@ -479,6 +624,7 @@ function M.handle_win_closed(winid)
479624
emit_popup_event("PeekstackClose", item, root_winid)
480625
feedback.highlight_origin(item.origin)
481626
table.remove(stack.popups, idx)
627+
unindex_popup(item)
482628
popup.close(item)
483629
removed = true
484630
end
@@ -541,6 +687,7 @@ function M.handle_buf_wipeout(bufnr)
541687
for idx = #stack.popups, 1, -1 do
542688
local item = stack.popups[idx]
543689
if item.bufnr == bufnr then
690+
unindex_popup(item)
544691
table.remove(stack.popups, idx)
545692
end
546693
end
@@ -551,13 +698,9 @@ end
551698
---Update last_active_at for a popup (when user interacts with it)
552699
---@param winid integer
553700
function M.touch(winid)
554-
for _, stack in pairs(stacks) do
555-
for _, item in ipairs(stack.popups) do
556-
if item.winid == winid then
557-
item.last_active_at = vim.uv.now()
558-
return
559-
end
560-
end
701+
local owner_stack, popup_model = M.find_by_winid(winid)
702+
if owner_stack and popup_model then
703+
popup_model.last_active_at = vim.uv.now()
561704
end
562705
end
563706

@@ -621,6 +764,7 @@ function M.handle_origin_wipeout(bufnr)
621764
local item = stack.popups[idx]
622765
if should_close_for_origin(item) then
623766
popup.close(item)
767+
unindex_popup(item)
624768
table.remove(stack.popups, idx)
625769
end
626770
end
@@ -643,6 +787,7 @@ function M.close_ephemerals()
643787
local item = stack.popups[idx]
644788
if item.ephemeral then
645789
popup.close(item)
790+
unindex_popup(item)
646791
table.remove(stack.popups, idx)
647792
removed = true
648793
end
@@ -679,6 +824,7 @@ function M.close_all(winid)
679824

680825
history.push_entry(stack, history.build_entry(item, idx))
681826

827+
unindex_popup(item)
682828
table.remove(stack.popups, idx)
683829
end
684830
stack.focused_id = nil
@@ -696,6 +842,9 @@ end
696842
function M._reset()
697843
stacks = {}
698844
ephemerals = {}
845+
stack_view_wins = {}
846+
popup_by_id = {}
847+
popup_by_winid = {}
699848
end
700849

701850
---Get ephemeral popups (for testing).

0 commit comments

Comments
 (0)