detached 266e0b8

refactor(cef): tighten popup registration follow-ups

commit
266e0b8717931267f79ec883ffdbe3dea4080b44
author
brice · · 2026-04-21 16:57 UTC
parent
24133f3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +44 -53
M internal/infrastructure/cef/webview_popup_proxy.go +10 -22
@@ -21,7 +21,13 @@ func (wv *WebView) syntheticPopupState(proxyID string) *syntheticPopupState {
2121
2222 wv.syntheticPopupMu.Lock()
2323 defer wv.syntheticPopupMu.Unlock()
24+ return wv.syntheticPopupStateLocked(proxyID)
25+}
2426
27+func (wv *WebView) syntheticPopupStateLocked(proxyID string) *syntheticPopupState {
28+ if wv == nil || proxyID == "" {
29+ return nil
30+ }
2531 if wv.syntheticPopups == nil {
2632 wv.syntheticPopups = make(map[string]*syntheticPopupState)
2733 }
@@ -33,16 +39,6 @@ func (wv *WebView) syntheticPopupState(proxyID string) *syntheticPopupState {
3339 return state
3440 }
3541
36-func (wv *WebView) lookupSyntheticPopupState(proxyID string) *syntheticPopupState {
37- if wv == nil || proxyID == "" {
38- return nil
39- }
40-
41- wv.syntheticPopupMu.Lock()
42- defer wv.syntheticPopupMu.Unlock()
43- return wv.syntheticPopups[proxyID]
44-}
45-
4642 func (wv *WebView) deleteSyntheticPopupState(proxyID string) {
4743 if wv == nil || proxyID == "" {
4844 return
@@ -58,12 +54,11 @@ func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string
5854 return
5955 }
6056
61- state := wv.lookupSyntheticPopupState(proxyID)
62- if state != nil {
63- wv.syntheticPopupMu.Lock()
57+ wv.syntheticPopupMu.Lock()
58+ if state := wv.syntheticPopups[proxyID]; state != nil {
6459 state.NoJavaScriptAccess = noJavaScriptAccess
65- wv.syntheticPopupMu.Unlock()
6660 }
61+ wv.syntheticPopupMu.Unlock()
6762
6863 wv.runOnGTK(func() {
6964 if wv.destroyed.Load() {
@@ -92,14 +87,7 @@ func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string
9287
9388 pendingURI := ""
9489 wv.syntheticPopupMu.Lock()
95- if wv.syntheticPopups == nil {
96- wv.syntheticPopups = make(map[string]*syntheticPopupState)
97- }
98- state := wv.syntheticPopups[proxyID]
99- if state == nil {
100- state = &syntheticPopupState{}
101- wv.syntheticPopups[proxyID] = state
102- }
90+ state := wv.syntheticPopupStateLocked(proxyID)
10391 state.WebView = popupWV
10492 state.NoJavaScriptAccess = noJavaScriptAccess
10593 pendingURI = strings.TrimSpace(state.PendingURI)
@@ -21,7 +21,13 @@ func (wv *WebView) syntheticPopupState(proxyID string) *syntheticPopupState {
21 21
22 wv.syntheticPopupMu.Lock()22 wv.syntheticPopupMu.Lock()
23 defer wv.syntheticPopupMu.Unlock()23 defer wv.syntheticPopupMu.Unlock()
24+ return wv.syntheticPopupStateLocked(proxyID)
25+}
24 26
27+func (wv *WebView) syntheticPopupStateLocked(proxyID string) *syntheticPopupState {
28+ if wv == nil || proxyID == "" {
29+ return nil
30+ }
25 if wv.syntheticPopups == nil {31 if wv.syntheticPopups == nil {
26 wv.syntheticPopups = make(map[string]*syntheticPopupState)32 wv.syntheticPopups = make(map[string]*syntheticPopupState)
27 }33 }
@@ -33,16 +39,6 @@ func (wv *WebView) syntheticPopupState(proxyID string) *syntheticPopupState {
33 return state39 return state
34 }40 }
35 41
36-func (wv *WebView) lookupSyntheticPopupState(proxyID string) *syntheticPopupState {
37- if wv == nil || proxyID == "" {
38- return nil
39- }
40-
41- wv.syntheticPopupMu.Lock()
42- defer wv.syntheticPopupMu.Unlock()
43- return wv.syntheticPopups[proxyID]
44-}
45-
46 func (wv *WebView) deleteSyntheticPopupState(proxyID string) {42 func (wv *WebView) deleteSyntheticPopupState(proxyID string) {
47 if wv == nil || proxyID == "" {43 if wv == nil || proxyID == "" {
48 return44 return
@@ -58,12 +54,11 @@ func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string
58 return54 return
59 }55 }
60 56
61- state := wv.lookupSyntheticPopupState(proxyID)57+ wv.syntheticPopupMu.Lock()
62- if state != nil {58+ if state := wv.syntheticPopups[proxyID]; state != nil {
63- wv.syntheticPopupMu.Lock()
64 state.NoJavaScriptAccess = noJavaScriptAccess59 state.NoJavaScriptAccess = noJavaScriptAccess
65- wv.syntheticPopupMu.Unlock()
66 }60 }
61+ wv.syntheticPopupMu.Unlock()
67 62
68 wv.runOnGTK(func() {63 wv.runOnGTK(func() {
69 if wv.destroyed.Load() {64 if wv.destroyed.Load() {
@@ -92,14 +87,7 @@ func (wv *WebView) handleSyntheticPopupOpen(targetURL, frameName, proxyID string
92 87
93 pendingURI := ""88 pendingURI := ""
94 wv.syntheticPopupMu.Lock()89 wv.syntheticPopupMu.Lock()
95- if wv.syntheticPopups == nil {90+ state := wv.syntheticPopupStateLocked(proxyID)
96- wv.syntheticPopups = make(map[string]*syntheticPopupState)
97- }
98- state := wv.syntheticPopups[proxyID]
99- if state == nil {
100- state = &syntheticPopupState{}
101- wv.syntheticPopups[proxyID] = state
102- }
103 state.WebView = popupWV91 state.WebView = popupWV
104 state.NoJavaScriptAccess = noJavaScriptAccess92 state.NoJavaScriptAccess = noJavaScriptAccess
105 pendingURI = strings.TrimSpace(state.PendingURI)93 pendingURI = strings.TrimSpace(state.PendingURI)
M internal/ui/coordinator/content/popup.go +15 -8
@@ -526,10 +526,23 @@ func (c *Coordinator) finishPopupCreate(
526526 Str("uri", logging.TruncateURL(req.TargetURI, logURLMaxLen)).
527527 Msg("popup OAuth check")
528528
529+ // Initialize callbacks before publishing the WebView so any reader that sees
530+ // the popup during workspace insertion gets a fully configured instance.
531+ c.setupWebViewCallbacks(ctx, paneID, popupWV)
532+
529533 // Register the popup WebView before workspace insertion so split/stack UI
530534 // updates can reuse the real popup instead of acquiring a placeholder pane
531535 // WebView that races the actual popup flow.
532- c.setWebViewLocked(paneID, popupWV)
536+ c.RegisterPopupWebView(paneID, popupWV)
537+ inserted := false
538+ defer func() {
539+ if inserted {
540+ return
541+ }
542+ if current := c.getWebViewLocked(paneID); current == popupWV {
543+ c.deleteWebViewLocked(paneID)
544+ }
545+ }()
533546
534547 // Insert into workspace IMMEDIATELY (WebView stays hidden)
535548 // This is required for WebKit to establish window.opener relationship
@@ -546,22 +559,16 @@ func (c *Coordinator) finishPopupCreate(
546559
547560 if err := c.onInsertPopup(ctx, popupInput); err != nil {
548561 log.Error().Err(err).Msg("failed to insert popup into workspace")
549- if current := c.getWebViewLocked(paneID); current == popupWV {
550- c.deleteWebViewLocked(paneID)
551- }
552562 popupWV.Destroy()
553563 return nil
554564 }
555565 }
566+ inserted = true
556567
557- // WebView already registered before insertion.
558568 if !req.NoJavaScriptAccess {
559569 c.storeReusableNamedPopup(parentPaneID, req.FrameName, popupWV)
560570 }
561571
562- // Setup standard callbacks (after successful insertion to avoid leak)
563- c.setupWebViewCallbacks(ctx, paneID, popupWV)
564-
565572 // Engines without native popup lifecycle hooks can still support
566573 // programmatic popup closure (proxy.close(), OAuth auto-close) by exposing
567574 // the optional OAuthCallbackCapable interface.
@@ -526,10 +526,23 @@ func (c *Coordinator) finishPopupCreate(
526 Str("uri", logging.TruncateURL(req.TargetURI, logURLMaxLen)).526 Str("uri", logging.TruncateURL(req.TargetURI, logURLMaxLen)).
527 Msg("popup OAuth check")527 Msg("popup OAuth check")
528 528
529+ // Initialize callbacks before publishing the WebView so any reader that sees
530+ // the popup during workspace insertion gets a fully configured instance.
531+ c.setupWebViewCallbacks(ctx, paneID, popupWV)
532+
529 // Register the popup WebView before workspace insertion so split/stack UI533 // Register the popup WebView before workspace insertion so split/stack UI
530 // updates can reuse the real popup instead of acquiring a placeholder pane534 // updates can reuse the real popup instead of acquiring a placeholder pane
531 // WebView that races the actual popup flow.535 // WebView that races the actual popup flow.
532- c.setWebViewLocked(paneID, popupWV)536+ c.RegisterPopupWebView(paneID, popupWV)
537+ inserted := false
538+ defer func() {
539+ if inserted {
540+ return
541+ }
542+ if current := c.getWebViewLocked(paneID); current == popupWV {
543+ c.deleteWebViewLocked(paneID)
544+ }
545+ }()
533 546
534 // Insert into workspace IMMEDIATELY (WebView stays hidden)547 // Insert into workspace IMMEDIATELY (WebView stays hidden)
535 // This is required for WebKit to establish window.opener relationship548 // This is required for WebKit to establish window.opener relationship
@@ -546,22 +559,16 @@ func (c *Coordinator) finishPopupCreate(
546 559
547 if err := c.onInsertPopup(ctx, popupInput); err != nil {560 if err := c.onInsertPopup(ctx, popupInput); err != nil {
548 log.Error().Err(err).Msg("failed to insert popup into workspace")561 log.Error().Err(err).Msg("failed to insert popup into workspace")
549- if current := c.getWebViewLocked(paneID); current == popupWV {
550- c.deleteWebViewLocked(paneID)
551- }
552 popupWV.Destroy()562 popupWV.Destroy()
553 return nil563 return nil
554 }564 }
555 }565 }
566+ inserted = true
556 567
557- // WebView already registered before insertion.
558 if !req.NoJavaScriptAccess {568 if !req.NoJavaScriptAccess {
559 c.storeReusableNamedPopup(parentPaneID, req.FrameName, popupWV)569 c.storeReusableNamedPopup(parentPaneID, req.FrameName, popupWV)
560 }570 }
561 571
562- // Setup standard callbacks (after successful insertion to avoid leak)
563- c.setupWebViewCallbacks(ctx, paneID, popupWV)
564-
565 // Engines without native popup lifecycle hooks can still support572 // Engines without native popup lifecycle hooks can still support
566 // programmatic popup closure (proxy.close(), OAuth auto-close) by exposing573 // programmatic popup closure (proxy.close(), OAuth auto-close) by exposing
567 // the optional OAuthCallbackCapable interface.574 // the optional OAuthCallbackCapable interface.
M internal/ui/coordinator/content/popup_test.go +19 -23
@@ -143,19 +143,16 @@ func TestNormalizePopupTargetURIForFallback_PreservesUnknownURLs(t *testing.T) {
143143 assert.Equal(t, raw, normalizePopupTargetURIForFallback(raw))
144144 }
145145
146-func TestHandlePopupCreate_RegistersPopupWebViewBeforeWorkspaceInsertion(t *testing.T) {
146+func newPopupCreateCoordinatorForTest(t *testing.T, popupID port.WebViewID) (context.Context, entity.PaneID, *mocks.MockWebView, *mocks.MockWebView, *Coordinator) {
147+ t.Helper()
148+
147149 ctx := context.Background()
148150 parentPaneID := entity.PaneID("parent-pane")
149151 parentWV := mocks.NewMockWebView(t)
150152 parentWV.EXPECT().ID().Return(port.WebViewID(101)).Once()
151153
152154 popupWV := mocks.NewMockWebView(t)
153- popupWV.EXPECT().ID().Return(port.WebViewID(150)).Once()
154- popupWV.EXPECT().Generation().Return(uint64(1)).Once()
155- popupWV.EXPECT().SetCallbacks(mock.Anything).Once()
156- popupWV.EXPECT().IsLoading().Return(false).Once()
157- popupWV.EXPECT().URI().Return("").Once()
158- popupWV.EXPECT().LoadURI(mock.Anything, "https://example.com/popup").Return(nil).Once()
155+ popupWV.EXPECT().ID().Return(popupID).Once()
159156
160157 factory := mocks.NewMockWebViewFactory(t)
161158 factory.EXPECT().Create(mock.Anything).Return(popupWV, nil).Once()
@@ -166,6 +163,18 @@ func TestHandlePopupCreate_RegistersPopupWebViewBeforeWorkspaceInsertion(t *test
166163 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
167164 }
168165 c.SetPopupConfig(factory, nil, func() string { return "popup-pane" })
166+
167+ return ctx, parentPaneID, parentWV, popupWV, c
168+}
169+
170+func TestHandlePopupCreate_RegistersPopupWebViewBeforeWorkspaceInsertion(t *testing.T) {
171+ ctx, parentPaneID, parentWV, popupWV, c := newPopupCreateCoordinatorForTest(t, port.WebViewID(150))
172+ popupWV.EXPECT().Generation().Return(uint64(1)).Once()
173+ popupWV.EXPECT().SetCallbacks(mock.Anything).Once()
174+ popupWV.EXPECT().IsLoading().Return(false).Once()
175+ popupWV.EXPECT().URI().Return("").Once()
176+ popupWV.EXPECT().LoadURI(mock.Anything, "https://example.com/popup").Return(nil).Once()
177+
169178 c.SetOnInsertPopup(func(_ context.Context, input InsertPopupInput) error {
170179 require.Equal(t, entity.PaneID("popup-pane"), input.PopupPane.ID)
171180 require.Same(t, popupWV, c.getWebViewLocked(input.PopupPane.ID))
@@ -183,24 +192,11 @@ func TestHandlePopupCreate_RegistersPopupWebViewBeforeWorkspaceInsertion(t *test
183192 }
184193
185194 func TestHandlePopupCreate_CleansUpPreRegisteredPopupWebViewWhenInsertionFails(t *testing.T) {
186- ctx := context.Background()
187- parentPaneID := entity.PaneID("parent-pane")
188- parentWV := mocks.NewMockWebView(t)
189- parentWV.EXPECT().ID().Return(port.WebViewID(101)).Once()
190-
191- popupWV := mocks.NewMockWebView(t)
192- popupWV.EXPECT().ID().Return(port.WebViewID(151)).Once()
195+ ctx, parentPaneID, parentWV, popupWV, c := newPopupCreateCoordinatorForTest(t, port.WebViewID(151))
196+ popupWV.EXPECT().Generation().Return(uint64(1)).Once()
197+ popupWV.EXPECT().SetCallbacks(mock.Anything).Once()
193198 popupWV.EXPECT().Destroy().Once()
194199
195- factory := mocks.NewMockWebViewFactory(t)
196- factory.EXPECT().Create(mock.Anything).Return(popupWV, nil).Once()
197-
198- c := &Coordinator{
199- webViews: make(map[entity.PaneID]port.WebView),
200- pendingPopups: make(map[port.WebViewID]*PendingPopup),
201- popupOAuth: make(map[port.WebViewID]*popupOAuthState),
202- }
203- c.SetPopupConfig(factory, nil, func() string { return "popup-pane" })
204200 c.SetOnInsertPopup(func(_ context.Context, input InsertPopupInput) error {
205201 require.Same(t, popupWV, c.getWebViewLocked(input.PopupPane.ID))
206202 return assert.AnError
@@ -143,19 +143,16 @@ func TestNormalizePopupTargetURIForFallback_PreservesUnknownURLs(t *testing.T) {
143 assert.Equal(t, raw, normalizePopupTargetURIForFallback(raw))143 assert.Equal(t, raw, normalizePopupTargetURIForFallback(raw))
144 }144 }
145 145
146-func TestHandlePopupCreate_RegistersPopupWebViewBeforeWorkspaceInsertion(t *testing.T) {146+func newPopupCreateCoordinatorForTest(t *testing.T, popupID port.WebViewID) (context.Context, entity.PaneID, *mocks.MockWebView, *mocks.MockWebView, *Coordinator) {
147+ t.Helper()
148+
147 ctx := context.Background()149 ctx := context.Background()
148 parentPaneID := entity.PaneID("parent-pane")150 parentPaneID := entity.PaneID("parent-pane")
149 parentWV := mocks.NewMockWebView(t)151 parentWV := mocks.NewMockWebView(t)
150 parentWV.EXPECT().ID().Return(port.WebViewID(101)).Once()152 parentWV.EXPECT().ID().Return(port.WebViewID(101)).Once()
151 153
152 popupWV := mocks.NewMockWebView(t)154 popupWV := mocks.NewMockWebView(t)
153- popupWV.EXPECT().ID().Return(port.WebViewID(150)).Once()155+ popupWV.EXPECT().ID().Return(popupID).Once()
154- popupWV.EXPECT().Generation().Return(uint64(1)).Once()
155- popupWV.EXPECT().SetCallbacks(mock.Anything).Once()
156- popupWV.EXPECT().IsLoading().Return(false).Once()
157- popupWV.EXPECT().URI().Return("").Once()
158- popupWV.EXPECT().LoadURI(mock.Anything, "https://example.com/popup").Return(nil).Once()
159 156
160 factory := mocks.NewMockWebViewFactory(t)157 factory := mocks.NewMockWebViewFactory(t)
161 factory.EXPECT().Create(mock.Anything).Return(popupWV, nil).Once()158 factory.EXPECT().Create(mock.Anything).Return(popupWV, nil).Once()
@@ -166,6 +163,18 @@ func TestHandlePopupCreate_RegistersPopupWebViewBeforeWorkspaceInsertion(t *test
166 popupOAuth: make(map[port.WebViewID]*popupOAuthState),163 popupOAuth: make(map[port.WebViewID]*popupOAuthState),
167 }164 }
168 c.SetPopupConfig(factory, nil, func() string { return "popup-pane" })165 c.SetPopupConfig(factory, nil, func() string { return "popup-pane" })
166+
167+ return ctx, parentPaneID, parentWV, popupWV, c
168+}
169+
170+func TestHandlePopupCreate_RegistersPopupWebViewBeforeWorkspaceInsertion(t *testing.T) {
171+ ctx, parentPaneID, parentWV, popupWV, c := newPopupCreateCoordinatorForTest(t, port.WebViewID(150))
172+ popupWV.EXPECT().Generation().Return(uint64(1)).Once()
173+ popupWV.EXPECT().SetCallbacks(mock.Anything).Once()
174+ popupWV.EXPECT().IsLoading().Return(false).Once()
175+ popupWV.EXPECT().URI().Return("").Once()
176+ popupWV.EXPECT().LoadURI(mock.Anything, "https://example.com/popup").Return(nil).Once()
177+
169 c.SetOnInsertPopup(func(_ context.Context, input InsertPopupInput) error {178 c.SetOnInsertPopup(func(_ context.Context, input InsertPopupInput) error {
170 require.Equal(t, entity.PaneID("popup-pane"), input.PopupPane.ID)179 require.Equal(t, entity.PaneID("popup-pane"), input.PopupPane.ID)
171 require.Same(t, popupWV, c.getWebViewLocked(input.PopupPane.ID))180 require.Same(t, popupWV, c.getWebViewLocked(input.PopupPane.ID))
@@ -183,24 +192,11 @@ func TestHandlePopupCreate_RegistersPopupWebViewBeforeWorkspaceInsertion(t *test
183 }192 }
184 193
185 func TestHandlePopupCreate_CleansUpPreRegisteredPopupWebViewWhenInsertionFails(t *testing.T) {194 func TestHandlePopupCreate_CleansUpPreRegisteredPopupWebViewWhenInsertionFails(t *testing.T) {
186- ctx := context.Background()195+ ctx, parentPaneID, parentWV, popupWV, c := newPopupCreateCoordinatorForTest(t, port.WebViewID(151))
187- parentPaneID := entity.PaneID("parent-pane")196+ popupWV.EXPECT().Generation().Return(uint64(1)).Once()
188- parentWV := mocks.NewMockWebView(t)197+ popupWV.EXPECT().SetCallbacks(mock.Anything).Once()
189- parentWV.EXPECT().ID().Return(port.WebViewID(101)).Once()
190-
191- popupWV := mocks.NewMockWebView(t)
192- popupWV.EXPECT().ID().Return(port.WebViewID(151)).Once()
193 popupWV.EXPECT().Destroy().Once()198 popupWV.EXPECT().Destroy().Once()
194 199
195- factory := mocks.NewMockWebViewFactory(t)
196- factory.EXPECT().Create(mock.Anything).Return(popupWV, nil).Once()
197-
198- c := &Coordinator{
199- webViews: make(map[entity.PaneID]port.WebView),
200- pendingPopups: make(map[port.WebViewID]*PendingPopup),
201- popupOAuth: make(map[port.WebViewID]*popupOAuthState),
202- }
203- c.SetPopupConfig(factory, nil, func() string { return "popup-pane" })
204 c.SetOnInsertPopup(func(_ context.Context, input InsertPopupInput) error {200 c.SetOnInsertPopup(func(_ context.Context, input InsertPopupInput) error {
205 require.Same(t, popupWV, c.getWebViewLocked(input.PopupPane.ID))201 require.Same(t, popupWV, c.getWebViewLocked(input.PopupPane.ID))
206 return assert.AnError202 return assert.AnError

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close