detached 92a7937

refactor: centralize autocomplete completion

commit
92a79376ab408166dc2d63600613df2dd1a61b31
author
brice · · 2026-01-26 04:29 UTC
parent
37c9b58
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +225 -18
M internal/application/usecase/autocomplete.go +154 -11
@@ -2,8 +2,11 @@ package usecase
22
33 import (
44 "context"
5+ "sync"
6+ "time"
57
68 "github.com/bnema/dumber/internal/domain/autocomplete"
9+ "github.com/bnema/dumber/internal/domain/entity"
710 "github.com/bnema/dumber/internal/logging"
811 )
912
@@ -12,8 +15,17 @@ type AutocompleteUseCase struct {
1215 historyUC *SearchHistoryUseCase
1316 favoritesUC *ManageFavoritesUseCase
1417 shortcutsUC *SearchShortcutsUseCase
18+
19+ cacheMu sync.Mutex
20+ favoritesCache []*entity.Favorite
21+ favoritesCached time.Time
22+ historyCache []*entity.HistoryEntry
23+ historyCached time.Time
1524 }
1625
26+const autocompleteHistoryLimit = 200
27+const autocompleteCacheTTL = 2 * time.Second
28+
1729 // NewAutocompleteUseCase creates a new autocomplete use case.
1830 func NewAutocompleteUseCase(
1931 historyUC *SearchHistoryUseCase,
@@ -38,6 +50,12 @@ type GetSuggestionOutput struct {
3850 Found bool
3951 }
4052
53+// CompletionOptions controls how URL completions are resolved.
54+type CompletionOptions struct {
55+ VisibleURLs []string
56+ AllowBangs bool
57+}
58+
4159 // GetSuggestion returns the best autocomplete suggestion for the given input.
4260 // Priority: Favorites > History (by frecency) > Bang shortcuts.
4361 func (uc *AutocompleteUseCase) GetSuggestion(ctx context.Context, input GetSuggestionInput) *GetSuggestionOutput {
@@ -77,18 +95,72 @@ func (uc *AutocompleteUseCase) GetSuggestion(ctx context.Context, input GetSugge
7795 return &GetSuggestionOutput{Found: false}
7896 }
7997
98+// ResolveCompletion resolves the best completion using visible list order first,
99+// then cached favorites/history as fallback.
100+func (uc *AutocompleteUseCase) ResolveCompletion(ctx context.Context, input string, opts CompletionOptions) *autocomplete.Suggestion {
101+ log := logging.FromContext(ctx)
102+
103+ if input == "" {
104+ return nil
105+ }
106+
107+ if len(opts.VisibleURLs) > 0 {
108+ if suggestion := findCompletionInURLs(ctx, input, opts.VisibleURLs, autocomplete.SourceHistory); suggestion != nil {
109+ log.Debug().
110+ Str("input", input).
111+ Str("match", suggestion.FullText).
112+ Msg("autocomplete: matched visible list")
113+ return suggestion
114+ }
115+ }
116+
117+ if suggestion := uc.findFavoriteSuggestion(ctx, input); suggestion != nil {
118+ log.Debug().
119+ Str("input", input).
120+ Str("match", suggestion.FullText).
121+ Msg("autocomplete: matched favorite fallback")
122+ return suggestion
123+ }
124+
125+ if suggestion := uc.findHistorySuggestion(ctx, input); suggestion != nil {
126+ log.Debug().
127+ Str("input", input).
128+ Str("match", suggestion.FullText).
129+ Msg("autocomplete: matched history fallback")
130+ return suggestion
131+ }
132+
133+ if opts.AllowBangs {
134+ if suggestion := uc.findBangSuggestion(ctx, input); suggestion != nil {
135+ log.Debug().
136+ Str("input", input).
137+ Str("match", suggestion.FullText).
138+ Msg("autocomplete: matched bang fallback")
139+ return suggestion
140+ }
141+ }
142+
143+ return nil
144+}
145+
80146 // findFavoriteSuggestion finds the best matching favorite for the input.
81147 func (uc *AutocompleteUseCase) findFavoriteSuggestion(ctx context.Context, input string) *autocomplete.Suggestion {
148+ log := logging.FromContext(ctx)
149+
82150 if uc.favoritesUC == nil {
83151 return nil
84152 }
85153
86- favorites, err := uc.favoritesUC.GetAll(ctx)
154+ favorites, err := uc.getCachedFavorites(ctx)
87155 if err != nil {
156+ log.Debug().Err(err).Msg("autocomplete: favorites cache fetch failed")
88157 return nil
89158 }
90159
91160 for _, fav := range favorites {
161+ if fav == nil || fav.URL == "" {
162+ continue
163+ }
92164 suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, fav.URL)
93165 if ok {
94166 return &autocomplete.Suggestion{
@@ -105,28 +177,29 @@ func (uc *AutocompleteUseCase) findFavoriteSuggestion(ctx context.Context, input
105177
106178 // findHistorySuggestion finds the best matching history entry for the input.
107179 func (uc *AutocompleteUseCase) findHistorySuggestion(ctx context.Context, input string) *autocomplete.Suggestion {
180+ log := logging.FromContext(ctx)
181+
108182 if uc.historyUC == nil {
109183 return nil
110184 }
111185
112- // Search history for matches
113- output, err := uc.historyUC.Search(ctx, SearchInput{
114- Query: input,
115- Limit: 10, // Get top 10 to find best prefix match
116- })
117- if err != nil || output == nil {
186+ entries, err := uc.getCachedHistory(ctx)
187+ if err != nil {
188+ log.Debug().Err(err).Msg("autocomplete: history cache fetch failed")
118189 return nil
119190 }
120191
121- // Find the first result that is a prefix match
122- for _, match := range output.Matches {
123- suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, match.Entry.URL)
192+ for _, entry := range entries {
193+ if entry == nil || entry.URL == "" {
194+ continue
195+ }
196+ suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, entry.URL)
124197 if ok {
125198 return &autocomplete.Suggestion{
126199 FullText: matchedURL,
127200 Suffix: suffix,
128201 Source: autocomplete.SourceHistory,
129- Title: match.Entry.Title,
202+ Title: entry.Title,
130203 }
131204 }
132205 }
@@ -136,6 +209,8 @@ func (uc *AutocompleteUseCase) findHistorySuggestion(ctx context.Context, input
136209
137210 // findBangSuggestion finds the best matching bang shortcut for the input.
138211 func (uc *AutocompleteUseCase) findBangSuggestion(ctx context.Context, input string) *autocomplete.Suggestion {
212+ _ = logging.FromContext(ctx)
213+
139214 if uc.shortcutsUC == nil || len(input) < 1 || input[0] != '!' {
140215 return nil
141216 }
@@ -163,6 +238,74 @@ func (uc *AutocompleteUseCase) findBangSuggestion(ctx context.Context, input str
163238 return nil
164239 }
165240
241+func findCompletionInURLs(_ context.Context, input string, urls []string, source autocomplete.SuggestionSource) *autocomplete.Suggestion {
242+ for _, u := range urls {
243+ if u == "" {
244+ continue
245+ }
246+ suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, u)
247+ if ok {
248+ return &autocomplete.Suggestion{
249+ FullText: matchedURL,
250+ Suffix: suffix,
251+ Source: source,
252+ }
253+ }
254+ }
255+
256+ return nil
257+}
258+
259+func (uc *AutocompleteUseCase) getCachedFavorites(ctx context.Context) ([]*entity.Favorite, error) {
260+ log := logging.FromContext(ctx)
261+
262+ uc.cacheMu.Lock()
263+ if time.Since(uc.favoritesCached) < autocompleteCacheTTL && uc.favoritesCache != nil {
264+ favorites := uc.favoritesCache
265+ uc.cacheMu.Unlock()
266+ return favorites, nil
267+ }
268+ uc.cacheMu.Unlock()
269+
270+ favorites, err := uc.favoritesUC.GetAll(ctx)
271+ if err != nil {
272+ return nil, err
273+ }
274+
275+ uc.cacheMu.Lock()
276+ uc.favoritesCache = favorites
277+ uc.favoritesCached = time.Now()
278+ uc.cacheMu.Unlock()
279+
280+ log.Debug().Int("count", len(favorites)).Msg("autocomplete: favorites cache refreshed")
281+ return favorites, nil
282+}
283+
284+func (uc *AutocompleteUseCase) getCachedHistory(ctx context.Context) ([]*entity.HistoryEntry, error) {
285+ log := logging.FromContext(ctx)
286+
287+ uc.cacheMu.Lock()
288+ if time.Since(uc.historyCached) < autocompleteCacheTTL && uc.historyCache != nil {
289+ history := uc.historyCache
290+ uc.cacheMu.Unlock()
291+ return history, nil
292+ }
293+ uc.cacheMu.Unlock()
294+
295+ entries, err := uc.historyUC.GetRecent(ctx, autocompleteHistoryLimit, 0)
296+ if err != nil {
297+ return nil, err
298+ }
299+
300+ uc.cacheMu.Lock()
301+ uc.historyCache = entries
302+ uc.historyCached = time.Now()
303+ uc.cacheMu.Unlock()
304+
305+ log.Debug().Int("count", len(entries)).Msg("autocomplete: history cache refreshed")
306+ return entries, nil
307+}
308+
166309 // GetSuggestionForURL returns an autocomplete suggestion specifically for a URL.
167310 // This is used when the user selects a row from the list to update ghost text.
168311 func (*AutocompleteUseCase) GetSuggestionForURL(_ context.Context, input, targetURL string) *GetSuggestionOutput {
@@ -2,8 +2,11 @@ package usecase
2 2
3 import (3 import (
4 "context"4 "context"
5+ "sync"
6+ "time"
5 7
6 "github.com/bnema/dumber/internal/domain/autocomplete"8 "github.com/bnema/dumber/internal/domain/autocomplete"
9+ "github.com/bnema/dumber/internal/domain/entity"
7 "github.com/bnema/dumber/internal/logging"10 "github.com/bnema/dumber/internal/logging"
8 )11 )
9 12
@@ -12,8 +15,17 @@ type AutocompleteUseCase struct {
12 historyUC *SearchHistoryUseCase15 historyUC *SearchHistoryUseCase
13 favoritesUC *ManageFavoritesUseCase16 favoritesUC *ManageFavoritesUseCase
14 shortcutsUC *SearchShortcutsUseCase17 shortcutsUC *SearchShortcutsUseCase
18+
19+ cacheMu sync.Mutex
20+ favoritesCache []*entity.Favorite
21+ favoritesCached time.Time
22+ historyCache []*entity.HistoryEntry
23+ historyCached time.Time
15 }24 }
16 25
26+const autocompleteHistoryLimit = 200
27+const autocompleteCacheTTL = 2 * time.Second
28+
17 // NewAutocompleteUseCase creates a new autocomplete use case.29 // NewAutocompleteUseCase creates a new autocomplete use case.
18 func NewAutocompleteUseCase(30 func NewAutocompleteUseCase(
19 historyUC *SearchHistoryUseCase,31 historyUC *SearchHistoryUseCase,
@@ -38,6 +50,12 @@ type GetSuggestionOutput struct {
38 Found bool50 Found bool
39 }51 }
40 52
53+// CompletionOptions controls how URL completions are resolved.
54+type CompletionOptions struct {
55+ VisibleURLs []string
56+ AllowBangs bool
57+}
58+
41 // GetSuggestion returns the best autocomplete suggestion for the given input.59 // GetSuggestion returns the best autocomplete suggestion for the given input.
42 // Priority: Favorites > History (by frecency) > Bang shortcuts.60 // Priority: Favorites > History (by frecency) > Bang shortcuts.
43 func (uc *AutocompleteUseCase) GetSuggestion(ctx context.Context, input GetSuggestionInput) *GetSuggestionOutput {61 func (uc *AutocompleteUseCase) GetSuggestion(ctx context.Context, input GetSuggestionInput) *GetSuggestionOutput {
@@ -77,18 +95,72 @@ func (uc *AutocompleteUseCase) GetSuggestion(ctx context.Context, input GetSugge
77 return &GetSuggestionOutput{Found: false}95 return &GetSuggestionOutput{Found: false}
78 }96 }
79 97
98+// ResolveCompletion resolves the best completion using visible list order first,
99+// then cached favorites/history as fallback.
100+func (uc *AutocompleteUseCase) ResolveCompletion(ctx context.Context, input string, opts CompletionOptions) *autocomplete.Suggestion {
101+ log := logging.FromContext(ctx)
102+
103+ if input == "" {
104+ return nil
105+ }
106+
107+ if len(opts.VisibleURLs) > 0 {
108+ if suggestion := findCompletionInURLs(ctx, input, opts.VisibleURLs, autocomplete.SourceHistory); suggestion != nil {
109+ log.Debug().
110+ Str("input", input).
111+ Str("match", suggestion.FullText).
112+ Msg("autocomplete: matched visible list")
113+ return suggestion
114+ }
115+ }
116+
117+ if suggestion := uc.findFavoriteSuggestion(ctx, input); suggestion != nil {
118+ log.Debug().
119+ Str("input", input).
120+ Str("match", suggestion.FullText).
121+ Msg("autocomplete: matched favorite fallback")
122+ return suggestion
123+ }
124+
125+ if suggestion := uc.findHistorySuggestion(ctx, input); suggestion != nil {
126+ log.Debug().
127+ Str("input", input).
128+ Str("match", suggestion.FullText).
129+ Msg("autocomplete: matched history fallback")
130+ return suggestion
131+ }
132+
133+ if opts.AllowBangs {
134+ if suggestion := uc.findBangSuggestion(ctx, input); suggestion != nil {
135+ log.Debug().
136+ Str("input", input).
137+ Str("match", suggestion.FullText).
138+ Msg("autocomplete: matched bang fallback")
139+ return suggestion
140+ }
141+ }
142+
143+ return nil
144+}
145+
80 // findFavoriteSuggestion finds the best matching favorite for the input.146 // findFavoriteSuggestion finds the best matching favorite for the input.
81 func (uc *AutocompleteUseCase) findFavoriteSuggestion(ctx context.Context, input string) *autocomplete.Suggestion {147 func (uc *AutocompleteUseCase) findFavoriteSuggestion(ctx context.Context, input string) *autocomplete.Suggestion {
148+ log := logging.FromContext(ctx)
149+
82 if uc.favoritesUC == nil {150 if uc.favoritesUC == nil {
83 return nil151 return nil
84 }152 }
85 153
86- favorites, err := uc.favoritesUC.GetAll(ctx)154+ favorites, err := uc.getCachedFavorites(ctx)
87 if err != nil {155 if err != nil {
156+ log.Debug().Err(err).Msg("autocomplete: favorites cache fetch failed")
88 return nil157 return nil
89 }158 }
90 159
91 for _, fav := range favorites {160 for _, fav := range favorites {
161+ if fav == nil || fav.URL == "" {
162+ continue
163+ }
92 suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, fav.URL)164 suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, fav.URL)
93 if ok {165 if ok {
94 return &autocomplete.Suggestion{166 return &autocomplete.Suggestion{
@@ -105,28 +177,29 @@ func (uc *AutocompleteUseCase) findFavoriteSuggestion(ctx context.Context, input
105 177
106 // findHistorySuggestion finds the best matching history entry for the input.178 // findHistorySuggestion finds the best matching history entry for the input.
107 func (uc *AutocompleteUseCase) findHistorySuggestion(ctx context.Context, input string) *autocomplete.Suggestion {179 func (uc *AutocompleteUseCase) findHistorySuggestion(ctx context.Context, input string) *autocomplete.Suggestion {
180+ log := logging.FromContext(ctx)
181+
108 if uc.historyUC == nil {182 if uc.historyUC == nil {
109 return nil183 return nil
110 }184 }
111 185
112- // Search history for matches186+ entries, err := uc.getCachedHistory(ctx)
113- output, err := uc.historyUC.Search(ctx, SearchInput{187+ if err != nil {
114- Query: input,188+ log.Debug().Err(err).Msg("autocomplete: history cache fetch failed")
115- Limit: 10, // Get top 10 to find best prefix match
116- })
117- if err != nil || output == nil {
118 return nil189 return nil
119 }190 }
120 191
121- // Find the first result that is a prefix match192+ for _, entry := range entries {
122- for _, match := range output.Matches {193+ if entry == nil || entry.URL == "" {
123- suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, match.Entry.URL)194+ continue
195+ }
196+ suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, entry.URL)
124 if ok {197 if ok {
125 return &autocomplete.Suggestion{198 return &autocomplete.Suggestion{
126 FullText: matchedURL,199 FullText: matchedURL,
127 Suffix: suffix,200 Suffix: suffix,
128 Source: autocomplete.SourceHistory,201 Source: autocomplete.SourceHistory,
129- Title: match.Entry.Title,202+ Title: entry.Title,
130 }203 }
131 }204 }
132 }205 }
@@ -136,6 +209,8 @@ func (uc *AutocompleteUseCase) findHistorySuggestion(ctx context.Context, input
136 209
137 // findBangSuggestion finds the best matching bang shortcut for the input.210 // findBangSuggestion finds the best matching bang shortcut for the input.
138 func (uc *AutocompleteUseCase) findBangSuggestion(ctx context.Context, input string) *autocomplete.Suggestion {211 func (uc *AutocompleteUseCase) findBangSuggestion(ctx context.Context, input string) *autocomplete.Suggestion {
212+ _ = logging.FromContext(ctx)
213+
139 if uc.shortcutsUC == nil || len(input) < 1 || input[0] != '!' {214 if uc.shortcutsUC == nil || len(input) < 1 || input[0] != '!' {
140 return nil215 return nil
141 }216 }
@@ -163,6 +238,74 @@ func (uc *AutocompleteUseCase) findBangSuggestion(ctx context.Context, input str
163 return nil238 return nil
164 }239 }
165 240
241+func findCompletionInURLs(_ context.Context, input string, urls []string, source autocomplete.SuggestionSource) *autocomplete.Suggestion {
242+ for _, u := range urls {
243+ if u == "" {
244+ continue
245+ }
246+ suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, u)
247+ if ok {
248+ return &autocomplete.Suggestion{
249+ FullText: matchedURL,
250+ Suffix: suffix,
251+ Source: source,
252+ }
253+ }
254+ }
255+
256+ return nil
257+}
258+
259+func (uc *AutocompleteUseCase) getCachedFavorites(ctx context.Context) ([]*entity.Favorite, error) {
260+ log := logging.FromContext(ctx)
261+
262+ uc.cacheMu.Lock()
263+ if time.Since(uc.favoritesCached) < autocompleteCacheTTL && uc.favoritesCache != nil {
264+ favorites := uc.favoritesCache
265+ uc.cacheMu.Unlock()
266+ return favorites, nil
267+ }
268+ uc.cacheMu.Unlock()
269+
270+ favorites, err := uc.favoritesUC.GetAll(ctx)
271+ if err != nil {
272+ return nil, err
273+ }
274+
275+ uc.cacheMu.Lock()
276+ uc.favoritesCache = favorites
277+ uc.favoritesCached = time.Now()
278+ uc.cacheMu.Unlock()
279+
280+ log.Debug().Int("count", len(favorites)).Msg("autocomplete: favorites cache refreshed")
281+ return favorites, nil
282+}
283+
284+func (uc *AutocompleteUseCase) getCachedHistory(ctx context.Context) ([]*entity.HistoryEntry, error) {
285+ log := logging.FromContext(ctx)
286+
287+ uc.cacheMu.Lock()
288+ if time.Since(uc.historyCached) < autocompleteCacheTTL && uc.historyCache != nil {
289+ history := uc.historyCache
290+ uc.cacheMu.Unlock()
291+ return history, nil
292+ }
293+ uc.cacheMu.Unlock()
294+
295+ entries, err := uc.historyUC.GetRecent(ctx, autocompleteHistoryLimit, 0)
296+ if err != nil {
297+ return nil, err
298+ }
299+
300+ uc.cacheMu.Lock()
301+ uc.historyCache = entries
302+ uc.historyCached = time.Now()
303+ uc.cacheMu.Unlock()
304+
305+ log.Debug().Int("count", len(entries)).Msg("autocomplete: history cache refreshed")
306+ return entries, nil
307+}
308+
166 // GetSuggestionForURL returns an autocomplete suggestion specifically for a URL.309 // GetSuggestionForURL returns an autocomplete suggestion specifically for a URL.
167 // This is used when the user selects a row from the list to update ghost text.310 // This is used when the user selects a row from the list to update ghost text.
168 func (*AutocompleteUseCase) GetSuggestionForURL(_ context.Context, input, targetURL string) *GetSuggestionOutput {311 func (*AutocompleteUseCase) GetSuggestionForURL(_ context.Context, input, targetURL string) *GetSuggestionOutput {
M internal/application/usecase/autocomplete_test.go +71 -7
@@ -71,10 +71,10 @@ func TestAutocompleteUseCase_GetSuggestion_HistoryFallback(t *testing.T) {
7171
7272 // History returns a match
7373 historyRepo := repomocks.NewMockHistoryRepository(t)
74- historyMatches := []entity.HistoryMatch{
75- {Entry: &entity.HistoryEntry{URL: "https://news.ycombinator.com", Title: "Hacker News"}},
74+ historyEntries := []*entity.HistoryEntry{
75+ {URL: "https://news.ycombinator.com", Title: "Hacker News"},
7676 }
77- historyRepo.EXPECT().Search(mock.Anything, "news", 10).Return(historyMatches, nil)
77+ historyRepo.EXPECT().GetRecent(mock.Anything, mock.Anything, 0).Return(historyEntries, nil)
7878
7979 favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
8080 historyUC := usecase.NewSearchHistoryUseCase(historyRepo)
@@ -136,7 +136,7 @@ func TestAutocompleteUseCase_GetSuggestion_NoMatch(t *testing.T) {
136136 favoriteRepo.EXPECT().GetAll(mock.Anything).Return([]*entity.Favorite{}, nil)
137137
138138 historyRepo := repomocks.NewMockHistoryRepository(t)
139- historyRepo.EXPECT().Search(mock.Anything, "xyz123", 10).Return([]entity.HistoryMatch{}, nil)
139+ historyRepo.EXPECT().GetRecent(mock.Anything, mock.Anything, 0).Return([]*entity.HistoryEntry{}, nil)
140140
141141 favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
142142 historyUC := usecase.NewSearchHistoryUseCase(historyRepo)
@@ -148,6 +148,70 @@ func TestAutocompleteUseCase_GetSuggestion_NoMatch(t *testing.T) {
148148 assert.False(t, output.Found)
149149 }
150150
151+func TestAutocompleteUseCase_ResolveCompletion_VisibleURLsOrder(t *testing.T) {
152+ ctx := testContext()
153+ uc := usecase.NewAutocompleteUseCase(nil, nil, nil)
154+
155+ urls := []string{
156+ "https://bnema.dev",
157+ "https://bnema.io",
158+ }
159+
160+ suggestion := uc.ResolveCompletion(ctx, "bnem", usecase.CompletionOptions{
161+ VisibleURLs: urls,
162+ AllowBangs: false,
163+ })
164+
165+ require.NotNil(t, suggestion)
166+ assert.Equal(t, "bnema.dev", suggestion.FullText)
167+ assert.Equal(t, "a.dev", suggestion.Suffix)
168+}
169+
170+func TestAutocompleteUseCase_GetSuggestion_FavoritesCached(t *testing.T) {
171+ ctx := testContext()
172+
173+ favoriteRepo := repomocks.NewMockFavoriteRepository(t)
174+ folderRepo := repomocks.NewMockFolderRepository(t)
175+ tagRepo := repomocks.NewMockTagRepository(t)
176+
177+ favorites := []*entity.Favorite{
178+ {ID: 1, URL: "https://github.com/user/repo", Title: "Repo"},
179+ }
180+ favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil).Once()
181+
182+ favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
183+ uc := usecase.NewAutocompleteUseCase(nil, favoritesUC, nil)
184+
185+ output := uc.GetSuggestion(ctx, usecase.GetSuggestionInput{Input: "github"})
186+ require.NotNil(t, output)
187+ assert.True(t, output.Found)
188+
189+ output = uc.GetSuggestion(ctx, usecase.GetSuggestionInput{Input: "github"})
190+ require.NotNil(t, output)
191+ assert.True(t, output.Found)
192+}
193+
194+func TestAutocompleteUseCase_GetSuggestion_HistoryCached(t *testing.T) {
195+ ctx := testContext()
196+
197+ historyRepo := repomocks.NewMockHistoryRepository(t)
198+ historyEntries := []*entity.HistoryEntry{
199+ {URL: "https://news.ycombinator.com", Title: "Hacker News"},
200+ }
201+ historyRepo.EXPECT().GetRecent(mock.Anything, mock.Anything, 0).Return(historyEntries, nil).Once()
202+
203+ historyUC := usecase.NewSearchHistoryUseCase(historyRepo)
204+ uc := usecase.NewAutocompleteUseCase(historyUC, nil, nil)
205+
206+ output := uc.GetSuggestion(ctx, usecase.GetSuggestionInput{Input: "news"})
207+ require.NotNil(t, output)
208+ assert.True(t, output.Found)
209+
210+ output = uc.GetSuggestion(ctx, usecase.GetSuggestionInput{Input: "news"})
211+ require.NotNil(t, output)
212+ assert.True(t, output.Found)
213+}
214+
151215 func TestAutocompleteUseCase_GetSuggestionForURL_EmptyInput(t *testing.T) {
152216 ctx := testContext()
153217 uc := usecase.NewAutocompleteUseCase(nil, nil, nil)
@@ -266,11 +330,11 @@ func TestAutocompleteUseCase_GetSuggestion_HistoryNotPrefixMatch(t *testing.T) {
266330
267331 // History returns results but none are prefix matches
268332 historyRepo := repomocks.NewMockHistoryRepository(t)
269- historyMatches := []entity.HistoryMatch{
333+ historyEntries := []*entity.HistoryEntry{
270334 // URL doesn't start with "git", so no prefix match
271- {Entry: &entity.HistoryEntry{URL: "https://example.com/git", Title: "Example"}},
335+ {URL: "https://example.com/git", Title: "Example"},
272336 }
273- historyRepo.EXPECT().Search(mock.Anything, "git", 10).Return(historyMatches, nil)
337+ historyRepo.EXPECT().GetRecent(mock.Anything, mock.Anything, 0).Return(historyEntries, nil)
274338
275339 favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
276340 historyUC := usecase.NewSearchHistoryUseCase(historyRepo)
@@ -71,10 +71,10 @@ func TestAutocompleteUseCase_GetSuggestion_HistoryFallback(t *testing.T) {
71 71
72 // History returns a match72 // History returns a match
73 historyRepo := repomocks.NewMockHistoryRepository(t)73 historyRepo := repomocks.NewMockHistoryRepository(t)
74- historyMatches := []entity.HistoryMatch{74+ historyEntries := []*entity.HistoryEntry{
75- {Entry: &entity.HistoryEntry{URL: "https://news.ycombinator.com", Title: "Hacker News"}},75+ {URL: "https://news.ycombinator.com", Title: "Hacker News"},
76 }76 }
77- historyRepo.EXPECT().Search(mock.Anything, "news", 10).Return(historyMatches, nil)77+ historyRepo.EXPECT().GetRecent(mock.Anything, mock.Anything, 0).Return(historyEntries, nil)
78 78
79 favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)79 favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
80 historyUC := usecase.NewSearchHistoryUseCase(historyRepo)80 historyUC := usecase.NewSearchHistoryUseCase(historyRepo)
@@ -136,7 +136,7 @@ func TestAutocompleteUseCase_GetSuggestion_NoMatch(t *testing.T) {
136 favoriteRepo.EXPECT().GetAll(mock.Anything).Return([]*entity.Favorite{}, nil)136 favoriteRepo.EXPECT().GetAll(mock.Anything).Return([]*entity.Favorite{}, nil)
137 137
138 historyRepo := repomocks.NewMockHistoryRepository(t)138 historyRepo := repomocks.NewMockHistoryRepository(t)
139- historyRepo.EXPECT().Search(mock.Anything, "xyz123", 10).Return([]entity.HistoryMatch{}, nil)139+ historyRepo.EXPECT().GetRecent(mock.Anything, mock.Anything, 0).Return([]*entity.HistoryEntry{}, nil)
140 140
141 favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)141 favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
142 historyUC := usecase.NewSearchHistoryUseCase(historyRepo)142 historyUC := usecase.NewSearchHistoryUseCase(historyRepo)
@@ -148,6 +148,70 @@ func TestAutocompleteUseCase_GetSuggestion_NoMatch(t *testing.T) {
148 assert.False(t, output.Found)148 assert.False(t, output.Found)
149 }149 }
150 150
151+func TestAutocompleteUseCase_ResolveCompletion_VisibleURLsOrder(t *testing.T) {
152+ ctx := testContext()
153+ uc := usecase.NewAutocompleteUseCase(nil, nil, nil)
154+
155+ urls := []string{
156+ "https://bnema.dev",
157+ "https://bnema.io",
158+ }
159+
160+ suggestion := uc.ResolveCompletion(ctx, "bnem", usecase.CompletionOptions{
161+ VisibleURLs: urls,
162+ AllowBangs: false,
163+ })
164+
165+ require.NotNil(t, suggestion)
166+ assert.Equal(t, "bnema.dev", suggestion.FullText)
167+ assert.Equal(t, "a.dev", suggestion.Suffix)
168+}
169+
170+func TestAutocompleteUseCase_GetSuggestion_FavoritesCached(t *testing.T) {
171+ ctx := testContext()
172+
173+ favoriteRepo := repomocks.NewMockFavoriteRepository(t)
174+ folderRepo := repomocks.NewMockFolderRepository(t)
175+ tagRepo := repomocks.NewMockTagRepository(t)
176+
177+ favorites := []*entity.Favorite{
178+ {ID: 1, URL: "https://github.com/user/repo", Title: "Repo"},
179+ }
180+ favoriteRepo.EXPECT().GetAll(mock.Anything).Return(favorites, nil).Once()
181+
182+ favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
183+ uc := usecase.NewAutocompleteUseCase(nil, favoritesUC, nil)
184+
185+ output := uc.GetSuggestion(ctx, usecase.GetSuggestionInput{Input: "github"})
186+ require.NotNil(t, output)
187+ assert.True(t, output.Found)
188+
189+ output = uc.GetSuggestion(ctx, usecase.GetSuggestionInput{Input: "github"})
190+ require.NotNil(t, output)
191+ assert.True(t, output.Found)
192+}
193+
194+func TestAutocompleteUseCase_GetSuggestion_HistoryCached(t *testing.T) {
195+ ctx := testContext()
196+
197+ historyRepo := repomocks.NewMockHistoryRepository(t)
198+ historyEntries := []*entity.HistoryEntry{
199+ {URL: "https://news.ycombinator.com", Title: "Hacker News"},
200+ }
201+ historyRepo.EXPECT().GetRecent(mock.Anything, mock.Anything, 0).Return(historyEntries, nil).Once()
202+
203+ historyUC := usecase.NewSearchHistoryUseCase(historyRepo)
204+ uc := usecase.NewAutocompleteUseCase(historyUC, nil, nil)
205+
206+ output := uc.GetSuggestion(ctx, usecase.GetSuggestionInput{Input: "news"})
207+ require.NotNil(t, output)
208+ assert.True(t, output.Found)
209+
210+ output = uc.GetSuggestion(ctx, usecase.GetSuggestionInput{Input: "news"})
211+ require.NotNil(t, output)
212+ assert.True(t, output.Found)
213+}
214+
151 func TestAutocompleteUseCase_GetSuggestionForURL_EmptyInput(t *testing.T) {215 func TestAutocompleteUseCase_GetSuggestionForURL_EmptyInput(t *testing.T) {
152 ctx := testContext()216 ctx := testContext()
153 uc := usecase.NewAutocompleteUseCase(nil, nil, nil)217 uc := usecase.NewAutocompleteUseCase(nil, nil, nil)
@@ -266,11 +330,11 @@ func TestAutocompleteUseCase_GetSuggestion_HistoryNotPrefixMatch(t *testing.T) {
266 330
267 // History returns results but none are prefix matches331 // History returns results but none are prefix matches
268 historyRepo := repomocks.NewMockHistoryRepository(t)332 historyRepo := repomocks.NewMockHistoryRepository(t)
269- historyMatches := []entity.HistoryMatch{333+ historyEntries := []*entity.HistoryEntry{
270 // URL doesn't start with "git", so no prefix match334 // URL doesn't start with "git", so no prefix match
271- {Entry: &entity.HistoryEntry{URL: "https://example.com/git", Title: "Example"}},335+ {URL: "https://example.com/git", Title: "Example"},
272 }336 }
273- historyRepo.EXPECT().Search(mock.Anything, "git", 10).Return(historyMatches, nil)337+ historyRepo.EXPECT().GetRecent(mock.Anything, mock.Anything, 0).Return(historyEntries, nil)
274 338
275 favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)339 favoritesUC := usecase.NewManageFavoritesUseCase(favoriteRepo, folderRepo, tagRepo)
276 historyUC := usecase.NewSearchHistoryUseCase(historyRepo)340 historyUC := usecase.NewSearchHistoryUseCase(historyRepo)

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