detached 14d539b

fix(vim): deduplicate highlighted web views

commit
14d539b05ef54924cbb40277326b7475a2e973ec
author
brice · · 2026-08-18 05:13 UTC
parent
3a00c38
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +15 -3
M internal/ui/app.go +9 -1
@@ -2205,7 +2205,14 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow,
22052205 }
22062206 if bw != nil && (action == "heading-next" || action == "heading-prev") {
22072207 if _, ok := wv.(port.SemanticNavigable); ok {
2208- bw.vimNavigationHighlightedWebViews = append(bw.vimNavigationHighlightedWebViews, wv)
2208+ if bw.vimNavigationHighlightedWebViewIDs == nil {
2209+ bw.vimNavigationHighlightedWebViewIDs = make(map[port.WebViewID]struct{})
2210+ }
2211+ id := wv.ID()
2212+ if _, exists := bw.vimNavigationHighlightedWebViewIDs[id]; !exists {
2213+ bw.vimNavigationHighlightedWebViewIDs[id] = struct{}{}
2214+ bw.vimNavigationHighlightedWebViews = append(bw.vimNavigationHighlightedWebViews, wv)
2215+ }
22092216 }
22102217 }
22112218 }
@@ -2224,6 +2231,7 @@ func (a *App) clearVimNavigationHighlight(ctx context.Context, bw *browserWindow
22242231 }
22252232 }
22262233 bw.vimNavigationHighlightedWebViews = nil
2234+ bw.vimNavigationHighlightedWebViewIDs = nil
22272235 }
22282236
22292237 func (a *App) vimNavigationHighlightColor() string {
@@ -2205,7 +2205,14 @@ func (a *App) navigateVimSequenceAction(ctx context.Context, bw *browserWindow,
2205 }2205 }
2206 if bw != nil && (action == "heading-next" || action == "heading-prev") {2206 if bw != nil && (action == "heading-next" || action == "heading-prev") {
2207 if _, ok := wv.(port.SemanticNavigable); ok {2207 if _, ok := wv.(port.SemanticNavigable); ok {
2208- bw.vimNavigationHighlightedWebViews = append(bw.vimNavigationHighlightedWebViews, wv)2208+ if bw.vimNavigationHighlightedWebViewIDs == nil {
2209+ bw.vimNavigationHighlightedWebViewIDs = make(map[port.WebViewID]struct{})
2210+ }
2211+ id := wv.ID()
2212+ if _, exists := bw.vimNavigationHighlightedWebViewIDs[id]; !exists {
2213+ bw.vimNavigationHighlightedWebViewIDs[id] = struct{}{}
2214+ bw.vimNavigationHighlightedWebViews = append(bw.vimNavigationHighlightedWebViews, wv)
2215+ }
2209 }2216 }
2210 }2217 }
2211 }2218 }
@@ -2224,6 +2231,7 @@ func (a *App) clearVimNavigationHighlight(ctx context.Context, bw *browserWindow
2224 }2231 }
2225 }2232 }
2226 bw.vimNavigationHighlightedWebViews = nil2233 bw.vimNavigationHighlightedWebViews = nil
2234+ bw.vimNavigationHighlightedWebViewIDs = nil
2227 }2235 }
2228 2236
2229 func (a *App) vimNavigationHighlightColor() string {2237 func (a *App) vimNavigationHighlightColor() string {
M internal/ui/app_vim_mode_test.go +3 -1
@@ -183,7 +183,7 @@ type semanticNavigatingWebView struct {
183183 func newSemanticNavigatingWebView(t *testing.T, id port.WebViewID) *semanticNavigatingWebView {
184184 t.Helper()
185185 wv := portmocks.NewMockWebView(t)
186- wv.EXPECT().ID().Return(id).Once()
186+ wv.EXPECT().ID().Return(id)
187187 return &semanticNavigatingWebView{MockWebView: wv}
188188 }
189189
@@ -528,11 +528,13 @@ func TestVimMode_Leave_ClearsNavigationHighlight(t *testing.T) {
528528 wv := newSemanticNavigatingWebView(t, 8)
529529 app.contentCoord.RegisterPopupWebView(paneID, wv)
530530 app.navigateVimSequenceAction(context.Background(), bw, "heading-next", 1)
531+ app.navigateVimSequenceAction(context.Background(), bw, "heading-next", 1)
531532
532533 app.handleModeChange(context.Background(), bw, input.ModeVim, input.ModeNormal)
533534
534535 assert.Equal(t, 1, wv.clearNavigationHighlightCalls)
535536 assert.Empty(t, bw.vimNavigationHighlightedWebViews)
537+ assert.Empty(t, bw.vimNavigationHighlightedWebViewIDs)
536538 }
537539
538540 func TestVimMode_Leave_ClearsNavigationHighlightAfterPaneSwitch(t *testing.T) {
@@ -183,7 +183,7 @@ type semanticNavigatingWebView struct {
183 func newSemanticNavigatingWebView(t *testing.T, id port.WebViewID) *semanticNavigatingWebView {183 func newSemanticNavigatingWebView(t *testing.T, id port.WebViewID) *semanticNavigatingWebView {
184 t.Helper()184 t.Helper()
185 wv := portmocks.NewMockWebView(t)185 wv := portmocks.NewMockWebView(t)
186- wv.EXPECT().ID().Return(id).Once()186+ wv.EXPECT().ID().Return(id)
187 return &semanticNavigatingWebView{MockWebView: wv}187 return &semanticNavigatingWebView{MockWebView: wv}
188 }188 }
189 189
@@ -528,11 +528,13 @@ func TestVimMode_Leave_ClearsNavigationHighlight(t *testing.T) {
528 wv := newSemanticNavigatingWebView(t, 8)528 wv := newSemanticNavigatingWebView(t, 8)
529 app.contentCoord.RegisterPopupWebView(paneID, wv)529 app.contentCoord.RegisterPopupWebView(paneID, wv)
530 app.navigateVimSequenceAction(context.Background(), bw, "heading-next", 1)530 app.navigateVimSequenceAction(context.Background(), bw, "heading-next", 1)
531+ app.navigateVimSequenceAction(context.Background(), bw, "heading-next", 1)
531 532
532 app.handleModeChange(context.Background(), bw, input.ModeVim, input.ModeNormal)533 app.handleModeChange(context.Background(), bw, input.ModeVim, input.ModeNormal)
533 534
534 assert.Equal(t, 1, wv.clearNavigationHighlightCalls)535 assert.Equal(t, 1, wv.clearNavigationHighlightCalls)
535 assert.Empty(t, bw.vimNavigationHighlightedWebViews)536 assert.Empty(t, bw.vimNavigationHighlightedWebViews)
537+ assert.Empty(t, bw.vimNavigationHighlightedWebViewIDs)
536 }538 }
537 539
538 func TestVimMode_Leave_ClearsNavigationHighlightAfterPaneSwitch(t *testing.T) {540 func TestVimMode_Leave_ClearsNavigationHighlightAfterPaneSwitch(t *testing.T) {
M internal/ui/browser_window.go +3 -1
@@ -58,7 +58,8 @@ type browserWindow struct {
5858 vimModePaneID entity.PaneID
5959 // vimNavigationHighlightedWebViews holds every WebView with a semantic Vim
6060 // navigation target that must be cleared when this window leaves Vim Mode.
61- vimNavigationHighlightedWebViews []port.WebView
61+ vimNavigationHighlightedWebViews []port.WebView
62+ vimNavigationHighlightedWebViewIDs map[port.WebViewID]struct{}
6263 }
6364
6465 func (bw *browserWindow) detachInputForDestroy() {
@@ -117,6 +118,7 @@ func (bw *browserWindow) clearShellState() {
117118 bw.activeSidebarKind = nativeSidebarNone
118119 bw.vimModePaneID = ""
119120 bw.vimNavigationHighlightedWebViews = nil
121+ bw.vimNavigationHighlightedWebViewIDs = nil
120122 }
121123
122124 func (bw *browserWindow) initChrome(ctx context.Context, a *App) {
@@ -58,7 +58,8 @@ type browserWindow struct {
58 vimModePaneID entity.PaneID58 vimModePaneID entity.PaneID
59 // vimNavigationHighlightedWebViews holds every WebView with a semantic Vim59 // vimNavigationHighlightedWebViews holds every WebView with a semantic Vim
60 // navigation target that must be cleared when this window leaves Vim Mode.60 // navigation target that must be cleared when this window leaves Vim Mode.
61- vimNavigationHighlightedWebViews []port.WebView61+ vimNavigationHighlightedWebViews []port.WebView
62+ vimNavigationHighlightedWebViewIDs map[port.WebViewID]struct{}
62 }63 }
63 64
64 func (bw *browserWindow) detachInputForDestroy() {65 func (bw *browserWindow) detachInputForDestroy() {
@@ -117,6 +118,7 @@ func (bw *browserWindow) clearShellState() {
117 bw.activeSidebarKind = nativeSidebarNone118 bw.activeSidebarKind = nativeSidebarNone
118 bw.vimModePaneID = ""119 bw.vimModePaneID = ""
119 bw.vimNavigationHighlightedWebViews = nil120 bw.vimNavigationHighlightedWebViews = nil
121+ bw.vimNavigationHighlightedWebViewIDs = nil
120 }122 }
121 123
122 func (bw *browserWindow) initChrome(ctx context.Context, a *App) {124 func (bw *browserWindow) initChrome(ctx context.Context, a *App) {

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