v0.23.2 bfd5eb4

search_shortcuts.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"sort"
6	"strings"
7
8	"github.com/bnema/dumber/internal/domain/url"
9	"github.com/bnema/dumber/internal/logging"
10)
11
12// SearchShortcut represents a search shortcut configuration.
13// This is a domain-level type to avoid depending on infrastructure config.
14type SearchShortcut struct {
15	URL         string
16	Description string
17}
18
19// BangSuggestion represents a configured bang shortcut for display.
20type BangSuggestion struct {
21	Key         string
22	Description string
23}
24
25// SearchShortcutsUseCase handles bang shortcut filtering and resolution.
26type SearchShortcutsUseCase struct {
27	shortcuts map[string]SearchShortcut
28}
29
30// NewSearchShortcutsUseCase creates a new search shortcuts use case.
31func NewSearchShortcutsUseCase(shortcuts map[string]SearchShortcut) *SearchShortcutsUseCase {
32	return &SearchShortcutsUseCase{
33		shortcuts: shortcuts,
34	}
35}
36
37// FilterBangsInput contains parameters for filtering bang suggestions.
38type FilterBangsInput struct {
39	Query string // e.g., "!" or "!g" or "!g query"
40}
41
42// FilterBangsOutput contains filtered bang suggestions.
43type FilterBangsOutput struct {
44	Suggestions []BangSuggestion
45}
46
47// FilterBangs returns bang shortcuts matching the query prefix.
48// Query should start with "!". The prefix is extracted before any space.
49func (uc *SearchShortcutsUseCase) FilterBangs(ctx context.Context, input FilterBangsInput) *FilterBangsOutput {
50	log := logging.FromContext(ctx)
51
52	suggestions := uc.buildBangSuggestions(input.Query)
53
54	log.Debug().
55		Str("query", input.Query).
56		Int("matches", len(suggestions)).
57		Msg("filtered bang suggestions")
58
59	return &FilterBangsOutput{Suggestions: suggestions}
60}
61
62// DetectBangKeyInput contains parameters for detecting a completed bang key.
63type DetectBangKeyInput struct {
64	Query string // e.g., "!gh query"
65}
66
67// DetectBangKeyOutput contains the detected bang key if found.
68type DetectBangKeyOutput struct {
69	Key         string // The matched key (empty if not found)
70	Description string // Description of the matched shortcut
71}
72
73// DetectBangKey checks if the query contains a valid, completed bang key.
74// A completed bang key requires "!<key> " with a space after the key.
75func (uc *SearchShortcutsUseCase) DetectBangKey(ctx context.Context, input DetectBangKeyInput) *DetectBangKeyOutput {
76	log := logging.FromContext(ctx)
77
78	key := uc.detectBangKey(input.Query)
79	if key == "" {
80		return &DetectBangKeyOutput{}
81	}
82
83	shortcut, ok := uc.shortcuts[key]
84	if !ok {
85		return &DetectBangKeyOutput{}
86	}
87
88	description := shortcut.Description
89	if description == "" {
90		description = shortcut.URL
91	}
92
93	log.Debug().
94		Str("query", input.Query).
95		Str("detected_key", key).
96		Msg("detected bang key")
97
98	return &DetectBangKeyOutput{
99		Key:         key,
100		Description: description,
101	}
102}
103
104// BuildNavigationTextInput contains parameters for building navigation text.
105type BuildNavigationTextInput struct {
106	EntryText string // e.g., "!GH dumber"
107}
108
109// BuildNavigationTextOutput contains the normalized navigation text.
110type BuildNavigationTextOutput struct {
111	Text  string // Normalized text (e.g., "!gh dumber") or empty if invalid
112	Valid bool   // Whether the entry text represents a valid bang navigation
113}
114
115// BuildNavigationText normalizes a bang query for navigation.
116// Returns the normalized text with the canonical key case.
117func (uc *SearchShortcutsUseCase) BuildNavigationText(ctx context.Context, input BuildNavigationTextInput) *BuildNavigationTextOutput {
118	log := logging.FromContext(ctx)
119
120	text := uc.buildBangNavigationText(input.EntryText)
121	if text == "" {
122		return &BuildNavigationTextOutput{}
123	}
124
125	log.Debug().
126		Str("entry_text", input.EntryText).
127		Str("navigation_text", text).
128		Msg("built bang navigation text")
129
130	return &BuildNavigationTextOutput{
131		Text:  text,
132		Valid: true,
133	}
134}
135
136// GetShortcut returns a shortcut by key (case-insensitive).
137func (uc *SearchShortcutsUseCase) GetShortcut(key string) (SearchShortcut, bool) {
138	normalizedKey, ok := uc.normalizeBangKey(key)
139	if !ok {
140		return SearchShortcut{}, false
141	}
142	shortcut, ok := uc.shortcuts[normalizedKey]
143	return shortcut, ok
144}
145
146// ShortcutURLs returns a map of shortcut keys to URL templates.
147// This is useful for passing to url.BuildSearchURL.
148func (uc *SearchShortcutsUseCase) ShortcutURLs() map[string]string {
149	result := make(map[string]string, len(uc.shortcuts))
150	for key, shortcut := range uc.shortcuts {
151		result[key] = shortcut.URL
152	}
153	return result
154}
155
156// buildBangSuggestions filters and sorts shortcuts matching the query prefix.
157func (uc *SearchShortcutsUseCase) buildBangSuggestions(query string) []BangSuggestion {
158	prefix := strings.TrimPrefix(query, "!")
159	if idx := strings.Index(prefix, " "); idx >= 0 {
160		prefix = prefix[:idx]
161	}
162	prefix = strings.ToLower(prefix)
163
164	suggestions := make([]BangSuggestion, 0, len(uc.shortcuts))
165	for key, shortcut := range uc.shortcuts {
166		if !strings.HasPrefix(strings.ToLower(key), prefix) {
167			continue
168		}
169		description := shortcut.Description
170		if description == "" {
171			description = shortcut.URL
172		}
173		suggestions = append(suggestions, BangSuggestion{
174			Key:         key,
175			Description: description,
176		})
177	}
178
179	sort.Slice(suggestions, func(i, j int) bool {
180		return strings.ToLower(suggestions[i].Key) < strings.ToLower(suggestions[j].Key)
181	})
182
183	return suggestions
184}
185
186// detectBangKey returns the matched shortcut key if the query has a valid bang prefix.
187func (uc *SearchShortcutsUseCase) detectBangKey(query string) string {
188	spaceIdx := strings.Index(query, " ")
189	if !strings.HasPrefix(query, "!") || spaceIdx <= 1 {
190		return ""
191	}
192
193	candidate := query[1:spaceIdx]
194	for key := range uc.shortcuts {
195		if strings.EqualFold(key, candidate) {
196			return key
197		}
198	}
199
200	return ""
201}
202
203// normalizeBangKey returns the canonical key for a shortcut (case-insensitive lookup).
204func (uc *SearchShortcutsUseCase) normalizeBangKey(shortcutKey string) (string, bool) {
205	for key := range uc.shortcuts {
206		if strings.EqualFold(key, shortcutKey) {
207			return key, true
208		}
209	}
210	return "", false
211}
212
213// buildBangNavigationText normalizes a bang query for navigation.
214func (uc *SearchShortcutsUseCase) buildBangNavigationText(entryText string) string {
215	shortcutKey, query, found := url.ParseBangShortcut(entryText)
216	if !found || query == "" {
217		return ""
218	}
219
220	resolvedKey, ok := uc.normalizeBangKey(shortcutKey)
221	if !ok {
222		return ""
223	}
224
225	return "!" + resolvedKey + " " + query
226}

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