@@ -28,6 +28,128 @@ local stacks = {}
2828local ephemerals = {}
2929--- @type table<integer , boolean>
3030local 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
33155function M ._register_stack_view_win (winid )
37159--- @param model PeekstackPopupModel
38160local function register_ephemeral (model )
39161 ephemerals [model .id ] = model
162+ index_popup (model , nil )
40163end
41164
42165--- @param id integer
43166local function unregister_ephemeral (id )
167+ local model = ephemerals [id ]
168+ if model then
169+ unindex_popup (model )
170+ end
44171 ephemerals [id ] = nil
45172end
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
59185end
@@ -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 ?
322472function 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
336484end
337485
338486--- @param id integer
339487--- @return PeekstackPopupModel ?
340488function 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
352494end
@@ -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
551698--- Update last_active_at for a popup (when user interacts with it)
552699--- @param winid integer
553700function 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
562705end
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
696842function M ._reset ()
697843 stacks = {}
698844 ephemerals = {}
845+ stack_view_wins = {}
846+ popup_by_id = {}
847+ popup_by_winid = {}
699848end
700849
701850--- Get ephemeral popups (for testing).
0 commit comments