v0.26.0 ae3a77a

autocomplete.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5
6	"github.com/bnema/dumber/internal/domain/autocomplete"
7	"github.com/bnema/dumber/internal/logging"
8)
9
10// AutocompleteUseCase handles inline autocomplete suggestions.
11type AutocompleteUseCase struct {
12	historyUC   *SearchHistoryUseCase
13	favoritesUC *ManageFavoritesUseCase
14	shortcutsUC *SearchShortcutsUseCase
15}
16
17// NewAutocompleteUseCase creates a new autocomplete use case.
18func NewAutocompleteUseCase(
19	historyUC *SearchHistoryUseCase,
20	favoritesUC *ManageFavoritesUseCase,
21	shortcutsUC *SearchShortcutsUseCase,
22) *AutocompleteUseCase {
23	return &AutocompleteUseCase{
24		historyUC:   historyUC,
25		favoritesUC: favoritesUC,
26		shortcutsUC: shortcutsUC,
27	}
28}
29
30// GetSuggestionInput contains parameters for getting an autocomplete suggestion.
31type GetSuggestionInput struct {
32	Input string // Current user input
33}
34
35// GetSuggestionOutput contains the best autocomplete suggestion.
36type GetSuggestionOutput struct {
37	Suggestion *autocomplete.Suggestion
38	Found      bool
39}
40
41// GetSuggestion returns the best autocomplete suggestion for the given input.
42// Priority: Favorites > History (by frecency) > Bang shortcuts.
43func (uc *AutocompleteUseCase) GetSuggestion(ctx context.Context, input GetSuggestionInput) *GetSuggestionOutput {
44	log := logging.FromContext(ctx)
45
46	if input.Input == "" {
47		return &GetSuggestionOutput{Found: false}
48	}
49
50	// Try favorites first (highest priority)
51	if suggestion := uc.findFavoriteSuggestion(ctx, input.Input); suggestion != nil {
52		log.Debug().
53			Str("input", input.Input).
54			Str("match", suggestion.FullText).
55			Msg("autocomplete: matched favorite")
56		return &GetSuggestionOutput{Suggestion: suggestion, Found: true}
57	}
58
59	// Try history (second priority)
60	if suggestion := uc.findHistorySuggestion(ctx, input.Input); suggestion != nil {
61		log.Debug().
62			Str("input", input.Input).
63			Str("match", suggestion.FullText).
64			Msg("autocomplete: matched history")
65		return &GetSuggestionOutput{Suggestion: suggestion, Found: true}
66	}
67
68	// Try bang shortcuts (lowest priority for URL-style completion)
69	if suggestion := uc.findBangSuggestion(ctx, input.Input); suggestion != nil {
70		log.Debug().
71			Str("input", input.Input).
72			Str("match", suggestion.FullText).
73			Msg("autocomplete: matched bang shortcut")
74		return &GetSuggestionOutput{Suggestion: suggestion, Found: true}
75	}
76
77	return &GetSuggestionOutput{Found: false}
78}
79
80// findFavoriteSuggestion finds the best matching favorite for the input.
81func (uc *AutocompleteUseCase) findFavoriteSuggestion(ctx context.Context, input string) *autocomplete.Suggestion {
82	if uc.favoritesUC == nil {
83		return nil
84	}
85
86	favorites, err := uc.favoritesUC.GetAll(ctx)
87	if err != nil {
88		return nil
89	}
90
91	for _, fav := range favorites {
92		suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, fav.URL)
93		if ok {
94			return &autocomplete.Suggestion{
95				FullText: matchedURL,
96				Suffix:   suffix,
97				Source:   autocomplete.SourceFavorite,
98				Title:    fav.Title,
99			}
100		}
101	}
102
103	return nil
104}
105
106// findHistorySuggestion finds the best matching history entry for the input.
107func (uc *AutocompleteUseCase) findHistorySuggestion(ctx context.Context, input string) *autocomplete.Suggestion {
108	if uc.historyUC == nil {
109		return nil
110	}
111
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 {
118		return nil
119	}
120
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)
124		if ok {
125			return &autocomplete.Suggestion{
126				FullText: matchedURL,
127				Suffix:   suffix,
128				Source:   autocomplete.SourceHistory,
129				Title:    match.Entry.Title,
130			}
131		}
132	}
133
134	return nil
135}
136
137// findBangSuggestion finds the best matching bang shortcut for the input.
138func (uc *AutocompleteUseCase) findBangSuggestion(ctx context.Context, input string) *autocomplete.Suggestion {
139	if uc.shortcutsUC == nil || len(input) < 1 || input[0] != '!' {
140		return nil
141	}
142
143	// Get filtered bang suggestions
144	output := uc.shortcutsUC.FilterBangs(ctx, FilterBangsInput{Query: input})
145	if len(output.Suggestions) == 0 {
146		return nil
147	}
148
149	// Use the first (best) match
150	bang := output.Suggestions[0]
151	fullBang := "!" + bang.Key
152
153	suffix, ok := autocomplete.ComputeCompletionSuffix(input, fullBang)
154	if ok {
155		return &autocomplete.Suggestion{
156			FullText: fullBang,
157			Suffix:   suffix,
158			Source:   autocomplete.SourceBangShortcut,
159			Title:    bang.Description,
160		}
161	}
162
163	return nil
164}
165
166// 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.
168func (*AutocompleteUseCase) GetSuggestionForURL(_ context.Context, input, targetURL string) *GetSuggestionOutput {
169	if input == "" || targetURL == "" {
170		return &GetSuggestionOutput{Found: false}
171	}
172
173	suffix, matchedURL, ok := autocomplete.ComputeURLCompletionSuffix(input, targetURL)
174	if !ok {
175		return &GetSuggestionOutput{Found: false}
176	}
177
178	return &GetSuggestionOutput{
179		Suggestion: &autocomplete.Suggestion{
180			FullText: matchedURL,
181			Suffix:   suffix,
182			Source:   autocomplete.SourceHistory, // Default to history for selected rows
183		},
184		Found: true,
185	}
186}

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