main 5041353

search_shortcuts_test.go RawHistoryFolder

1package usecase
2
3import (
4	"context"
5	"testing"
6)
7
8func TestFilterBangs(t *testing.T) {
9	shortcuts := map[string]SearchShortcut{
10		"ddg": {URL: "https://duckduckgo.com/?q=%s", Description: "DuckDuckGo search"},
11		"g":   {URL: "https://google.com/search?q=%s", Description: "Google search"},
12		"gh":  {URL: "https://github.com/search?q=%s", Description: "GitHub search"},
13		"n":   {URL: "https://news.ycombinator.com/", Description: ""},
14	}
15	uc := NewSearchShortcutsUseCase(shortcuts)
16	ctx := context.Background()
17
18	cases := []struct {
19		name      string
20		query     string
21		wantKeys  []string
22		wantDescr map[string]string
23	}{
24		{
25			name:     "bang only returns all sorted",
26			query:    "!",
27			wantKeys: []string{"ddg", "g", "gh", "n"},
28		},
29		{
30			name:     "filters by prefix",
31			query:    "!g",
32			wantKeys: []string{"g", "gh"},
33		},
34		{
35			name:     "filters by prefix case-insensitive",
36			query:    "!DD",
37			wantKeys: []string{"ddg"},
38		},
39		{
40			name:     "stops prefix at space",
41			query:    "!g query",
42			wantKeys: []string{"g", "gh"},
43		},
44		{
45			name:     "falls back to url when description empty",
46			query:    "!n",
47			wantKeys: []string{"n"},
48			wantDescr: map[string]string{
49				"n": "https://news.ycombinator.com/",
50			},
51		},
52	}
53
54	for _, tc := range cases {
55		t.Run(tc.name, func(t *testing.T) {
56			output := uc.FilterBangs(ctx, FilterBangsInput{Query: tc.query})
57			got := output.Suggestions
58			if len(got) != len(tc.wantKeys) {
59				t.Fatalf("len=%d want=%d", len(got), len(tc.wantKeys))
60			}
61			for i, wantKey := range tc.wantKeys {
62				if got[i].Key != wantKey {
63					t.Fatalf("idx %d key=%q want=%q", i, got[i].Key, wantKey)
64				}
65				if tc.wantDescr != nil {
66					if wantD, ok := tc.wantDescr[wantKey]; ok {
67						if got[i].Description != wantD {
68							t.Fatalf("key %q description=%q want=%q", wantKey, got[i].Description, wantD)
69						}
70					}
71				}
72			}
73		})
74	}
75}
76
77func TestDetectBangKey(t *testing.T) {
78	shortcuts := map[string]SearchShortcut{
79		"gh":  {URL: "https://github.com/search?q=%s", Description: "GitHub search"},
80		"ddg": {URL: "https://duckduckgo.com/?q=%s", Description: ""},
81	}
82	uc := NewSearchShortcutsUseCase(shortcuts)
83	ctx := context.Background()
84
85	cases := []struct {
86		name      string
87		query     string
88		wantKey   string
89		wantDescr string
90	}{
91		{name: "no bang prefix", query: "gh test", wantKey: "", wantDescr: ""},
92		{name: "bang only has no space", query: "!gh", wantKey: "", wantDescr: ""},
93		{name: "space at position 1", query: "! test", wantKey: "", wantDescr: ""},
94		{name: "unknown bang", query: "!nope test", wantKey: "", wantDescr: ""},
95		{name: "case-insensitive match", query: "!GH test", wantKey: "gh", wantDescr: "GitHub search"},
96		{name: "valid bang key", query: "!ddg test", wantKey: "ddg", wantDescr: "https://duckduckgo.com/?q=%s"},
97		{name: "description fallback to URL", query: "!DDG query", wantKey: "ddg", wantDescr: "https://duckduckgo.com/?q=%s"},
98	}
99
100	for _, tc := range cases {
101		t.Run(tc.name, func(t *testing.T) {
102			output := uc.DetectBangKey(ctx, DetectBangKeyInput{Query: tc.query})
103			if output.Key != tc.wantKey {
104				t.Fatalf("DetectBangKey(%q).Key=%q want=%q", tc.query, output.Key, tc.wantKey)
105			}
106			if output.Description != tc.wantDescr {
107				t.Fatalf("DetectBangKey(%q).Description=%q want=%q", tc.query, output.Description, tc.wantDescr)
108			}
109		})
110	}
111}
112
113func TestBuildNavigationText(t *testing.T) {
114	shortcuts := map[string]SearchShortcut{
115		"gh":  {URL: "https://github.com/search?q=%s"},
116		"ddg": {URL: "https://duckduckgo.com/?q=%s"},
117	}
118	uc := NewSearchShortcutsUseCase(shortcuts)
119	ctx := context.Background()
120
121	cases := []struct {
122		name      string
123		entryText string
124		want      string
125		wantValid bool
126	}{
127		{name: "not a bang shortcut", entryText: "example.com", want: "", wantValid: false},
128		{name: "bang key without query", entryText: "!gh ", want: "", wantValid: false},
129		{name: "unknown bang key", entryText: "!nope test", want: "", wantValid: false},
130		{name: "normalizes key case", entryText: "!GH dumber", want: "!gh dumber", wantValid: true},
131		{name: "keeps query unchanged", entryText: "!ddg some query", want: "!ddg some query", wantValid: true},
132	}
133
134	for _, tc := range cases {
135		t.Run(tc.name, func(t *testing.T) {
136			output := uc.BuildNavigationText(ctx, BuildNavigationTextInput{EntryText: tc.entryText})
137			if output.Text != tc.want {
138				t.Fatalf("BuildNavigationText(%q).Text=%q want=%q", tc.entryText, output.Text, tc.want)
139			}
140			if output.Valid != tc.wantValid {
141				t.Fatalf("BuildNavigationText(%q).Valid=%v want=%v", tc.entryText, output.Valid, tc.wantValid)
142			}
143		})
144	}
145}
146
147func TestGetShortcut(t *testing.T) {
148	shortcuts := map[string]SearchShortcut{
149		"gh": {URL: "https://github.com/search?q=%s", Description: "GitHub"},
150	}
151	uc := NewSearchShortcutsUseCase(shortcuts)
152
153	cases := []struct {
154		name    string
155		key     string
156		wantOK  bool
157		wantURL string
158	}{
159		{name: "exact match", key: "gh", wantOK: true, wantURL: "https://github.com/search?q=%s"},
160		{name: "case insensitive", key: "GH", wantOK: true, wantURL: "https://github.com/search?q=%s"},
161		{name: "not found", key: "unknown", wantOK: false, wantURL: ""},
162	}
163
164	for _, tc := range cases {
165		t.Run(tc.name, func(t *testing.T) {
166			shortcut, ok := uc.GetShortcut(tc.key)
167			if ok != tc.wantOK {
168				t.Fatalf("GetShortcut(%q) ok=%v want=%v", tc.key, ok, tc.wantOK)
169			}
170			if shortcut.URL != tc.wantURL {
171				t.Fatalf("GetShortcut(%q).URL=%q want=%q", tc.key, shortcut.URL, tc.wantURL)
172			}
173		})
174	}
175}

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