detached 36bf68a

fix(omnibox): cap rendered rows to navigable results and scroll only on keyboard selection

commit
36bf68a301736c1993ed5bbbe942607640520d76
author
brice · · 2026-10-10 01:06 UTC
parent
c89d890
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +98 -65
M internal/ui/component/modal_sizing_test.go +2 -3
@@ -178,9 +178,8 @@ func TestOmniboxValign(t *testing.T) {
178178 assert.Equal(t, gtk.AlignStartValue, omniboxValign(ModalSizeConfig{UseFixedTopMargin: true}))
179179 }
180180
181-func TestOmniboxListDefaults_ShowFiveScrollTen(t *testing.T) {
182- assert.Equal(t, 5, OmniboxListDefaults.MaxVisibleRows)
183- assert.Equal(t, 10, OmniboxListDefaults.MaxResults)
181+func TestOmniboxListDefaults_ScrollableResults(t *testing.T) {
182+ assert.Less(t, OmniboxListDefaults.MaxVisibleRows, OmniboxListDefaults.MaxResults)
184183 assert.Less(t, OmniboxListDefaults.SmallMaxVisibleRows, OmniboxListDefaults.MaxVisibleRows)
185184 }
186185
@@ -178,9 +178,8 @@ func TestOmniboxValign(t *testing.T) {
178 assert.Equal(t, gtk.AlignStartValue, omniboxValign(ModalSizeConfig{UseFixedTopMargin: true}))178 assert.Equal(t, gtk.AlignStartValue, omniboxValign(ModalSizeConfig{UseFixedTopMargin: true}))
179 }179 }
180 180
181-func TestOmniboxListDefaults_ShowFiveScrollTen(t *testing.T) {181+func TestOmniboxListDefaults_ScrollableResults(t *testing.T) {
182- assert.Equal(t, 5, OmniboxListDefaults.MaxVisibleRows)182+ assert.Less(t, OmniboxListDefaults.MaxVisibleRows, OmniboxListDefaults.MaxResults)
183- assert.Equal(t, 10, OmniboxListDefaults.MaxResults)
184 assert.Less(t, OmniboxListDefaults.SmallMaxVisibleRows, OmniboxListDefaults.MaxVisibleRows)183 assert.Less(t, OmniboxListDefaults.SmallMaxVisibleRows, OmniboxListDefaults.MaxVisibleRows)
185 }184 }
186 185
M internal/ui/component/omnibox.go +31 -34
@@ -1512,7 +1512,7 @@ func (o *Omnibox) restoreEntryToRealInput() {
15121512 // updateGhostFromSelectionWithInput updates ghost text based on selected row and input.
15131513 func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) {
15141514 log := logging.FromContext(o.ctx)
1515- maxVisible := OmniboxListDefaults.MaxResults
1515+ maxResults := OmniboxListDefaults.MaxResults
15161516 o.mu.RLock()
15171517 idx := o.selectedIndex
15181518 mode := o.viewMode
@@ -1540,10 +1540,10 @@ func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) {
15401540 return
15411541 }
15421542
1543- targetURL, hasExplicitSelection := selectedTargetURL(mode, idx, maxVisible, suggestions, favorites)
1543+ targetURL, hasExplicitSelection := selectedTargetURL(mode, idx, maxResults, suggestions, favorites)
15441544
15451545 if entryText != "" {
1546- o.resolveGhostCompletion(entryText, targetURL, hasExplicitSelection, mode, maxVisible, suggestions, favorites)
1546+ o.resolveGhostCompletion(entryText, targetURL, hasExplicitSelection, mode, maxResults, suggestions, favorites)
15471547 return
15481548 }
15491549
@@ -1565,7 +1565,7 @@ func (o *Omnibox) resolveGhostCompletion(
15651565 entryText, selectedURL string,
15661566 hasExplicitSelection bool,
15671567 mode ViewMode,
1568- maxVisible int,
1568+ maxResults int,
15691569 suggestions []Suggestion,
15701570 favorites []Favorite,
15711571 ) {
@@ -1575,7 +1575,7 @@ func (o *Omnibox) resolveGhostCompletion(
15751575 SelectedURL: selectedURL,
15761576 HasExplicitSelection: hasExplicitSelection,
15771577 Mode: mode,
1578- MaxVisible: maxVisible,
1578+ MaxResults: maxResults,
15791579 Suggestions: suggestions,
15801580 Favorites: favorites,
15811581 })
@@ -1987,6 +1987,11 @@ func (o *Omnibox) rebuildList() {
19871987 bangSuggestions := o.bangSuggestions
19881988 o.mu.RUnlock()
19891989
1990+ limit := OmniboxListDefaults.MaxResults
1991+ bangSuggestions = bangSuggestions[:limitedCount(len(bangSuggestions), limit)]
1992+ suggestions = suggestions[:limitedCount(len(suggestions), limit)]
1993+ favorites = favorites[:limitedCount(len(favorites), limit)]
1994+
19901995 if bangMode {
19911996 for i, b := range bangSuggestions {
19921997 row := o.createBangRow(b, i)
@@ -2267,17 +2272,22 @@ func (o *Omnibox) selectIndex(index int) {
22672272 row := o.listBox.GetRowAtIndex(index)
22682273 if row != nil {
22692274 o.listBox.SelectRow(row)
2270- o.scrollRowIntoView(row)
22712275 return
22722276 }
22732277
22742278 o.updateGhostFromSelection()
22752279 }
22762280
2277-// scrollRowIntoView scrolls the results list so the row is fully visible.
2278-// The entry keeps keyboard focus, so GTK does not scroll the list itself.
2279-func (o *Omnibox) scrollRowIntoView(row *gtk.ListBoxRow) {
2280- if o.scrolledWin == nil || row == nil {
2281+// scrollSelectionIntoView scrolls the results list so the row at index is
2282+// fully visible. The entry keeps keyboard focus, so GTK does not scroll the
2283+// list itself. Only keyboard navigation calls this: hover must not move rows
2284+// under the pointer.
2285+func (o *Omnibox) scrollSelectionIntoView(index int) {
2286+ if o.scrolledWin == nil || o.listBox == nil {
2287+ return
2288+ }
2289+ row := o.listBox.GetRowAtIndex(index)
2290+ if row == nil {
22812291 return
22822292 }
22832293 adj := o.scrolledWin.GetVadjustment()
@@ -2297,17 +2307,8 @@ func (o *Omnibox) scrollRowIntoView(row *gtk.ListBoxRow) {
22972307 func (o *Omnibox) selectNext() {
22982308 o.mu.Lock()
22992309 current := o.selectedIndex
2300- mode := o.viewMode
2301- bangMode := o.bangMode
2302- maxVisible := OmniboxListDefaults.MaxResults
2303- var maxIndex int
2304- if bangMode {
2305- maxIndex = visibleResultCount(len(o.bangSuggestions), maxVisible) - 1
2306- } else if mode == ViewModeHistory {
2307- maxIndex = visibleResultCount(len(o.suggestions), maxVisible) - 1
2308- } else {
2309- maxIndex = visibleResultCount(len(o.favorites), maxVisible) - 1
2310- }
2310+ maxIndex := navigableCount(o.bangMode, o.viewMode,
2311+ len(o.bangSuggestions), len(o.suggestions), len(o.favorites)) - 1
23112312 o.hasNavigated = true // User is navigating with arrow keys
23122313 o.mu.Unlock()
23132314
@@ -2320,23 +2321,15 @@ func (o *Omnibox) selectNext() {
23202321 newIndex = 0 // Wrap around
23212322 }
23222323 o.selectIndex(newIndex)
2324+ o.scrollSelectionIntoView(newIndex)
23232325 }
23242326
23252327 // selectPrevious moves selection up.
23262328 func (o *Omnibox) selectPrevious() {
23272329 o.mu.Lock()
23282330 current := o.selectedIndex
2329- mode := o.viewMode
2330- bangMode := o.bangMode
2331- maxVisible := OmniboxListDefaults.MaxResults
2332- var maxIndex int
2333- if bangMode {
2334- maxIndex = visibleResultCount(len(o.bangSuggestions), maxVisible) - 1
2335- } else if mode == ViewModeHistory {
2336- maxIndex = visibleResultCount(len(o.suggestions), maxVisible) - 1
2337- } else {
2338- maxIndex = visibleResultCount(len(o.favorites), maxVisible) - 1
2339- }
2331+ maxIndex := navigableCount(o.bangMode, o.viewMode,
2332+ len(o.bangSuggestions), len(o.suggestions), len(o.favorites)) - 1
23402333 o.hasNavigated = true // User is navigating with arrow keys
23412334 o.mu.Unlock()
23422335
@@ -2349,6 +2342,7 @@ func (o *Omnibox) selectPrevious() {
23492342 newIndex = maxIndex // Wrap around
23502343 }
23512344 o.selectIndex(newIndex)
2345+ o.scrollSelectionIntoView(newIndex)
23522346 }
23532347
23542348 // selectAndNavigate selects an index and navigates to it.
@@ -2361,14 +2355,14 @@ func (o *Omnibox) selectAndNavigate(index int) {
23612355 suggestions := o.suggestions
23622356 favorites := o.favorites
23632357 o.mu.RUnlock()
2364- maxVisible := OmniboxListDefaults.MaxResults
2358+ maxResults := OmniboxListDefaults.MaxResults
23652359
23662360 if bangMode {
23672361 o.navigateToSelected()
23682362 return
23692363 }
23702364
2371- targetURL := resolveTargetURLForSelection(mode, index, maxVisible, suggestions, favorites)
2365+ targetURL := resolveTargetURLForSelection(mode, index, maxResults, suggestions, favorites)
23722366 if targetURL == "" {
23732367 return
23742368 }
@@ -2833,6 +2827,9 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
28332827 Msg("omnibox geometry decision")
28342828
28352829 o.mainBox.SetSizeRequest(width, -1)
2830+ if omniboxValign(o.sizeCfg) == gtk.AlignCenterValue {
2831+ marginTop = 0 // centered by GTK; a margin would push it off-center
2832+ }
28362833 o.outerBox.SetMarginTop(marginTop)
28372834
28382835 // Show the omnibox
@@ -1512,7 +1512,7 @@ func (o *Omnibox) restoreEntryToRealInput() {
1512 // updateGhostFromSelectionWithInput updates ghost text based on selected row and input.1512 // updateGhostFromSelectionWithInput updates ghost text based on selected row and input.
1513 func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) {1513 func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) {
1514 log := logging.FromContext(o.ctx)1514 log := logging.FromContext(o.ctx)
1515- maxVisible := OmniboxListDefaults.MaxResults1515+ maxResults := OmniboxListDefaults.MaxResults
1516 o.mu.RLock()1516 o.mu.RLock()
1517 idx := o.selectedIndex1517 idx := o.selectedIndex
1518 mode := o.viewMode1518 mode := o.viewMode
@@ -1540,10 +1540,10 @@ func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) {
1540 return1540 return
1541 }1541 }
1542 1542
1543- targetURL, hasExplicitSelection := selectedTargetURL(mode, idx, maxVisible, suggestions, favorites)1543+ targetURL, hasExplicitSelection := selectedTargetURL(mode, idx, maxResults, suggestions, favorites)
1544 1544
1545 if entryText != "" {1545 if entryText != "" {
1546- o.resolveGhostCompletion(entryText, targetURL, hasExplicitSelection, mode, maxVisible, suggestions, favorites)1546+ o.resolveGhostCompletion(entryText, targetURL, hasExplicitSelection, mode, maxResults, suggestions, favorites)
1547 return1547 return
1548 }1548 }
1549 1549
@@ -1565,7 +1565,7 @@ func (o *Omnibox) resolveGhostCompletion(
1565 entryText, selectedURL string,1565 entryText, selectedURL string,
1566 hasExplicitSelection bool,1566 hasExplicitSelection bool,
1567 mode ViewMode,1567 mode ViewMode,
1568- maxVisible int,1568+ maxResults int,
1569 suggestions []Suggestion,1569 suggestions []Suggestion,
1570 favorites []Favorite,1570 favorites []Favorite,
1571 ) {1571 ) {
@@ -1575,7 +1575,7 @@ func (o *Omnibox) resolveGhostCompletion(
1575 SelectedURL: selectedURL,1575 SelectedURL: selectedURL,
1576 HasExplicitSelection: hasExplicitSelection,1576 HasExplicitSelection: hasExplicitSelection,
1577 Mode: mode,1577 Mode: mode,
1578- MaxVisible: maxVisible,1578+ MaxResults: maxResults,
1579 Suggestions: suggestions,1579 Suggestions: suggestions,
1580 Favorites: favorites,1580 Favorites: favorites,
1581 })1581 })
@@ -1987,6 +1987,11 @@ func (o *Omnibox) rebuildList() {
1987 bangSuggestions := o.bangSuggestions1987 bangSuggestions := o.bangSuggestions
1988 o.mu.RUnlock()1988 o.mu.RUnlock()
1989 1989
1990+ limit := OmniboxListDefaults.MaxResults
1991+ bangSuggestions = bangSuggestions[:limitedCount(len(bangSuggestions), limit)]
1992+ suggestions = suggestions[:limitedCount(len(suggestions), limit)]
1993+ favorites = favorites[:limitedCount(len(favorites), limit)]
1994+
1990 if bangMode {1995 if bangMode {
1991 for i, b := range bangSuggestions {1996 for i, b := range bangSuggestions {
1992 row := o.createBangRow(b, i)1997 row := o.createBangRow(b, i)
@@ -2267,17 +2272,22 @@ func (o *Omnibox) selectIndex(index int) {
2267 row := o.listBox.GetRowAtIndex(index)2272 row := o.listBox.GetRowAtIndex(index)
2268 if row != nil {2273 if row != nil {
2269 o.listBox.SelectRow(row)2274 o.listBox.SelectRow(row)
2270- o.scrollRowIntoView(row)
2271 return2275 return
2272 }2276 }
2273 2277
2274 o.updateGhostFromSelection()2278 o.updateGhostFromSelection()
2275 }2279 }
2276 2280
2277-// scrollRowIntoView scrolls the results list so the row is fully visible.2281+// scrollSelectionIntoView scrolls the results list so the row at index is
2278-// The entry keeps keyboard focus, so GTK does not scroll the list itself.2282+// fully visible. The entry keeps keyboard focus, so GTK does not scroll the
2279-func (o *Omnibox) scrollRowIntoView(row *gtk.ListBoxRow) {2283+// list itself. Only keyboard navigation calls this: hover must not move rows
2280- if o.scrolledWin == nil || row == nil {2284+// under the pointer.
2285+func (o *Omnibox) scrollSelectionIntoView(index int) {
2286+ if o.scrolledWin == nil || o.listBox == nil {
2287+ return
2288+ }
2289+ row := o.listBox.GetRowAtIndex(index)
2290+ if row == nil {
2281 return2291 return
2282 }2292 }
2283 adj := o.scrolledWin.GetVadjustment()2293 adj := o.scrolledWin.GetVadjustment()
@@ -2297,17 +2307,8 @@ func (o *Omnibox) scrollRowIntoView(row *gtk.ListBoxRow) {
2297 func (o *Omnibox) selectNext() {2307 func (o *Omnibox) selectNext() {
2298 o.mu.Lock()2308 o.mu.Lock()
2299 current := o.selectedIndex2309 current := o.selectedIndex
2300- mode := o.viewMode2310+ maxIndex := navigableCount(o.bangMode, o.viewMode,
2301- bangMode := o.bangMode2311+ len(o.bangSuggestions), len(o.suggestions), len(o.favorites)) - 1
2302- maxVisible := OmniboxListDefaults.MaxResults
2303- var maxIndex int
2304- if bangMode {
2305- maxIndex = visibleResultCount(len(o.bangSuggestions), maxVisible) - 1
2306- } else if mode == ViewModeHistory {
2307- maxIndex = visibleResultCount(len(o.suggestions), maxVisible) - 1
2308- } else {
2309- maxIndex = visibleResultCount(len(o.favorites), maxVisible) - 1
2310- }
2311 o.hasNavigated = true // User is navigating with arrow keys2312 o.hasNavigated = true // User is navigating with arrow keys
2312 o.mu.Unlock()2313 o.mu.Unlock()
2313 2314
@@ -2320,23 +2321,15 @@ func (o *Omnibox) selectNext() {
2320 newIndex = 0 // Wrap around2321 newIndex = 0 // Wrap around
2321 }2322 }
2322 o.selectIndex(newIndex)2323 o.selectIndex(newIndex)
2324+ o.scrollSelectionIntoView(newIndex)
2323 }2325 }
2324 2326
2325 // selectPrevious moves selection up.2327 // selectPrevious moves selection up.
2326 func (o *Omnibox) selectPrevious() {2328 func (o *Omnibox) selectPrevious() {
2327 o.mu.Lock()2329 o.mu.Lock()
2328 current := o.selectedIndex2330 current := o.selectedIndex
2329- mode := o.viewMode2331+ maxIndex := navigableCount(o.bangMode, o.viewMode,
2330- bangMode := o.bangMode2332+ len(o.bangSuggestions), len(o.suggestions), len(o.favorites)) - 1
2331- maxVisible := OmniboxListDefaults.MaxResults
2332- var maxIndex int
2333- if bangMode {
2334- maxIndex = visibleResultCount(len(o.bangSuggestions), maxVisible) - 1
2335- } else if mode == ViewModeHistory {
2336- maxIndex = visibleResultCount(len(o.suggestions), maxVisible) - 1
2337- } else {
2338- maxIndex = visibleResultCount(len(o.favorites), maxVisible) - 1
2339- }
2340 o.hasNavigated = true // User is navigating with arrow keys2333 o.hasNavigated = true // User is navigating with arrow keys
2341 o.mu.Unlock()2334 o.mu.Unlock()
2342 2335
@@ -2349,6 +2342,7 @@ func (o *Omnibox) selectPrevious() {
2349 newIndex = maxIndex // Wrap around2342 newIndex = maxIndex // Wrap around
2350 }2343 }
2351 o.selectIndex(newIndex)2344 o.selectIndex(newIndex)
2345+ o.scrollSelectionIntoView(newIndex)
2352 }2346 }
2353 2347
2354 // selectAndNavigate selects an index and navigates to it.2348 // selectAndNavigate selects an index and navigates to it.
@@ -2361,14 +2355,14 @@ func (o *Omnibox) selectAndNavigate(index int) {
2361 suggestions := o.suggestions2355 suggestions := o.suggestions
2362 favorites := o.favorites2356 favorites := o.favorites
2363 o.mu.RUnlock()2357 o.mu.RUnlock()
2364- maxVisible := OmniboxListDefaults.MaxResults2358+ maxResults := OmniboxListDefaults.MaxResults
2365 2359
2366 if bangMode {2360 if bangMode {
2367 o.navigateToSelected()2361 o.navigateToSelected()
2368 return2362 return
2369 }2363 }
2370 2364
2371- targetURL := resolveTargetURLForSelection(mode, index, maxVisible, suggestions, favorites)2365+ targetURL := resolveTargetURLForSelection(mode, index, maxResults, suggestions, favorites)
2372 if targetURL == "" {2366 if targetURL == "" {
2373 return2367 return
2374 }2368 }
@@ -2833,6 +2827,9 @@ func (o *Omnibox) Show(ctx context.Context, query string) {
2833 Msg("omnibox geometry decision")2827 Msg("omnibox geometry decision")
2834 2828
2835 o.mainBox.SetSizeRequest(width, -1)2829 o.mainBox.SetSizeRequest(width, -1)
2830+ if omniboxValign(o.sizeCfg) == gtk.AlignCenterValue {
2831+ marginTop = 0 // centered by GTK; a margin would push it off-center
2832+ }
2836 o.outerBox.SetMarginTop(marginTop)2833 o.outerBox.SetMarginTop(marginTop)
2837 2834
2838 // Show the omnibox2835 // Show the omnibox
M internal/ui/component/omnibox_suggestions.go +35 -22
@@ -19,7 +19,7 @@ type ghostCompletionRequest struct {
1919 SelectedURL string
2020 HasExplicitSelection bool
2121 Mode ViewMode
22- MaxVisible int
22+ MaxResults int
2323 Suggestions []Suggestion
2424 Favorites []Favorite
2525 }
@@ -34,7 +34,7 @@ func ghostCompletion(req ghostCompletionRequest) (fullText, suffix string, ok bo
3434 return "", "", false
3535 }
3636 fullText, suffix, ok = visibleGhostSuggestion(
37- input, req.SelectedURL, req.HasExplicitSelection, req.Mode, req.MaxVisible, req.Suggestions, req.Favorites,
37+ input, req.SelectedURL, req.HasExplicitSelection, req.Mode, req.MaxResults, req.Suggestions, req.Favorites,
3838 )
3939 if !ok {
4040 return "", "", false
@@ -122,7 +122,7 @@ func visibleGhostSuggestion(
122122 query, selectedURL string,
123123 hasExplicitSelection bool,
124124 mode ViewMode,
125- maxVisible int,
125+ maxResults int,
126126 suggestions []Suggestion,
127127 favorites []Favorite,
128128 ) (fullText, suffix string, ok bool) {
@@ -131,16 +131,16 @@ func visibleGhostSuggestion(
131131 return fullText, suffix, ok
132132 }
133133
134- visibleURLs := visibleURLsForMode(mode, maxVisible, suggestions, favorites)
135- suffix, fullText, ok = autocomplete.BestURLCompletion(query, visibleURLs)
134+ listedURLs := listedURLsForMode(mode, maxResults, suggestions, favorites)
135+ suffix, fullText, ok = autocomplete.BestURLCompletion(query, listedURLs)
136136 return fullText, suffix, ok
137137 }
138138
139-func visibleURLsForMode(mode ViewMode, maxVisible int, suggestions []Suggestion, favorites []Favorite) []string {
139+func listedURLsForMode(mode ViewMode, maxResults int, suggestions []Suggestion, favorites []Favorite) []string {
140140 if mode == ViewModeHistory {
141- visibleCount := visibleResultCount(len(suggestions), maxVisible)
142- urls := make([]string, 0, visibleCount)
143- for _, s := range suggestions[:visibleCount] {
141+ count := limitedCount(len(suggestions), maxResults)
142+ urls := make([]string, 0, count)
143+ for _, s := range suggestions[:count] {
144144 if s.URL != "" {
145145 urls = append(urls, s.URL)
146146 }
@@ -148,9 +148,9 @@ func visibleURLsForMode(mode ViewMode, maxVisible int, suggestions []Suggestion,
148148 return urls
149149 }
150150
151- visibleCount := visibleResultCount(len(favorites), maxVisible)
152- urls := make([]string, 0, visibleCount)
153- for _, f := range favorites[:visibleCount] {
151+ count := limitedCount(len(favorites), maxResults)
152+ urls := make([]string, 0, count)
153+ for _, f := range favorites[:count] {
154154 if f.URL != "" {
155155 urls = append(urls, f.URL)
156156 }
@@ -158,11 +158,11 @@ func visibleURLsForMode(mode ViewMode, maxVisible int, suggestions []Suggestion,
158158 return urls
159159 }
160160
161-func selectedTargetURL(mode ViewMode, idx, maxVisible int, suggestions []Suggestion, favorites []Favorite) (string, bool) {
161+func selectedTargetURL(mode ViewMode, idx, maxResults int, suggestions []Suggestion, favorites []Favorite) (string, bool) {
162162 if idx < 0 {
163163 return "", false
164164 }
165- return resolveTargetURLForSelection(mode, idx, maxVisible, suggestions, favorites), true
165+ return resolveTargetURLForSelection(mode, idx, maxResults, suggestions, favorites), true
166166 }
167167
168168 func bangSuggestionTextAt(idx int, bangSuggestions []BangSuggestion) (string, bool) {
@@ -172,14 +172,27 @@ func bangSuggestionTextAt(idx int, bangSuggestions []BangSuggestion) (string, bo
172172 return "!" + bangSuggestions[idx].Key + " ", true
173173 }
174174
175-func visibleResultCount(total, maxVisible int) int {
175+func limitedCount(total, maxResults int) int {
176176 if total <= 0 {
177177 return 0
178178 }
179- if maxVisible <= 0 || total < maxVisible {
179+ if maxResults <= 0 || total < maxResults {
180180 return total
181181 }
182- return maxVisible
182+ return maxResults
183+}
184+
185+// navigableCount returns how many rows of the active list are rendered and
186+// reachable by keyboard: the active list length capped at MaxResults.
187+func navigableCount(bangMode bool, mode ViewMode, bangs, suggestions, favorites int) int {
188+ total := favorites
189+ switch {
190+ case bangMode:
191+ total = bangs
192+ case mode == ViewModeHistory:
193+ total = suggestions
194+ }
195+ return limitedCount(total, OmniboxListDefaults.MaxResults)
183196 }
184197
185198 func effectiveSearchQuery(entryText, realInput string, hasGhost bool) string {
@@ -189,16 +202,16 @@ func effectiveSearchQuery(entryText, realInput string, hasGhost bool) string {
189202 return entryText
190203 }
191204
192-func resolveTargetURLForSelection(mode ViewMode, idx, maxVisible int, suggestions []Suggestion, favorites []Favorite) string {
205+func resolveTargetURLForSelection(mode ViewMode, idx, maxResults int, suggestions []Suggestion, favorites []Favorite) string {
193206 if mode == ViewModeHistory {
194- visibleCount := visibleResultCount(len(suggestions), maxVisible)
195- if idx >= 0 && idx < visibleCount {
207+ count := limitedCount(len(suggestions), maxResults)
208+ if idx >= 0 && idx < count {
196209 return suggestions[idx].URL
197210 }
198211 return ""
199212 }
200- visibleCount := visibleResultCount(len(favorites), maxVisible)
201- if idx >= 0 && idx < visibleCount {
213+ count := limitedCount(len(favorites), maxResults)
214+ if idx >= 0 && idx < count {
202215 return favorites[idx].URL
203216 }
204217 return ""
@@ -19,7 +19,7 @@ type ghostCompletionRequest struct {
19 SelectedURL string19 SelectedURL string
20 HasExplicitSelection bool20 HasExplicitSelection bool
21 Mode ViewMode21 Mode ViewMode
22- MaxVisible int22+ MaxResults int
23 Suggestions []Suggestion23 Suggestions []Suggestion
24 Favorites []Favorite24 Favorites []Favorite
25 }25 }
@@ -34,7 +34,7 @@ func ghostCompletion(req ghostCompletionRequest) (fullText, suffix string, ok bo
34 return "", "", false34 return "", "", false
35 }35 }
36 fullText, suffix, ok = visibleGhostSuggestion(36 fullText, suffix, ok = visibleGhostSuggestion(
37- input, req.SelectedURL, req.HasExplicitSelection, req.Mode, req.MaxVisible, req.Suggestions, req.Favorites,37+ input, req.SelectedURL, req.HasExplicitSelection, req.Mode, req.MaxResults, req.Suggestions, req.Favorites,
38 )38 )
39 if !ok {39 if !ok {
40 return "", "", false40 return "", "", false
@@ -122,7 +122,7 @@ func visibleGhostSuggestion(
122 query, selectedURL string,122 query, selectedURL string,
123 hasExplicitSelection bool,123 hasExplicitSelection bool,
124 mode ViewMode,124 mode ViewMode,
125- maxVisible int,125+ maxResults int,
126 suggestions []Suggestion,126 suggestions []Suggestion,
127 favorites []Favorite,127 favorites []Favorite,
128 ) (fullText, suffix string, ok bool) {128 ) (fullText, suffix string, ok bool) {
@@ -131,16 +131,16 @@ func visibleGhostSuggestion(
131 return fullText, suffix, ok131 return fullText, suffix, ok
132 }132 }
133 133
134- visibleURLs := visibleURLsForMode(mode, maxVisible, suggestions, favorites)134+ listedURLs := listedURLsForMode(mode, maxResults, suggestions, favorites)
135- suffix, fullText, ok = autocomplete.BestURLCompletion(query, visibleURLs)135+ suffix, fullText, ok = autocomplete.BestURLCompletion(query, listedURLs)
136 return fullText, suffix, ok136 return fullText, suffix, ok
137 }137 }
138 138
139-func visibleURLsForMode(mode ViewMode, maxVisible int, suggestions []Suggestion, favorites []Favorite) []string {139+func listedURLsForMode(mode ViewMode, maxResults int, suggestions []Suggestion, favorites []Favorite) []string {
140 if mode == ViewModeHistory {140 if mode == ViewModeHistory {
141- visibleCount := visibleResultCount(len(suggestions), maxVisible)141+ count := limitedCount(len(suggestions), maxResults)
142- urls := make([]string, 0, visibleCount)142+ urls := make([]string, 0, count)
143- for _, s := range suggestions[:visibleCount] {143+ for _, s := range suggestions[:count] {
144 if s.URL != "" {144 if s.URL != "" {
145 urls = append(urls, s.URL)145 urls = append(urls, s.URL)
146 }146 }
@@ -148,9 +148,9 @@ func visibleURLsForMode(mode ViewMode, maxVisible int, suggestions []Suggestion,
148 return urls148 return urls
149 }149 }
150 150
151- visibleCount := visibleResultCount(len(favorites), maxVisible)151+ count := limitedCount(len(favorites), maxResults)
152- urls := make([]string, 0, visibleCount)152+ urls := make([]string, 0, count)
153- for _, f := range favorites[:visibleCount] {153+ for _, f := range favorites[:count] {
154 if f.URL != "" {154 if f.URL != "" {
155 urls = append(urls, f.URL)155 urls = append(urls, f.URL)
156 }156 }
@@ -158,11 +158,11 @@ func visibleURLsForMode(mode ViewMode, maxVisible int, suggestions []Suggestion,
158 return urls158 return urls
159 }159 }
160 160
161-func selectedTargetURL(mode ViewMode, idx, maxVisible int, suggestions []Suggestion, favorites []Favorite) (string, bool) {161+func selectedTargetURL(mode ViewMode, idx, maxResults int, suggestions []Suggestion, favorites []Favorite) (string, bool) {
162 if idx < 0 {162 if idx < 0 {
163 return "", false163 return "", false
164 }164 }
165- return resolveTargetURLForSelection(mode, idx, maxVisible, suggestions, favorites), true165+ return resolveTargetURLForSelection(mode, idx, maxResults, suggestions, favorites), true
166 }166 }
167 167
168 func bangSuggestionTextAt(idx int, bangSuggestions []BangSuggestion) (string, bool) {168 func bangSuggestionTextAt(idx int, bangSuggestions []BangSuggestion) (string, bool) {
@@ -172,14 +172,27 @@ func bangSuggestionTextAt(idx int, bangSuggestions []BangSuggestion) (string, bo
172 return "!" + bangSuggestions[idx].Key + " ", true172 return "!" + bangSuggestions[idx].Key + " ", true
173 }173 }
174 174
175-func visibleResultCount(total, maxVisible int) int {175+func limitedCount(total, maxResults int) int {
176 if total <= 0 {176 if total <= 0 {
177 return 0177 return 0
178 }178 }
179- if maxVisible <= 0 || total < maxVisible {179+ if maxResults <= 0 || total < maxResults {
180 return total180 return total
181 }181 }
182- return maxVisible182+ return maxResults
183+}
184+
185+// navigableCount returns how many rows of the active list are rendered and
186+// reachable by keyboard: the active list length capped at MaxResults.
187+func navigableCount(bangMode bool, mode ViewMode, bangs, suggestions, favorites int) int {
188+ total := favorites
189+ switch {
190+ case bangMode:
191+ total = bangs
192+ case mode == ViewModeHistory:
193+ total = suggestions
194+ }
195+ return limitedCount(total, OmniboxListDefaults.MaxResults)
183 }196 }
184 197
185 func effectiveSearchQuery(entryText, realInput string, hasGhost bool) string {198 func effectiveSearchQuery(entryText, realInput string, hasGhost bool) string {
@@ -189,16 +202,16 @@ func effectiveSearchQuery(entryText, realInput string, hasGhost bool) string {
189 return entryText202 return entryText
190 }203 }
191 204
192-func resolveTargetURLForSelection(mode ViewMode, idx, maxVisible int, suggestions []Suggestion, favorites []Favorite) string {205+func resolveTargetURLForSelection(mode ViewMode, idx, maxResults int, suggestions []Suggestion, favorites []Favorite) string {
193 if mode == ViewModeHistory {206 if mode == ViewModeHistory {
194- visibleCount := visibleResultCount(len(suggestions), maxVisible)207+ count := limitedCount(len(suggestions), maxResults)
195- if idx >= 0 && idx < visibleCount {208+ if idx >= 0 && idx < count {
196 return suggestions[idx].URL209 return suggestions[idx].URL
197 }210 }
198 return ""211 return ""
199 }212 }
200- visibleCount := visibleResultCount(len(favorites), maxVisible)213+ count := limitedCount(len(favorites), maxResults)
201- if idx >= 0 && idx < visibleCount {214+ if idx >= 0 && idx < count {
202 return favorites[idx].URL215 return favorites[idx].URL
203 }216 }
204 return ""217 return ""
M internal/ui/component/omnibox_suggestions_test.go +30 -6
@@ -18,26 +18,26 @@ func TestGhostCompletion(t *testing.T) {
1818 }{
1919 {
2020 name: "host-like input completes to the domain, not the redirect URL",
21- req: ghostCompletionRequest{EntryText: "goo", Mode: ViewModeHistory, MaxVisible: 5, Suggestions: history},
21+ req: ghostCompletionRequest{EntryText: "goo", Mode: ViewModeHistory, MaxResults: 5, Suggestions: history},
2222 wantFull: "google.com",
2323 wantSuffix: "gle.com",
2424 wantOK: true,
2525 },
2626 {
2727 name: "leading whitespace is ignored",
28- req: ghostCompletionRequest{EntryText: " gith", Mode: ViewModeHistory, MaxVisible: 5, Suggestions: history},
28+ req: ghostCompletionRequest{EntryText: " gith", Mode: ViewModeHistory, MaxResults: 5, Suggestions: history},
2929 wantFull: "github.com",
3030 wantSuffix: "ub.com",
3131 wantOK: true,
3232 },
3333 {
3434 name: "hidden rows never complete",
35- req: ghostCompletionRequest{EntryText: "gith", Mode: ViewModeHistory, MaxVisible: 1, Suggestions: history},
35+ req: ghostCompletionRequest{EntryText: "gith", Mode: ViewModeHistory, MaxResults: 1, Suggestions: history},
3636 wantOK: false,
3737 },
3838 {
3939 name: "favorites mode uses favorites",
40- req: ghostCompletionRequest{EntryText: "gitl", Mode: ViewModeFavorites, MaxVisible: 5, Favorites: favorites},
40+ req: ghostCompletionRequest{EntryText: "gitl", Mode: ViewModeFavorites, MaxResults: 5, Favorites: favorites},
4141 wantFull: "gitlab.com",
4242 wantSuffix: "ab.com",
4343 wantOK: true,
@@ -46,7 +46,7 @@ func TestGhostCompletion(t *testing.T) {
4646 name: "explicit selection completes from the selected URL, not the best match",
4747 req: ghostCompletionRequest{
4848 EntryText: "gi", SelectedURL: "https://gitlab.com/", HasExplicitSelection: true,
49- Mode: ViewModeHistory, MaxVisible: 5, Suggestions: history,
49+ Mode: ViewModeHistory, MaxResults: 5, Suggestions: history,
5050 },
5151 wantFull: "gitlab.com",
5252 wantSuffix: "tlab.com",
@@ -54,7 +54,7 @@ func TestGhostCompletion(t *testing.T) {
5454 },
5555 {
5656 name: "blank input shows nothing",
57- req: ghostCompletionRequest{EntryText: " ", Mode: ViewModeHistory, MaxVisible: 5, Suggestions: history},
57+ req: ghostCompletionRequest{EntryText: " ", Mode: ViewModeHistory, MaxResults: 5, Suggestions: history},
5858 wantOK: false,
5959 },
6060 }
@@ -69,3 +69,27 @@ func TestGhostCompletion(t *testing.T) {
6969 })
7070 }
7171 }
72+
73+func TestNavigableCount(t *testing.T) {
74+ limit := OmniboxListDefaults.MaxResults
75+ tests := []struct {
76+ name string
77+ bangMode bool
78+ mode ViewMode
79+ bangs, suggestions, favorites int
80+ want int
81+ }{
82+ {"history beyond visible rows", false, ViewModeHistory, 0, limit, 30, limit},
83+ {"favorites capped at results limit", false, ViewModeFavorites, 0, 3, 30, limit},
84+ {"bangs win over view mode", true, ViewModeHistory, 2, 8, 0, 2},
85+ {"empty list", false, ViewModeFavorites, 4, 4, 0, 0},
86+ }
87+ for _, tt := range tests {
88+ t.Run(tt.name, func(t *testing.T) {
89+ got := navigableCount(tt.bangMode, tt.mode, tt.bangs, tt.suggestions, tt.favorites)
90+ if got != tt.want {
91+ t.Fatalf("navigableCount() = %d, want %d", got, tt.want)
92+ }
93+ })
94+ }
95+}
@@ -18,26 +18,26 @@ func TestGhostCompletion(t *testing.T) {
18 }{18 }{
19 {19 {
20 name: "host-like input completes to the domain, not the redirect URL",20 name: "host-like input completes to the domain, not the redirect URL",
21- req: ghostCompletionRequest{EntryText: "goo", Mode: ViewModeHistory, MaxVisible: 5, Suggestions: history},21+ req: ghostCompletionRequest{EntryText: "goo", Mode: ViewModeHistory, MaxResults: 5, Suggestions: history},
22 wantFull: "google.com",22 wantFull: "google.com",
23 wantSuffix: "gle.com",23 wantSuffix: "gle.com",
24 wantOK: true,24 wantOK: true,
25 },25 },
26 {26 {
27 name: "leading whitespace is ignored",27 name: "leading whitespace is ignored",
28- req: ghostCompletionRequest{EntryText: " gith", Mode: ViewModeHistory, MaxVisible: 5, Suggestions: history},28+ req: ghostCompletionRequest{EntryText: " gith", Mode: ViewModeHistory, MaxResults: 5, Suggestions: history},
29 wantFull: "github.com",29 wantFull: "github.com",
30 wantSuffix: "ub.com",30 wantSuffix: "ub.com",
31 wantOK: true,31 wantOK: true,
32 },32 },
33 {33 {
34 name: "hidden rows never complete",34 name: "hidden rows never complete",
35- req: ghostCompletionRequest{EntryText: "gith", Mode: ViewModeHistory, MaxVisible: 1, Suggestions: history},35+ req: ghostCompletionRequest{EntryText: "gith", Mode: ViewModeHistory, MaxResults: 1, Suggestions: history},
36 wantOK: false,36 wantOK: false,
37 },37 },
38 {38 {
39 name: "favorites mode uses favorites",39 name: "favorites mode uses favorites",
40- req: ghostCompletionRequest{EntryText: "gitl", Mode: ViewModeFavorites, MaxVisible: 5, Favorites: favorites},40+ req: ghostCompletionRequest{EntryText: "gitl", Mode: ViewModeFavorites, MaxResults: 5, Favorites: favorites},
41 wantFull: "gitlab.com",41 wantFull: "gitlab.com",
42 wantSuffix: "ab.com",42 wantSuffix: "ab.com",
43 wantOK: true,43 wantOK: true,
@@ -46,7 +46,7 @@ func TestGhostCompletion(t *testing.T) {
46 name: "explicit selection completes from the selected URL, not the best match",46 name: "explicit selection completes from the selected URL, not the best match",
47 req: ghostCompletionRequest{47 req: ghostCompletionRequest{
48 EntryText: "gi", SelectedURL: "https://gitlab.com/", HasExplicitSelection: true,48 EntryText: "gi", SelectedURL: "https://gitlab.com/", HasExplicitSelection: true,
49- Mode: ViewModeHistory, MaxVisible: 5, Suggestions: history,49+ Mode: ViewModeHistory, MaxResults: 5, Suggestions: history,
50 },50 },
51 wantFull: "gitlab.com",51 wantFull: "gitlab.com",
52 wantSuffix: "tlab.com",52 wantSuffix: "tlab.com",
@@ -54,7 +54,7 @@ func TestGhostCompletion(t *testing.T) {
54 },54 },
55 {55 {
56 name: "blank input shows nothing",56 name: "blank input shows nothing",
57- req: ghostCompletionRequest{EntryText: " ", Mode: ViewModeHistory, MaxVisible: 5, Suggestions: history},57+ req: ghostCompletionRequest{EntryText: " ", Mode: ViewModeHistory, MaxResults: 5, Suggestions: history},
58 wantOK: false,58 wantOK: false,
59 },59 },
60 }60 }
@@ -69,3 +69,27 @@ func TestGhostCompletion(t *testing.T) {
69 })69 })
70 }70 }
71 }71 }
72+
73+func TestNavigableCount(t *testing.T) {
74+ limit := OmniboxListDefaults.MaxResults
75+ tests := []struct {
76+ name string
77+ bangMode bool
78+ mode ViewMode
79+ bangs, suggestions, favorites int
80+ want int
81+ }{
82+ {"history beyond visible rows", false, ViewModeHistory, 0, limit, 30, limit},
83+ {"favorites capped at results limit", false, ViewModeFavorites, 0, 3, 30, limit},
84+ {"bangs win over view mode", true, ViewModeHistory, 2, 8, 0, 2},
85+ {"empty list", false, ViewModeFavorites, 4, 4, 0, 0},
86+ }
87+ for _, tt := range tests {
88+ t.Run(tt.name, func(t *testing.T) {
89+ got := navigableCount(tt.bangMode, tt.mode, tt.bangs, tt.suggestions, tt.favorites)
90+ if got != tt.want {
91+ t.Fatalf("navigableCount() = %d, want %d", got, tt.want)
92+ }
93+ })
94+ }
95+}

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