detached c031fea

refactor(ui): extract omnibox favorite toggle helpers

commit
c031fea0aad12fcf336aa2594274ee5fa8b0f85a
author
brice · · 2026-06-25 12:49 UTC
parent
0b6a0cf
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +242 -13
M internal/ui/component/omnibox.go +89 -13
@@ -39,6 +39,19 @@ type favoriteRowIndicatorUpdate struct {
3939 ShowStarSlot bool
4040 }
4141
42+type favoriteToggleTarget struct {
43+ url string
44+ title string
45+ favoriteID int64
46+ wasFavorite bool
47+}
48+
49+type favoriteToggleResultUpdate struct {
50+ Apply bool
51+ Index int
52+ IsFavorite bool
53+}
54+
4255 // ViewMode distinguishes history search from favorites display.
4356 type ViewMode string
4457
@@ -2385,6 +2398,63 @@ func shouldPreferTypedURLNavigation(entryText string) bool {
23852398 return url.LooksLikeURL(entryText)
23862399 }
23872400
2401+func resolveFavoriteToggleTarget(
2402+ mode ViewMode,
2403+ idx int,
2404+ suggestions []Suggestion,
2405+ favorites []Favorite,
2406+) (favoriteToggleTarget, bool) {
2407+ if mode == ViewModeHistory {
2408+ if idx < 0 || idx >= len(suggestions) {
2409+ return favoriteToggleTarget{}, false
2410+ }
2411+ s := suggestions[idx]
2412+ if s.URL == "" {
2413+ return favoriteToggleTarget{}, false
2414+ }
2415+ return favoriteToggleTarget{
2416+ url: s.URL,
2417+ title: s.Title,
2418+ wasFavorite: s.IsFavorite,
2419+ }, true
2420+ }
2421+
2422+ if idx < 0 || idx >= len(favorites) {
2423+ return favoriteToggleTarget{}, false
2424+ }
2425+ f := favorites[idx]
2426+ if f.ID == 0 {
2427+ return favoriteToggleTarget{}, false
2428+ }
2429+ return favoriteToggleTarget{
2430+ url: f.URL,
2431+ title: f.Title,
2432+ favoriteID: f.ID,
2433+ }, true
2434+}
2435+
2436+func resolveFavoriteToggleResultUpdate(
2437+ suggestions []Suggestion,
2438+ index int,
2439+ expectedURL string,
2440+ isFavorite bool,
2441+) favoriteToggleResultUpdate {
2442+ if index < 0 || index >= len(suggestions) {
2443+ return favoriteToggleResultUpdate{}
2444+ }
2445+ if suggestions[index].URL != expectedURL {
2446+ return favoriteToggleResultUpdate{}
2447+ }
2448+ return favoriteToggleResultUpdate{Apply: true, Index: index, IsFavorite: isFavorite}
2449+}
2450+
2451+func favoriteToggleErrorMessage(wasFavorite bool) string {
2452+ if wasFavorite {
2453+ return "Failed to remove favorite"
2454+ }
2455+ return "Failed to add favorite"
2456+}
2457+
23882458 // toggleFavorite adds or removes the selected item from favorites.
23892459 // In History mode: adds the selected item to favorites
23902460 // In Favorites mode: removes the selected item from favorites
@@ -2415,19 +2485,20 @@ func (o *Omnibox) toggleFavorite() {
24152485 log.Debug().Msg("toggle favorite: empty URL")
24162486 return
24172487 }
2488+ target, ok := resolveFavoriteToggleTarget(mode, idx, suggestions, favorites)
2489+ if !ok {
2490+ return
2491+ }
24182492
24192493 go func() {
24202494 ctx := o.ctx
24212495 goLog := logging.FromContext(ctx)
24222496
2423- result, err := o.favoritesUC.Toggle(ctx, s.URL, s.Title)
2497+ result, err := o.favoritesUC.Toggle(ctx, target.url, target.title)
24242498 if err != nil {
2425- goLog.Error().Err(err).Str("url", s.URL).Msg("failed to toggle favorite")
2499+ goLog.Error().Err(err).Str("url", target.url).Msg("failed to toggle favorite")
24262500 if o.onToast != nil {
2427- msg := "Failed to add favorite"
2428- if s.IsFavorite {
2429- msg = "Failed to remove favorite"
2430- }
2501+ msg := favoriteToggleErrorMessage(target.wasFavorite)
24312502 cb := glib.SourceFunc(func(_ uintptr) bool {
24322503 o.onToast(ctx, msg, ToastError)
24332504 return false
@@ -2439,14 +2510,15 @@ func (o *Omnibox) toggleFavorite() {
24392510
24402511 // Update suggestion state
24412512 o.mu.Lock()
2442- if idx < len(o.suggestions) && o.suggestions[idx].URL == s.URL {
2443- o.suggestions[idx].IsFavorite = result.Added
2513+ update := resolveFavoriteToggleResultUpdate(o.suggestions, idx, target.url, result.Added)
2514+ if update.Apply {
2515+ o.suggestions[update.Index].IsFavorite = update.IsFavorite
24442516 }
24452517 o.mu.Unlock()
24462518
24472519 // Update row CSS and show toast on GTK main thread
24482520 cb := glib.SourceFunc(func(_ uintptr) bool {
2449- o.updateRowFavoriteIndicator(idx, s.URL, result.Added)
2521+ o.updateRowFavoriteIndicator(idx, target.url, result.Added)
24502522 if o.onToast != nil {
24512523 o.onToast(ctx, result.Message, ToastSuccess)
24522524 }
@@ -2466,17 +2538,21 @@ func (o *Omnibox) toggleFavorite() {
24662538 log.Debug().Msg("remove favorite: invalid ID")
24672539 return
24682540 }
2541+ target, ok := resolveFavoriteToggleTarget(mode, idx, suggestions, favorites)
2542+ if !ok {
2543+ return
2544+ }
24692545 entryText := o.entry.GetText()
24702546
24712547 go func() {
24722548 ctx := o.ctx
24732549 log := logging.FromContext(ctx)
24742550
2475- log.Debug().Int64("id", f.ID).Str("url", f.URL).Msg("removing from favorites")
2551+ log.Debug().Int64("id", target.favoriteID).Str("url", target.url).Msg("removing from favorites")
24762552
2477- err := o.favoritesUC.Remove(ctx, entity.FavoriteID(f.ID))
2553+ err := o.favoritesUC.Remove(ctx, entity.FavoriteID(target.favoriteID))
24782554 if err != nil {
2479- log.Error().Err(err).Int64("id", f.ID).Msg("failed to remove favorite")
2555+ log.Error().Err(err).Int64("id", target.favoriteID).Msg("failed to remove favorite")
24802556 // Show error toast
24812557 if o.onToast != nil {
24822558 cb := glib.SourceFunc(func(_ uintptr) bool {
@@ -2488,7 +2564,7 @@ func (o *Omnibox) toggleFavorite() {
24882564 return
24892565 }
24902566
2491- log.Info().Int64("id", f.ID).Str("url", f.URL).Msg("favorite removed from omnibox")
2567+ log.Info().Int64("id", target.favoriteID).Str("url", target.url).Msg("favorite removed from omnibox")
24922568
24932569 // Refresh favorites list and show toast
24942570 o.mu.RLock()
@@ -39,6 +39,19 @@ type favoriteRowIndicatorUpdate struct {
39 ShowStarSlot bool39 ShowStarSlot bool
40 }40 }
41 41
42+type favoriteToggleTarget struct {
43+ url string
44+ title string
45+ favoriteID int64
46+ wasFavorite bool
47+}
48+
49+type favoriteToggleResultUpdate struct {
50+ Apply bool
51+ Index int
52+ IsFavorite bool
53+}
54+
42 // ViewMode distinguishes history search from favorites display.55 // ViewMode distinguishes history search from favorites display.
43 type ViewMode string56 type ViewMode string
44 57
@@ -2385,6 +2398,63 @@ func shouldPreferTypedURLNavigation(entryText string) bool {
2385 return url.LooksLikeURL(entryText)2398 return url.LooksLikeURL(entryText)
2386 }2399 }
2387 2400
2401+func resolveFavoriteToggleTarget(
2402+ mode ViewMode,
2403+ idx int,
2404+ suggestions []Suggestion,
2405+ favorites []Favorite,
2406+) (favoriteToggleTarget, bool) {
2407+ if mode == ViewModeHistory {
2408+ if idx < 0 || idx >= len(suggestions) {
2409+ return favoriteToggleTarget{}, false
2410+ }
2411+ s := suggestions[idx]
2412+ if s.URL == "" {
2413+ return favoriteToggleTarget{}, false
2414+ }
2415+ return favoriteToggleTarget{
2416+ url: s.URL,
2417+ title: s.Title,
2418+ wasFavorite: s.IsFavorite,
2419+ }, true
2420+ }
2421+
2422+ if idx < 0 || idx >= len(favorites) {
2423+ return favoriteToggleTarget{}, false
2424+ }
2425+ f := favorites[idx]
2426+ if f.ID == 0 {
2427+ return favoriteToggleTarget{}, false
2428+ }
2429+ return favoriteToggleTarget{
2430+ url: f.URL,
2431+ title: f.Title,
2432+ favoriteID: f.ID,
2433+ }, true
2434+}
2435+
2436+func resolveFavoriteToggleResultUpdate(
2437+ suggestions []Suggestion,
2438+ index int,
2439+ expectedURL string,
2440+ isFavorite bool,
2441+) favoriteToggleResultUpdate {
2442+ if index < 0 || index >= len(suggestions) {
2443+ return favoriteToggleResultUpdate{}
2444+ }
2445+ if suggestions[index].URL != expectedURL {
2446+ return favoriteToggleResultUpdate{}
2447+ }
2448+ return favoriteToggleResultUpdate{Apply: true, Index: index, IsFavorite: isFavorite}
2449+}
2450+
2451+func favoriteToggleErrorMessage(wasFavorite bool) string {
2452+ if wasFavorite {
2453+ return "Failed to remove favorite"
2454+ }
2455+ return "Failed to add favorite"
2456+}
2457+
2388 // toggleFavorite adds or removes the selected item from favorites.2458 // toggleFavorite adds or removes the selected item from favorites.
2389 // In History mode: adds the selected item to favorites2459 // In History mode: adds the selected item to favorites
2390 // In Favorites mode: removes the selected item from favorites2460 // In Favorites mode: removes the selected item from favorites
@@ -2415,19 +2485,20 @@ func (o *Omnibox) toggleFavorite() {
2415 log.Debug().Msg("toggle favorite: empty URL")2485 log.Debug().Msg("toggle favorite: empty URL")
2416 return2486 return
2417 }2487 }
2488+ target, ok := resolveFavoriteToggleTarget(mode, idx, suggestions, favorites)
2489+ if !ok {
2490+ return
2491+ }
2418 2492
2419 go func() {2493 go func() {
2420 ctx := o.ctx2494 ctx := o.ctx
2421 goLog := logging.FromContext(ctx)2495 goLog := logging.FromContext(ctx)
2422 2496
2423- result, err := o.favoritesUC.Toggle(ctx, s.URL, s.Title)2497+ result, err := o.favoritesUC.Toggle(ctx, target.url, target.title)
2424 if err != nil {2498 if err != nil {
2425- goLog.Error().Err(err).Str("url", s.URL).Msg("failed to toggle favorite")2499+ goLog.Error().Err(err).Str("url", target.url).Msg("failed to toggle favorite")
2426 if o.onToast != nil {2500 if o.onToast != nil {
2427- msg := "Failed to add favorite"2501+ msg := favoriteToggleErrorMessage(target.wasFavorite)
2428- if s.IsFavorite {
2429- msg = "Failed to remove favorite"
2430- }
2431 cb := glib.SourceFunc(func(_ uintptr) bool {2502 cb := glib.SourceFunc(func(_ uintptr) bool {
2432 o.onToast(ctx, msg, ToastError)2503 o.onToast(ctx, msg, ToastError)
2433 return false2504 return false
@@ -2439,14 +2510,15 @@ func (o *Omnibox) toggleFavorite() {
2439 2510
2440 // Update suggestion state2511 // Update suggestion state
2441 o.mu.Lock()2512 o.mu.Lock()
2442- if idx < len(o.suggestions) && o.suggestions[idx].URL == s.URL {2513+ update := resolveFavoriteToggleResultUpdate(o.suggestions, idx, target.url, result.Added)
2443- o.suggestions[idx].IsFavorite = result.Added2514+ if update.Apply {
2515+ o.suggestions[update.Index].IsFavorite = update.IsFavorite
2444 }2516 }
2445 o.mu.Unlock()2517 o.mu.Unlock()
2446 2518
2447 // Update row CSS and show toast on GTK main thread2519 // Update row CSS and show toast on GTK main thread
2448 cb := glib.SourceFunc(func(_ uintptr) bool {2520 cb := glib.SourceFunc(func(_ uintptr) bool {
2449- o.updateRowFavoriteIndicator(idx, s.URL, result.Added)2521+ o.updateRowFavoriteIndicator(idx, target.url, result.Added)
2450 if o.onToast != nil {2522 if o.onToast != nil {
2451 o.onToast(ctx, result.Message, ToastSuccess)2523 o.onToast(ctx, result.Message, ToastSuccess)
2452 }2524 }
@@ -2466,17 +2538,21 @@ func (o *Omnibox) toggleFavorite() {
2466 log.Debug().Msg("remove favorite: invalid ID")2538 log.Debug().Msg("remove favorite: invalid ID")
2467 return2539 return
2468 }2540 }
2541+ target, ok := resolveFavoriteToggleTarget(mode, idx, suggestions, favorites)
2542+ if !ok {
2543+ return
2544+ }
2469 entryText := o.entry.GetText()2545 entryText := o.entry.GetText()
2470 2546
2471 go func() {2547 go func() {
2472 ctx := o.ctx2548 ctx := o.ctx
2473 log := logging.FromContext(ctx)2549 log := logging.FromContext(ctx)
2474 2550
2475- log.Debug().Int64("id", f.ID).Str("url", f.URL).Msg("removing from favorites")2551+ log.Debug().Int64("id", target.favoriteID).Str("url", target.url).Msg("removing from favorites")
2476 2552
2477- err := o.favoritesUC.Remove(ctx, entity.FavoriteID(f.ID))2553+ err := o.favoritesUC.Remove(ctx, entity.FavoriteID(target.favoriteID))
2478 if err != nil {2554 if err != nil {
2479- log.Error().Err(err).Int64("id", f.ID).Msg("failed to remove favorite")2555+ log.Error().Err(err).Int64("id", target.favoriteID).Msg("failed to remove favorite")
2480 // Show error toast2556 // Show error toast
2481 if o.onToast != nil {2557 if o.onToast != nil {
2482 cb := glib.SourceFunc(func(_ uintptr) bool {2558 cb := glib.SourceFunc(func(_ uintptr) bool {
@@ -2488,7 +2564,7 @@ func (o *Omnibox) toggleFavorite() {
2488 return2564 return
2489 }2565 }
2490 2566
2491- log.Info().Int64("id", f.ID).Str("url", f.URL).Msg("favorite removed from omnibox")2567+ log.Info().Int64("id", target.favoriteID).Str("url", target.url).Msg("favorite removed from omnibox")
2492 2568
2493 // Refresh favorites list and show toast2569 // Refresh favorites list and show toast
2494 o.mu.RLock()2570 o.mu.RLock()
A internal/ui/component/omnibox_favorite_toggle_test.go +153 -0
@@ -0,0 +1,153 @@
1+package component
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+)
8+
9+func TestResolveFavoriteToggleTarget(t *testing.T) {
10+ suggestions := []Suggestion{
11+ {URL: "https://example.com", Title: "Example"},
12+ {URL: "https://favorite.example.com", Title: "Favorite", IsFavorite: true},
13+ }
14+ favorites := []Favorite{
15+ {ID: 42, URL: "https://saved.example.com", Title: "Saved"},
16+ {ID: 0, URL: "https://invalid.example.com", Title: "Invalid"},
17+ }
18+
19+ tests := []struct {
20+ name string
21+ mode ViewMode
22+ index int
23+ suggestions []Suggestion
24+ favorites []Favorite
25+ wantTarget favoriteToggleTarget
26+ wantValidTarget bool
27+ }{
28+ {
29+ name: "history add target uses selected suggestion URL and title",
30+ mode: ViewModeHistory,
31+ index: 0,
32+ suggestions: suggestions,
33+ favorites: favorites,
34+ wantTarget: favoriteToggleTarget{
35+ url: "https://example.com",
36+ title: "Example",
37+ },
38+ wantValidTarget: true,
39+ },
40+ {
41+ name: "history remove target preserves prior favorite state",
42+ mode: ViewModeHistory,
43+ index: 1,
44+ suggestions: suggestions,
45+ favorites: favorites,
46+ wantTarget: favoriteToggleTarget{
47+ url: "https://favorite.example.com",
48+ title: "Favorite",
49+ wasFavorite: true,
50+ },
51+ wantValidTarget: true,
52+ },
53+ {
54+ name: "history invalid index has no target",
55+ mode: ViewModeHistory,
56+ index: 2,
57+ suggestions: suggestions,
58+ favorites: favorites,
59+ wantValidTarget: false,
60+ },
61+ {
62+ name: "history empty URL has no target",
63+ mode: ViewModeHistory,
64+ index: 0,
65+ suggestions: []Suggestion{{Title: "Missing URL"}},
66+ favorites: favorites,
67+ wantValidTarget: false,
68+ },
69+ {
70+ name: "favorites remove target uses favorite ID",
71+ mode: ViewModeFavorites,
72+ index: 0,
73+ suggestions: suggestions,
74+ favorites: favorites,
75+ wantTarget: favoriteToggleTarget{
76+ url: "https://saved.example.com",
77+ title: "Saved",
78+ favoriteID: 42,
79+ },
80+ wantValidTarget: true,
81+ },
82+ {
83+ name: "favorites invalid index has no target",
84+ mode: ViewModeFavorites,
85+ index: -1,
86+ suggestions: suggestions,
87+ favorites: favorites,
88+ wantValidTarget: false,
89+ },
90+ {
91+ name: "favorites zero ID has no target",
92+ mode: ViewModeFavorites,
93+ index: 1,
94+ suggestions: suggestions,
95+ favorites: favorites,
96+ wantValidTarget: false,
97+ },
98+ }
99+
100+ for _, tt := range tests {
101+ t.Run(tt.name, func(t *testing.T) {
102+ gotTarget, gotValidTarget := resolveFavoriteToggleTarget(tt.mode, tt.index, tt.suggestions, tt.favorites)
103+ assert.Equal(t, tt.wantValidTarget, gotValidTarget)
104+ assert.Equal(t, tt.wantTarget, gotTarget)
105+ })
106+ }
107+}
108+
109+func TestResolveFavoriteToggleResultUpdate(t *testing.T) {
110+ suggestions := []Suggestion{{URL: "https://example.com"}}
111+
112+ tests := []struct {
113+ name string
114+ index int
115+ expectedURL string
116+ added bool
117+ want favoriteToggleResultUpdate
118+ }{
119+ {
120+ name: "matching URL updates selected suggestion favorite state",
121+ index: 0,
122+ expectedURL: "https://example.com",
123+ added: true,
124+ want: favoriteToggleResultUpdate{Apply: true, Index: 0, IsFavorite: true},
125+ },
126+ {
127+ name: "stale URL skips update",
128+ index: 0,
129+ expectedURL: "https://stale.example.com",
130+ added: true,
131+ want: favoriteToggleResultUpdate{},
132+ },
133+ {
134+ name: "out of range index skips update",
135+ index: 1,
136+ expectedURL: "https://example.com",
137+ added: true,
138+ want: favoriteToggleResultUpdate{},
139+ },
140+ }
141+
142+ for _, tt := range tests {
143+ t.Run(tt.name, func(t *testing.T) {
144+ got := resolveFavoriteToggleResultUpdate(suggestions, tt.index, tt.expectedURL, tt.added)
145+ assert.Equal(t, tt.want, got)
146+ })
147+ }
148+}
149+
150+func TestFavoriteToggleErrorMessage(t *testing.T) {
151+ assert.Equal(t, "Failed to add favorite", favoriteToggleErrorMessage(false))
152+ assert.Equal(t, "Failed to remove favorite", favoriteToggleErrorMessage(true))
153+}
@@ -0,0 +1,153 @@
1+package component
2+
3+import (
4+ "testing"
5+
6+ "github.com/stretchr/testify/assert"
7+)
8+
9+func TestResolveFavoriteToggleTarget(t *testing.T) {
10+ suggestions := []Suggestion{
11+ {URL: "https://example.com", Title: "Example"},
12+ {URL: "https://favorite.example.com", Title: "Favorite", IsFavorite: true},
13+ }
14+ favorites := []Favorite{
15+ {ID: 42, URL: "https://saved.example.com", Title: "Saved"},
16+ {ID: 0, URL: "https://invalid.example.com", Title: "Invalid"},
17+ }
18+
19+ tests := []struct {
20+ name string
21+ mode ViewMode
22+ index int
23+ suggestions []Suggestion
24+ favorites []Favorite
25+ wantTarget favoriteToggleTarget
26+ wantValidTarget bool
27+ }{
28+ {
29+ name: "history add target uses selected suggestion URL and title",
30+ mode: ViewModeHistory,
31+ index: 0,
32+ suggestions: suggestions,
33+ favorites: favorites,
34+ wantTarget: favoriteToggleTarget{
35+ url: "https://example.com",
36+ title: "Example",
37+ },
38+ wantValidTarget: true,
39+ },
40+ {
41+ name: "history remove target preserves prior favorite state",
42+ mode: ViewModeHistory,
43+ index: 1,
44+ suggestions: suggestions,
45+ favorites: favorites,
46+ wantTarget: favoriteToggleTarget{
47+ url: "https://favorite.example.com",
48+ title: "Favorite",
49+ wasFavorite: true,
50+ },
51+ wantValidTarget: true,
52+ },
53+ {
54+ name: "history invalid index has no target",
55+ mode: ViewModeHistory,
56+ index: 2,
57+ suggestions: suggestions,
58+ favorites: favorites,
59+ wantValidTarget: false,
60+ },
61+ {
62+ name: "history empty URL has no target",
63+ mode: ViewModeHistory,
64+ index: 0,
65+ suggestions: []Suggestion{{Title: "Missing URL"}},
66+ favorites: favorites,
67+ wantValidTarget: false,
68+ },
69+ {
70+ name: "favorites remove target uses favorite ID",
71+ mode: ViewModeFavorites,
72+ index: 0,
73+ suggestions: suggestions,
74+ favorites: favorites,
75+ wantTarget: favoriteToggleTarget{
76+ url: "https://saved.example.com",
77+ title: "Saved",
78+ favoriteID: 42,
79+ },
80+ wantValidTarget: true,
81+ },
82+ {
83+ name: "favorites invalid index has no target",
84+ mode: ViewModeFavorites,
85+ index: -1,
86+ suggestions: suggestions,
87+ favorites: favorites,
88+ wantValidTarget: false,
89+ },
90+ {
91+ name: "favorites zero ID has no target",
92+ mode: ViewModeFavorites,
93+ index: 1,
94+ suggestions: suggestions,
95+ favorites: favorites,
96+ wantValidTarget: false,
97+ },
98+ }
99+
100+ for _, tt := range tests {
101+ t.Run(tt.name, func(t *testing.T) {
102+ gotTarget, gotValidTarget := resolveFavoriteToggleTarget(tt.mode, tt.index, tt.suggestions, tt.favorites)
103+ assert.Equal(t, tt.wantValidTarget, gotValidTarget)
104+ assert.Equal(t, tt.wantTarget, gotTarget)
105+ })
106+ }
107+}
108+
109+func TestResolveFavoriteToggleResultUpdate(t *testing.T) {
110+ suggestions := []Suggestion{{URL: "https://example.com"}}
111+
112+ tests := []struct {
113+ name string
114+ index int
115+ expectedURL string
116+ added bool
117+ want favoriteToggleResultUpdate
118+ }{
119+ {
120+ name: "matching URL updates selected suggestion favorite state",
121+ index: 0,
122+ expectedURL: "https://example.com",
123+ added: true,
124+ want: favoriteToggleResultUpdate{Apply: true, Index: 0, IsFavorite: true},
125+ },
126+ {
127+ name: "stale URL skips update",
128+ index: 0,
129+ expectedURL: "https://stale.example.com",
130+ added: true,
131+ want: favoriteToggleResultUpdate{},
132+ },
133+ {
134+ name: "out of range index skips update",
135+ index: 1,
136+ expectedURL: "https://example.com",
137+ added: true,
138+ want: favoriteToggleResultUpdate{},
139+ },
140+ }
141+
142+ for _, tt := range tests {
143+ t.Run(tt.name, func(t *testing.T) {
144+ got := resolveFavoriteToggleResultUpdate(suggestions, tt.index, tt.expectedURL, tt.added)
145+ assert.Equal(t, tt.want, got)
146+ })
147+ }
148+}
149+
150+func TestFavoriteToggleErrorMessage(t *testing.T) {
151+ assert.Equal(t, "Failed to add favorite", favoriteToggleErrorMessage(false))
152+ assert.Equal(t, "Failed to remove favorite", favoriteToggleErrorMessage(true))
153+}

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