fix(omnibox): center in-pane omnibox and show 5 of 10 scrollable results (#466)
* fix(omnibox): center in-pane omnibox and show 5 of 10 scrollable results * fix(omnibox): cap rendered rows to navigable results and scroll only on keyboard selection * fix(omnibox): log applied top margin and tighten result limit tests * build(lint): pin golangci-lint v2.14.0 for Go 1.27.2 export data * fix(omnibox): top-align omnibox when a top margin percentage is configured
- commit
- aefccc8a62499d798b66b3c232be30ff68f89650
- author
- bnema · · 2026-10-10 01:31 UTC
- committer
- GitHub · · 2026-10-10 01:31 UTC
- parent
- e7ebbf7
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
8 files · +155 -75
M Makefile +1 -1
| @@ -14,7 +14,7 @@ MAIN_PATH=./cmd/dumber | |||
| 14 | 14 | DIST_DIR=dist | |
| 15 | 15 | LOCAL_BIN_DIR?=$(HOME)/.local/bin | |
| 16 | 16 | TOOL_BIN_DIR?=$(shell go env GOPATH)/bin | |
| 17 | - | GOLANGCI_LINT_VERSION?=v2.13.2 | |
| 17 | + | GOLANGCI_LINT_VERSION?=v2.14.0 | |
| 18 | 18 | STATICCHECK_VERSION?=v0.7.0 | |
| 19 | 19 | ||
| 20 | 20 | # Detect number of CPU cores for parallel compilation | |
| @@ -14,7 +14,7 @@ MAIN_PATH=./cmd/dumber | |||||
| 14 | DIST_DIR=dist | 14 | DIST_DIR=dist | ||
| 15 | LOCAL_BIN_DIR?=$(HOME)/.local/bin | 15 | LOCAL_BIN_DIR?=$(HOME)/.local/bin | ||
| 16 | TOOL_BIN_DIR?=$(shell go env GOPATH)/bin | 16 | TOOL_BIN_DIR?=$(shell go env GOPATH)/bin | ||
| 17 | - | GOLANGCI_LINT_VERSION?=v2.13.2 | 17 | + | GOLANGCI_LINT_VERSION?=v2.14.0 |
| 18 | STATICCHECK_VERSION?=v0.7.0 | 18 | STATICCHECK_VERSION?=v0.7.0 | ||
| 19 | 19 | ||||
| 20 | # Detect number of CPU cores for parallel compilation | 20 | # Detect number of CPU cores for parallel compilation | ||
M internal/ui/component/modal_sizing.go +5 -4
| @@ -43,19 +43,20 @@ var DefaultRowHeights = RowHeightDefaults{ | |||
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | 45 | // OmniboxSizeDefaults provides default sizing for omnibox modal. | |
| 46 | + | // The in-pane omnibox is vertically centered, so it has no top margin. | |
| 46 | 47 | var OmniboxSizeDefaults = ModalSizeConfig{ | |
| 47 | 48 | WidthPct: 0.8, | |
| 48 | 49 | MaxWidth: 800, | |
| 49 | - | TopMarginPct: 0.2, | |
| 50 | 50 | FallbackWidth: 800, | |
| 51 | 51 | FallbackHeight: 600, | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | - | // OmniboxListDefaults provides display limits for omnibox modal. | |
| 54 | + | // OmniboxListDefaults provides display limits for omnibox modal: | |
| 55 | + | // MaxResults rows are listed and navigable, MaxVisibleRows fit before scrolling. | |
| 55 | 56 | var OmniboxListDefaults = ListDisplayDefaults{ | |
| 56 | - | MaxVisibleRows: 10, | |
| 57 | + | MaxVisibleRows: 5, | |
| 57 | 58 | MaxResults: 10, | |
| 58 | - | SmallMaxVisibleRows: 5, | |
| 59 | + | SmallMaxVisibleRows: 3, | |
| 59 | 60 | } | |
| 60 | 61 | ||
| 61 | 62 | // SessionManagerSizeDefaults provides default sizing for session manager modal. | |
| @@ -43,19 +43,20 @@ var DefaultRowHeights = RowHeightDefaults{ | |||||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | // OmniboxSizeDefaults provides default sizing for omnibox modal. | 45 | // OmniboxSizeDefaults provides default sizing for omnibox modal. | ||
| 46 | + | // The in-pane omnibox is vertically centered, so it has no top margin. | |||
| 46 | var OmniboxSizeDefaults = ModalSizeConfig{ | 47 | var OmniboxSizeDefaults = ModalSizeConfig{ | ||
| 47 | WidthPct: 0.8, | 48 | WidthPct: 0.8, | ||
| 48 | MaxWidth: 800, | 49 | MaxWidth: 800, | ||
| 49 | - | TopMarginPct: 0.2, | |||
| 50 | FallbackWidth: 800, | 50 | FallbackWidth: 800, | ||
| 51 | FallbackHeight: 600, | 51 | FallbackHeight: 600, | ||
| 52 | } | 52 | } | ||
| 53 | 53 | ||||
| 54 | - | // OmniboxListDefaults provides display limits for omnibox modal. | 54 | + | // OmniboxListDefaults provides display limits for omnibox modal: |
| 55 | + | // MaxResults rows are listed and navigable, MaxVisibleRows fit before scrolling. | |||
| 55 | var OmniboxListDefaults = ListDisplayDefaults{ | 56 | var OmniboxListDefaults = ListDisplayDefaults{ | ||
| 56 | - | MaxVisibleRows: 10, | 57 | + | MaxVisibleRows: 5, |
| 57 | MaxResults: 10, | 58 | MaxResults: 10, | ||
| 58 | - | SmallMaxVisibleRows: 5, | 59 | + | SmallMaxVisibleRows: 3, |
| 59 | } | 60 | } | ||
| 60 | 61 | ||||
| 61 | // SessionManagerSizeDefaults provides default sizing for session manager modal. | 62 | // SessionManagerSizeDefaults provides default sizing for session manager modal. | ||
M internal/ui/component/modal_sizing_test.go +16 -3
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "testing" | |
| 6 | 6 | ||
| 7 | 7 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | |
| 8 | + | "github.com/bnema/puregotk/v4/gtk" | |
| 8 | 9 | "github.com/stretchr/testify/assert" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| @@ -168,10 +169,22 @@ func TestDefaultRowHeights_Values(t *testing.T) { | |||
| 168 | 169 | func TestOmniboxSizeDefaults_Values(t *testing.T) { | |
| 169 | 170 | assert.InDelta(t, 0.8, OmniboxSizeDefaults.WidthPct, 0.0001) | |
| 170 | 171 | assert.Equal(t, 800, OmniboxSizeDefaults.MaxWidth) | |
| 171 | - | assert.InDelta(t, 0.2, OmniboxSizeDefaults.TopMarginPct, 0.0001) | |
| 172 | + | assert.Zero(t, OmniboxSizeDefaults.TopMarginPct) | |
| 172 | 173 | assert.Equal(t, 600, OmniboxSizeDefaults.FallbackHeight) | |
| 173 | 174 | } | |
| 174 | 175 | ||
| 176 | + | func TestOmniboxValign(t *testing.T) { | |
| 177 | + | assert.Equal(t, gtk.AlignCenterValue, omniboxValign(OmniboxSizeDefaults)) | |
| 178 | + | assert.Equal(t, gtk.AlignStartValue, omniboxValign(ModalSizeConfig{UseFixedTopMargin: true})) | |
| 179 | + | assert.Equal(t, gtk.AlignStartValue, omniboxValign(ResolveModalSizeConfig( | |
| 180 | + | ModalSizeConfig{TopMarginPct: 0.2}, OmniboxSizeDefaults))) | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | func TestOmniboxListDefaults_ScrollableResults(t *testing.T) { | |
| 184 | + | assert.Less(t, OmniboxListDefaults.MaxVisibleRows, OmniboxListDefaults.MaxResults) | |
| 185 | + | assert.Less(t, OmniboxListDefaults.SmallMaxVisibleRows, OmniboxListDefaults.MaxVisibleRows) | |
| 186 | + | } | |
| 187 | + | ||
| 175 | 188 | func TestSessionManagerSizeDefaults_Values(t *testing.T) { | |
| 176 | 189 | assert.Greater(t, SessionManagerSizeDefaults.WidthPct, 0.0) | |
| 177 | 190 | assert.LessOrEqual(t, SessionManagerSizeDefaults.WidthPct, 1.0) | |
| @@ -325,7 +338,7 @@ func TestCalculateModalDimensionsWithScale_LargeParent(t *testing.T) { | |||
| 325 | 338 | parent := mockParent(t, 3000, 1200) | |
| 326 | 339 | w, m := CalculateModalDimensionsWithScale(parent, cfg, tt.scale) | |
| 327 | 340 | assert.Equal(t, tt.want, w, "scale %v", tt.scale) | |
| 328 | - | assert.Equal(t, 240, m, "top margin unchanged") | |
| 341 | + | assert.Zero(t, m, "omnibox is centered, not top-margined") | |
| 329 | 342 | } | |
| 330 | 343 | } | |
| 331 | 344 | ||
| @@ -351,7 +364,7 @@ func TestCalculateModalDimensionsWithScale_NormalizesBadScale(t *testing.T) { | |||
| 351 | 364 | parent := mockParent(t, 3000, 1080) | |
| 352 | 365 | w, m := CalculateModalDimensionsWithScale(parent, cfg, scale) | |
| 353 | 366 | assert.Equal(t, 800, w, "scale %v", scale) | |
| 354 | - | assert.Equal(t, 216, m, "scale %v", scale) | |
| 367 | + | assert.Zero(t, m, "scale %v", scale) | |
| 355 | 368 | } | |
| 356 | 369 | } | |
| 357 | 370 | ||
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "testing" | 5 | "testing" | ||
| 6 | 6 | ||||
| 7 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | 7 | layoutmocks "github.com/bnema/dumber/internal/ui/layout/mocks" | ||
| 8 | + | "github.com/bnema/puregotk/v4/gtk" | |||
| 8 | "github.com/stretchr/testify/assert" | 9 | "github.com/stretchr/testify/assert" | ||
| 9 | ) | 10 | ) | ||
| 10 | 11 | ||||
| @@ -168,10 +169,22 @@ func TestDefaultRowHeights_Values(t *testing.T) { | |||||
| 168 | func TestOmniboxSizeDefaults_Values(t *testing.T) { | 169 | func TestOmniboxSizeDefaults_Values(t *testing.T) { | ||
| 169 | assert.InDelta(t, 0.8, OmniboxSizeDefaults.WidthPct, 0.0001) | 170 | assert.InDelta(t, 0.8, OmniboxSizeDefaults.WidthPct, 0.0001) | ||
| 170 | assert.Equal(t, 800, OmniboxSizeDefaults.MaxWidth) | 171 | assert.Equal(t, 800, OmniboxSizeDefaults.MaxWidth) | ||
| 171 | - | assert.InDelta(t, 0.2, OmniboxSizeDefaults.TopMarginPct, 0.0001) | 172 | + | assert.Zero(t, OmniboxSizeDefaults.TopMarginPct) |
| 172 | assert.Equal(t, 600, OmniboxSizeDefaults.FallbackHeight) | 173 | assert.Equal(t, 600, OmniboxSizeDefaults.FallbackHeight) | ||
| 173 | } | 174 | } | ||
| 174 | 175 | ||||
| 176 | + | func TestOmniboxValign(t *testing.T) { | |||
| 177 | + | assert.Equal(t, gtk.AlignCenterValue, omniboxValign(OmniboxSizeDefaults)) | |||
| 178 | + | assert.Equal(t, gtk.AlignStartValue, omniboxValign(ModalSizeConfig{UseFixedTopMargin: true})) | |||
| 179 | + | assert.Equal(t, gtk.AlignStartValue, omniboxValign(ResolveModalSizeConfig( | |||
| 180 | + | ModalSizeConfig{TopMarginPct: 0.2}, OmniboxSizeDefaults))) | |||
| 181 | + | } | |||
| 182 | + | ||||
| 183 | + | func TestOmniboxListDefaults_ScrollableResults(t *testing.T) { | |||
| 184 | + | assert.Less(t, OmniboxListDefaults.MaxVisibleRows, OmniboxListDefaults.MaxResults) | |||
| 185 | + | assert.Less(t, OmniboxListDefaults.SmallMaxVisibleRows, OmniboxListDefaults.MaxVisibleRows) | |||
| 186 | + | } | |||
| 187 | + | ||||
| 175 | func TestSessionManagerSizeDefaults_Values(t *testing.T) { | 188 | func TestSessionManagerSizeDefaults_Values(t *testing.T) { | ||
| 176 | assert.Greater(t, SessionManagerSizeDefaults.WidthPct, 0.0) | 189 | assert.Greater(t, SessionManagerSizeDefaults.WidthPct, 0.0) | ||
| 177 | assert.LessOrEqual(t, SessionManagerSizeDefaults.WidthPct, 1.0) | 190 | assert.LessOrEqual(t, SessionManagerSizeDefaults.WidthPct, 1.0) | ||
| @@ -325,7 +338,7 @@ func TestCalculateModalDimensionsWithScale_LargeParent(t *testing.T) { | |||||
| 325 | parent := mockParent(t, 3000, 1200) | 338 | parent := mockParent(t, 3000, 1200) | ||
| 326 | w, m := CalculateModalDimensionsWithScale(parent, cfg, tt.scale) | 339 | w, m := CalculateModalDimensionsWithScale(parent, cfg, tt.scale) | ||
| 327 | assert.Equal(t, tt.want, w, "scale %v", tt.scale) | 340 | assert.Equal(t, tt.want, w, "scale %v", tt.scale) | ||
| 328 | - | assert.Equal(t, 240, m, "top margin unchanged") | 341 | + | assert.Zero(t, m, "omnibox is centered, not top-margined") |
| 329 | } | 342 | } | ||
| 330 | } | 343 | } | ||
| 331 | 344 | ||||
| @@ -351,7 +364,7 @@ func TestCalculateModalDimensionsWithScale_NormalizesBadScale(t *testing.T) { | |||||
| 351 | parent := mockParent(t, 3000, 1080) | 364 | parent := mockParent(t, 3000, 1080) | ||
| 352 | w, m := CalculateModalDimensionsWithScale(parent, cfg, scale) | 365 | w, m := CalculateModalDimensionsWithScale(parent, cfg, scale) | ||
| 353 | assert.Equal(t, 800, w, "scale %v", scale) | 366 | assert.Equal(t, 800, w, "scale %v", scale) | ||
| 354 | - | assert.Equal(t, 216, m, "scale %v", scale) | 367 | + | assert.Zero(t, m, "scale %v", scale) |
| 355 | } | 368 | } | ||
| 356 | } | 369 | } | ||
| 357 | 370 | ||||
M internal/ui/component/omnibox.go +66 -37
| @@ -776,12 +776,22 @@ func (o *Omnibox) initOuterBox() error { | |||
| 776 | 776 | return errNilWidget("outerBox") | |
| 777 | 777 | } | |
| 778 | 778 | o.outerBox.AddCssClass("omnibox-outer") | |
| 779 | - | o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally | |
| 780 | - | o.outerBox.SetValign(gtk.AlignStartValue) // Align to top | |
| 781 | - | o.outerBox.SetVisible(false) // Hidden by default | |
| 779 | + | o.outerBox.SetHalign(gtk.AlignCenterValue) | |
| 780 | + | o.outerBox.SetValign(omniboxValign(o.sizeCfg)) | |
| 781 | + | o.outerBox.SetVisible(false) // Hidden by default | |
| 782 | 782 | return nil | |
| 783 | 783 | } | |
| 784 | 784 | ||
| 785 | + | // omniboxValign centers the omnibox vertically by default. Any configured top | |
| 786 | + | // margin (fixed for the content-sized standalone window, or a percentage | |
| 787 | + | // override) keeps it top-aligned so the margin is applied as requested. | |
| 788 | + | func omniboxValign(cfg ModalSizeConfig) gtk.Align { | |
| 789 | + | if cfg.UseFixedTopMargin || cfg.TopMarginPct > 0 { | |
| 790 | + | return gtk.AlignStartValue | |
| 791 | + | } | |
| 792 | + | return gtk.AlignCenterValue | |
| 793 | + | } | |
| 794 | + | ||
| 785 | 795 | func (o *Omnibox) initMainBox() error { | |
| 786 | 796 | o.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0) | |
| 787 | 797 | if o.mainBox == nil { | |
| @@ -923,7 +933,7 @@ func (o *Omnibox) initList() error { | |||
| 923 | 933 | return | |
| 924 | 934 | } | |
| 925 | 935 | ||
| 926 | - | targetURL := resolveTargetURLForSelection(mode, idx, o.effectiveMaxRows(), suggestions, favorites) | |
| 936 | + | targetURL := resolveTargetURLForSelection(mode, idx, OmniboxListDefaults.MaxResults, suggestions, favorites) | |
| 927 | 937 | ||
| 928 | 938 | if targetURL != "" { | |
| 929 | 939 | o.submitNavigation(targetURL) | |
| @@ -1503,7 +1513,7 @@ func (o *Omnibox) restoreEntryToRealInput() { | |||
| 1503 | 1513 | // updateGhostFromSelectionWithInput updates ghost text based on selected row and input. | |
| 1504 | 1514 | func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) { | |
| 1505 | 1515 | log := logging.FromContext(o.ctx) | |
| 1506 | - | maxVisible := o.effectiveMaxRows() | |
| 1516 | + | maxResults := OmniboxListDefaults.MaxResults | |
| 1507 | 1517 | o.mu.RLock() | |
| 1508 | 1518 | idx := o.selectedIndex | |
| 1509 | 1519 | mode := o.viewMode | |
| @@ -1531,10 +1541,10 @@ func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) { | |||
| 1531 | 1541 | return | |
| 1532 | 1542 | } | |
| 1533 | 1543 | ||
| 1534 | - | targetURL, hasExplicitSelection := selectedTargetURL(mode, idx, maxVisible, suggestions, favorites) | |
| 1544 | + | targetURL, hasExplicitSelection := selectedTargetURL(mode, idx, maxResults, suggestions, favorites) | |
| 1535 | 1545 | ||
| 1536 | 1546 | if entryText != "" { | |
| 1537 | - | o.resolveGhostCompletion(entryText, targetURL, hasExplicitSelection, mode, maxVisible, suggestions, favorites) | |
| 1547 | + | o.resolveGhostCompletion(entryText, targetURL, hasExplicitSelection, mode, maxResults, suggestions, favorites) | |
| 1538 | 1548 | return | |
| 1539 | 1549 | } | |
| 1540 | 1550 | ||
| @@ -1556,7 +1566,7 @@ func (o *Omnibox) resolveGhostCompletion( | |||
| 1556 | 1566 | entryText, selectedURL string, | |
| 1557 | 1567 | hasExplicitSelection bool, | |
| 1558 | 1568 | mode ViewMode, | |
| 1559 | - | maxVisible int, | |
| 1569 | + | maxResults int, | |
| 1560 | 1570 | suggestions []Suggestion, | |
| 1561 | 1571 | favorites []Favorite, | |
| 1562 | 1572 | ) { | |
| @@ -1566,7 +1576,7 @@ func (o *Omnibox) resolveGhostCompletion( | |||
| 1566 | 1576 | SelectedURL: selectedURL, | |
| 1567 | 1577 | HasExplicitSelection: hasExplicitSelection, | |
| 1568 | 1578 | Mode: mode, | |
| 1569 | - | MaxVisible: maxVisible, | |
| 1579 | + | MaxResults: maxResults, | |
| 1570 | 1580 | Suggestions: suggestions, | |
| 1571 | 1581 | Favorites: favorites, | |
| 1572 | 1582 | }) | |
| @@ -1632,7 +1642,7 @@ func (o *Omnibox) performSearch() { | |||
| 1632 | 1642 | } | |
| 1633 | 1643 | ||
| 1634 | 1644 | // Perform fuzzy history search in background | |
| 1635 | - | o.searchHistory(query, o.effectiveMaxRows(), token) | |
| 1645 | + | o.searchHistory(query, OmniboxListDefaults.MaxResults, token) | |
| 1636 | 1646 | } | |
| 1637 | 1647 | ||
| 1638 | 1648 | // searchHistory runs a fuzzy history search in a background goroutine. | |
| @@ -1706,7 +1716,7 @@ func (o *Omnibox) loadInitialHistory(token uint64) { | |||
| 1706 | 1716 | ||
| 1707 | 1717 | // Capture effective result limit and age window on the GTK main thread | |
| 1708 | 1718 | // before spawning goroutine. | |
| 1709 | - | initialLimit := o.effectiveMaxRows() | |
| 1719 | + | initialLimit := OmniboxListDefaults.MaxResults | |
| 1710 | 1720 | initialBehavior := o.initialBehavior | |
| 1711 | 1721 | maxHistoryDays := o.maxHistoryDays | |
| 1712 | 1722 | ||
| @@ -1962,8 +1972,13 @@ func (o *Omnibox) updateFavorites(favorites []Favorite, query string) { | |||
| 1962 | 1972 | ||
| 1963 | 1973 | // rebuildList rebuilds the ListBox contents. | |
| 1964 | 1974 | func (o *Omnibox) rebuildList() { | |
| 1965 | - | // Clear existing rows | |
| 1975 | + | // Clear existing rows and show new results from the top | |
| 1966 | 1976 | o.listBox.RemoveAll() | |
| 1977 | + | if o.scrolledWin != nil { | |
| 1978 | + | if adj := o.scrolledWin.GetVadjustment(); adj != nil { | |
| 1979 | + | adj.SetValue(0) | |
| 1980 | + | } | |
| 1981 | + | } | |
| 1967 | 1982 | ||
| 1968 | 1983 | o.mu.RLock() | |
| 1969 | 1984 | mode := o.viewMode | |
| @@ -1973,6 +1988,11 @@ func (o *Omnibox) rebuildList() { | |||
| 1973 | 1988 | bangSuggestions := o.bangSuggestions | |
| 1974 | 1989 | o.mu.RUnlock() | |
| 1975 | 1990 | ||
| 1991 | + | limit := OmniboxListDefaults.MaxResults | |
| 1992 | + | bangSuggestions = bangSuggestions[:limitedCount(len(bangSuggestions), limit)] | |
| 1993 | + | suggestions = suggestions[:limitedCount(len(suggestions), limit)] | |
| 1994 | + | favorites = favorites[:limitedCount(len(favorites), limit)] | |
| 1995 | + | ||
| 1976 | 1996 | if bangMode { | |
| 1977 | 1997 | for i, b := range bangSuggestions { | |
| 1978 | 1998 | row := o.createBangRow(b, i) | |
| @@ -2259,21 +2279,37 @@ func (o *Omnibox) selectIndex(index int) { | |||
| 2259 | 2279 | o.updateGhostFromSelection() | |
| 2260 | 2280 | } | |
| 2261 | 2281 | ||
| 2282 | + | // scrollSelectionIntoView scrolls the results list so the row at index is | |
| 2283 | + | // fully visible. The entry keeps keyboard focus, so GTK does not scroll the | |
| 2284 | + | // list itself. Only keyboard navigation calls this: hover must not move rows | |
| 2285 | + | // under the pointer. | |
| 2286 | + | func (o *Omnibox) scrollSelectionIntoView(index int) { | |
| 2287 | + | if o.scrolledWin == nil || o.listBox == nil { | |
| 2288 | + | return | |
| 2289 | + | } | |
| 2290 | + | row := o.listBox.GetRowAtIndex(index) | |
| 2291 | + | if row == nil { | |
| 2292 | + | return | |
| 2293 | + | } | |
| 2294 | + | adj := o.scrolledWin.GetVadjustment() | |
| 2295 | + | if adj == nil { | |
| 2296 | + | return | |
| 2297 | + | } | |
| 2298 | + | src := &graphene.Point{} | |
| 2299 | + | out := &graphene.Point{} | |
| 2300 | + | if !row.ComputePoint(&o.listBox.Widget, src, out) { | |
| 2301 | + | return | |
| 2302 | + | } | |
| 2303 | + | top := float64(out.Y) | |
| 2304 | + | adj.ClampPage(top, top+float64(row.GetAllocatedHeight())) | |
| 2305 | + | } | |
| 2306 | + | ||
| 2262 | 2307 | // selectNext moves selection down. | |
| 2263 | 2308 | func (o *Omnibox) selectNext() { | |
| 2264 | 2309 | o.mu.Lock() | |
| 2265 | 2310 | current := o.selectedIndex | |
| 2266 | - | mode := o.viewMode | |
| 2267 | - | bangMode := o.bangMode | |
| 2268 | - | maxVisible := o.effectiveMaxRows() | |
| 2269 | - | var maxIndex int | |
| 2270 | - | if bangMode { | |
| 2271 | - | maxIndex = visibleResultCount(len(o.bangSuggestions), maxVisible) - 1 | |
| 2272 | - | } else if mode == ViewModeHistory { | |
| 2273 | - | maxIndex = visibleResultCount(len(o.suggestions), maxVisible) - 1 | |
| 2274 | - | } else { | |
| 2275 | - | maxIndex = visibleResultCount(len(o.favorites), maxVisible) - 1 | |
| 2276 | - | } | |
| 2311 | + | maxIndex := navigableCount(o.bangMode, o.viewMode, | |
| 2312 | + | len(o.bangSuggestions), len(o.suggestions), len(o.favorites)) - 1 | |
| 2277 | 2313 | o.hasNavigated = true // User is navigating with arrow keys | |
| 2278 | 2314 | o.mu.Unlock() | |
| 2279 | 2315 | ||
| @@ -2286,23 +2322,15 @@ func (o *Omnibox) selectNext() { | |||
| 2286 | 2322 | newIndex = 0 // Wrap around | |
| 2287 | 2323 | } | |
| 2288 | 2324 | o.selectIndex(newIndex) | |
| 2325 | + | o.scrollSelectionIntoView(newIndex) | |
| 2289 | 2326 | } | |
| 2290 | 2327 | ||
| 2291 | 2328 | // selectPrevious moves selection up. | |
| 2292 | 2329 | func (o *Omnibox) selectPrevious() { | |
| 2293 | 2330 | o.mu.Lock() | |
| 2294 | 2331 | current := o.selectedIndex | |
| 2295 | - | mode := o.viewMode | |
| 2296 | - | bangMode := o.bangMode | |
| 2297 | - | maxVisible := o.effectiveMaxRows() | |
| 2298 | - | var maxIndex int | |
| 2299 | - | if bangMode { | |
| 2300 | - | maxIndex = visibleResultCount(len(o.bangSuggestions), maxVisible) - 1 | |
| 2301 | - | } else if mode == ViewModeHistory { | |
| 2302 | - | maxIndex = visibleResultCount(len(o.suggestions), maxVisible) - 1 | |
| 2303 | - | } else { | |
| 2304 | - | maxIndex = visibleResultCount(len(o.favorites), maxVisible) - 1 | |
| 2305 | - | } | |
| 2332 | + | maxIndex := navigableCount(o.bangMode, o.viewMode, | |
| 2333 | + | len(o.bangSuggestions), len(o.suggestions), len(o.favorites)) - 1 | |
| 2306 | 2334 | o.hasNavigated = true // User is navigating with arrow keys | |
| 2307 | 2335 | o.mu.Unlock() | |
| 2308 | 2336 | ||
| @@ -2315,6 +2343,7 @@ func (o *Omnibox) selectPrevious() { | |||
| 2315 | 2343 | newIndex = maxIndex // Wrap around | |
| 2316 | 2344 | } | |
| 2317 | 2345 | o.selectIndex(newIndex) | |
| 2346 | + | o.scrollSelectionIntoView(newIndex) | |
| 2318 | 2347 | } | |
| 2319 | 2348 | ||
| 2320 | 2349 | // selectAndNavigate selects an index and navigates to it. | |
| @@ -2327,14 +2356,14 @@ func (o *Omnibox) selectAndNavigate(index int) { | |||
| 2327 | 2356 | suggestions := o.suggestions | |
| 2328 | 2357 | favorites := o.favorites | |
| 2329 | 2358 | o.mu.RUnlock() | |
| 2330 | - | maxVisible := o.effectiveMaxRows() | |
| 2359 | + | maxResults := OmniboxListDefaults.MaxResults | |
| 2331 | 2360 | ||
| 2332 | 2361 | if bangMode { | |
| 2333 | 2362 | o.navigateToSelected() | |
| 2334 | 2363 | return | |
| 2335 | 2364 | } | |
| 2336 | 2365 | ||
| 2337 | - | targetURL := resolveTargetURLForSelection(mode, index, maxVisible, suggestions, favorites) | |
| 2366 | + | targetURL := resolveTargetURLForSelection(mode, index, maxResults, suggestions, favorites) | |
| 2338 | 2367 | if targetURL == "" { | |
| 2339 | 2368 | return | |
| 2340 | 2369 | } | |
| @@ -2400,7 +2429,7 @@ func (o *Omnibox) navigateToSelected() { | |||
| 2400 | 2429 | var targetURL string | |
| 2401 | 2430 | if idx >= 0 { | |
| 2402 | 2431 | // If user has selected a result, navigate to that result. | |
| 2403 | - | targetURL = resolveTargetURLForSelection(mode, idx, o.effectiveMaxRows(), suggestions, favorites) | |
| 2432 | + | targetURL = resolveTargetURLForSelection(mode, idx, OmniboxListDefaults.MaxResults, suggestions, favorites) | |
| 2404 | 2433 | } else { | |
| 2405 | 2434 | targetURL = o.buildURL(entryText) | |
| 2406 | 2435 | } | |
| @@ -776,12 +776,22 @@ func (o *Omnibox) initOuterBox() error { | |||||
| 776 | return errNilWidget("outerBox") | 776 | return errNilWidget("outerBox") | ||
| 777 | } | 777 | } | ||
| 778 | o.outerBox.AddCssClass("omnibox-outer") | 778 | o.outerBox.AddCssClass("omnibox-outer") | ||
| 779 | - | o.outerBox.SetHalign(gtk.AlignCenterValue) // Center horizontally | 779 | + | o.outerBox.SetHalign(gtk.AlignCenterValue) |
| 780 | - | o.outerBox.SetValign(gtk.AlignStartValue) // Align to top | 780 | + | o.outerBox.SetValign(omniboxValign(o.sizeCfg)) |
| 781 | - | o.outerBox.SetVisible(false) // Hidden by default | 781 | + | o.outerBox.SetVisible(false) // Hidden by default |
| 782 | return nil | 782 | return nil | ||
| 783 | } | 783 | } | ||
| 784 | 784 | ||||
| 785 | + | // omniboxValign centers the omnibox vertically by default. Any configured top | |||
| 786 | + | // margin (fixed for the content-sized standalone window, or a percentage | |||
| 787 | + | // override) keeps it top-aligned so the margin is applied as requested. | |||
| 788 | + | func omniboxValign(cfg ModalSizeConfig) gtk.Align { | |||
| 789 | + | if cfg.UseFixedTopMargin || cfg.TopMarginPct > 0 { | |||
| 790 | + | return gtk.AlignStartValue | |||
| 791 | + | } | |||
| 792 | + | return gtk.AlignCenterValue | |||
| 793 | + | } | |||
| 794 | + | ||||
| 785 | func (o *Omnibox) initMainBox() error { | 795 | func (o *Omnibox) initMainBox() error { | ||
| 786 | o.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0) | 796 | o.mainBox = gtk.NewBox(gtk.OrientationVerticalValue, 0) | ||
| 787 | if o.mainBox == nil { | 797 | if o.mainBox == nil { | ||
| @@ -923,7 +933,7 @@ func (o *Omnibox) initList() error { | |||||
| 923 | return | 933 | return | ||
| 924 | } | 934 | } | ||
| 925 | 935 | ||||
| 926 | - | targetURL := resolveTargetURLForSelection(mode, idx, o.effectiveMaxRows(), suggestions, favorites) | 936 | + | targetURL := resolveTargetURLForSelection(mode, idx, OmniboxListDefaults.MaxResults, suggestions, favorites) |
| 927 | 937 | ||||
| 928 | if targetURL != "" { | 938 | if targetURL != "" { | ||
| 929 | o.submitNavigation(targetURL) | 939 | o.submitNavigation(targetURL) | ||
| @@ -1503,7 +1513,7 @@ func (o *Omnibox) restoreEntryToRealInput() { | |||||
| 1503 | // updateGhostFromSelectionWithInput updates ghost text based on selected row and input. | 1513 | // updateGhostFromSelectionWithInput updates ghost text based on selected row and input. | ||
| 1504 | func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) { | 1514 | func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) { | ||
| 1505 | log := logging.FromContext(o.ctx) | 1515 | log := logging.FromContext(o.ctx) | ||
| 1506 | - | maxVisible := o.effectiveMaxRows() | 1516 | + | maxResults := OmniboxListDefaults.MaxResults |
| 1507 | o.mu.RLock() | 1517 | o.mu.RLock() | ||
| 1508 | idx := o.selectedIndex | 1518 | idx := o.selectedIndex | ||
| 1509 | mode := o.viewMode | 1519 | mode := o.viewMode | ||
| @@ -1531,10 +1541,10 @@ func (o *Omnibox) updateGhostFromSelectionWithInput(entryText string) { | |||||
| 1531 | return | 1541 | return | ||
| 1532 | } | 1542 | } | ||
| 1533 | 1543 | ||||
| 1534 | - | targetURL, hasExplicitSelection := selectedTargetURL(mode, idx, maxVisible, suggestions, favorites) | 1544 | + | targetURL, hasExplicitSelection := selectedTargetURL(mode, idx, maxResults, suggestions, favorites) |
| 1535 | 1545 | ||||
| 1536 | if entryText != "" { | 1546 | if entryText != "" { | ||
| 1537 | - | o.resolveGhostCompletion(entryText, targetURL, hasExplicitSelection, mode, maxVisible, suggestions, favorites) | 1547 | + | o.resolveGhostCompletion(entryText, targetURL, hasExplicitSelection, mode, maxResults, suggestions, favorites) |
| 1538 | return | 1548 | return | ||
| 1539 | } | 1549 | } | ||
| 1540 | 1550 | ||||
| @@ -1556,7 +1566,7 @@ func (o *Omnibox) resolveGhostCompletion( | |||||
| 1556 | entryText, selectedURL string, | 1566 | entryText, selectedURL string, | ||
| 1557 | hasExplicitSelection bool, | 1567 | hasExplicitSelection bool, | ||
| 1558 | mode ViewMode, | 1568 | mode ViewMode, | ||
| 1559 | - | maxVisible int, | 1569 | + | maxResults int, |
| 1560 | suggestions []Suggestion, | 1570 | suggestions []Suggestion, | ||
| 1561 | favorites []Favorite, | 1571 | favorites []Favorite, | ||
| 1562 | ) { | 1572 | ) { | ||
| @@ -1566,7 +1576,7 @@ func (o *Omnibox) resolveGhostCompletion( | |||||
| 1566 | SelectedURL: selectedURL, | 1576 | SelectedURL: selectedURL, | ||
| 1567 | HasExplicitSelection: hasExplicitSelection, | 1577 | HasExplicitSelection: hasExplicitSelection, | ||
| 1568 | Mode: mode, | 1578 | Mode: mode, | ||
| 1569 | - | MaxVisible: maxVisible, | 1579 | + | MaxResults: maxResults, |
| 1570 | Suggestions: suggestions, | 1580 | Suggestions: suggestions, | ||
| 1571 | Favorites: favorites, | 1581 | Favorites: favorites, | ||
| 1572 | }) | 1582 | }) | ||
| @@ -1632,7 +1642,7 @@ func (o *Omnibox) performSearch() { | |||||
| 1632 | } | 1642 | } | ||
| 1633 | 1643 | ||||
| 1634 | // Perform fuzzy history search in background | 1644 | // Perform fuzzy history search in background | ||
| 1635 | - | o.searchHistory(query, o.effectiveMaxRows(), token) | 1645 | + | o.searchHistory(query, OmniboxListDefaults.MaxResults, token) |
| 1636 | } | 1646 | } | ||
| 1637 | 1647 | ||||
| 1638 | // searchHistory runs a fuzzy history search in a background goroutine. | 1648 | // searchHistory runs a fuzzy history search in a background goroutine. | ||
| @@ -1706,7 +1716,7 @@ func (o *Omnibox) loadInitialHistory(token uint64) { | |||||
| 1706 | 1716 | ||||
| 1707 | // Capture effective result limit and age window on the GTK main thread | 1717 | // Capture effective result limit and age window on the GTK main thread | ||
| 1708 | // before spawning goroutine. | 1718 | // before spawning goroutine. | ||
| 1709 | - | initialLimit := o.effectiveMaxRows() | 1719 | + | initialLimit := OmniboxListDefaults.MaxResults |
| 1710 | initialBehavior := o.initialBehavior | 1720 | initialBehavior := o.initialBehavior | ||
| 1711 | maxHistoryDays := o.maxHistoryDays | 1721 | maxHistoryDays := o.maxHistoryDays | ||
| 1712 | 1722 | ||||
| @@ -1962,8 +1972,13 @@ func (o *Omnibox) updateFavorites(favorites []Favorite, query string) { | |||||
| 1962 | 1972 | ||||
| 1963 | // rebuildList rebuilds the ListBox contents. | 1973 | // rebuildList rebuilds the ListBox contents. | ||
| 1964 | func (o *Omnibox) rebuildList() { | 1974 | func (o *Omnibox) rebuildList() { | ||
| 1965 | - | // Clear existing rows | 1975 | + | // Clear existing rows and show new results from the top |
| 1966 | o.listBox.RemoveAll() | 1976 | o.listBox.RemoveAll() | ||
| 1977 | + | if o.scrolledWin != nil { | |||
| 1978 | + | if adj := o.scrolledWin.GetVadjustment(); adj != nil { | |||
| 1979 | + | adj.SetValue(0) | |||
| 1980 | + | } | |||
| 1981 | + | } | |||
| 1967 | 1982 | ||||
| 1968 | o.mu.RLock() | 1983 | o.mu.RLock() | ||
| 1969 | mode := o.viewMode | 1984 | mode := o.viewMode | ||
| @@ -1973,6 +1988,11 @@ func (o *Omnibox) rebuildList() { | |||||
| 1973 | bangSuggestions := o.bangSuggestions | 1988 | bangSuggestions := o.bangSuggestions | ||
| 1974 | o.mu.RUnlock() | 1989 | o.mu.RUnlock() | ||
| 1975 | 1990 | ||||
| 1991 | + | limit := OmniboxListDefaults.MaxResults | |||
| 1992 | + | bangSuggestions = bangSuggestions[:limitedCount(len(bangSuggestions), limit)] | |||
| 1993 | + | suggestions = suggestions[:limitedCount(len(suggestions), limit)] | |||
| 1994 | + | favorites = favorites[:limitedCount(len(favorites), limit)] | |||
| 1995 | + | ||||
| 1976 | if bangMode { | 1996 | if bangMode { | ||
| 1977 | for i, b := range bangSuggestions { | 1997 | for i, b := range bangSuggestions { | ||
| 1978 | row := o.createBangRow(b, i) | 1998 | row := o.createBangRow(b, i) | ||
| @@ -2259,21 +2279,37 @@ func (o *Omnibox) selectIndex(index int) { | |||||
| 2259 | o.updateGhostFromSelection() | 2279 | o.updateGhostFromSelection() | ||
| 2260 | } | 2280 | } | ||
| 2261 | 2281 | ||||
| 2282 | + | // scrollSelectionIntoView scrolls the results list so the row at index is | |||
| 2283 | + | // fully visible. The entry keeps keyboard focus, so GTK does not scroll the | |||
| 2284 | + | // list itself. Only keyboard navigation calls this: hover must not move rows | |||
| 2285 | + | // under the pointer. | |||
| 2286 | + | func (o *Omnibox) scrollSelectionIntoView(index int) { | |||
| 2287 | + | if o.scrolledWin == nil || o.listBox == nil { | |||
| 2288 | + | return | |||
| 2289 | + | } | |||
| 2290 | + | row := o.listBox.GetRowAtIndex(index) | |||
| 2291 | + | if row == nil { | |||
| 2292 | + | return | |||
| 2293 | + | } | |||
| 2294 | + | adj := o.scrolledWin.GetVadjustment() | |||
| 2295 | + | if adj == nil { | |||
| 2296 | + | return | |||
| 2297 | + | } | |||
| 2298 | + | src := &graphene.Point{} | |||
| 2299 | + | out := &graphene.Point{} | |||
| 2300 | + | if !row.ComputePoint(&o.listBox.Widget, src, out) { | |||
| 2301 | + | return | |||
| 2302 | + | } | |||
| 2303 | + | top := float64(out.Y) | |||
| 2304 | + | adj.ClampPage(top, top+float64(row.GetAllocatedHeight())) | |||
| 2305 | + | } | |||
| 2306 | + | ||||
| 2262 | // selectNext moves selection down. | 2307 | // selectNext moves selection down. | ||
| 2263 | func (o *Omnibox) selectNext() { | 2308 | func (o *Omnibox) selectNext() { | ||
| 2264 | o.mu.Lock() | 2309 | o.mu.Lock() | ||
| 2265 | current := o.selectedIndex | 2310 | current := o.selectedIndex | ||
| 2266 | - | mode := o.viewMode | 2311 | + | maxIndex := navigableCount(o.bangMode, o.viewMode, |
| 2267 | - | bangMode := o.bangMode | 2312 | + | len(o.bangSuggestions), len(o.suggestions), len(o.favorites)) - 1 |
| 2268 | - | maxVisible := o.effectiveMaxRows() | |||
| 2269 | - | var maxIndex int | |||
| 2270 | - | if bangMode { | |||
| 2271 | - | maxIndex = visibleResultCount(len(o.bangSuggestions), maxVisible) - 1 | |||
| 2272 | - | } else if mode == ViewModeHistory { | |||
| 2273 | - | maxIndex = visibleResultCount(len(o.suggestions), maxVisible) - 1 | |||
| 2274 | - | } else { | |||
| 2275 | - | maxIndex = visibleResultCount(len(o.favorites), maxVisible) - 1 | |||
| 2276 | - | } | |||
| 2277 | o.hasNavigated = true // User is navigating with arrow keys | 2313 | o.hasNavigated = true // User is navigating with arrow keys | ||
| 2278 | o.mu.Unlock() | 2314 | o.mu.Unlock() | ||
| 2279 | 2315 | ||||
| @@ -2286,23 +2322,15 @@ func (o *Omnibox) selectNext() { | |||||
| 2286 | newIndex = 0 // Wrap around | 2322 | newIndex = 0 // Wrap around | ||
| 2287 | } | 2323 | } | ||
| 2288 | o.selectIndex(newIndex) | 2324 | o.selectIndex(newIndex) | ||
| 2325 | + | o.scrollSelectionIntoView(newIndex) | |||
| 2289 | } | 2326 | } | ||
| 2290 | 2327 | ||||
| 2291 | // selectPrevious moves selection up. | 2328 | // selectPrevious moves selection up. | ||
| 2292 | func (o *Omnibox) selectPrevious() { | 2329 | func (o *Omnibox) selectPrevious() { | ||
| 2293 | o.mu.Lock() | 2330 | o.mu.Lock() | ||
| 2294 | current := o.selectedIndex | 2331 | current := o.selectedIndex | ||
| 2295 | - | mode := o.viewMode | 2332 | + | maxIndex := navigableCount(o.bangMode, o.viewMode, |
| 2296 | - | bangMode := o.bangMode | 2333 | + | len(o.bangSuggestions), len(o.suggestions), len(o.favorites)) - 1 |
| 2297 | - | maxVisible := o.effectiveMaxRows() | |||
| 2298 | - | var maxIndex int | |||
| 2299 | - | if bangMode { | |||
| 2300 | - | maxIndex = visibleResultCount(len(o.bangSuggestions), maxVisible) - 1 | |||
| 2301 | - | } else if mode == ViewModeHistory { | |||
| 2302 | - | maxIndex = visibleResultCount(len(o.suggestions), maxVisible) - 1 | |||
| 2303 | - | } else { | |||
| 2304 | - | maxIndex = visibleResultCount(len(o.favorites), maxVisible) - 1 | |||
| 2305 | - | } | |||
| 2306 | o.hasNavigated = true // User is navigating with arrow keys | 2334 | o.hasNavigated = true // User is navigating with arrow keys | ||
| 2307 | o.mu.Unlock() | 2335 | o.mu.Unlock() | ||
| 2308 | 2336 | ||||
| @@ -2315,6 +2343,7 @@ func (o *Omnibox) selectPrevious() { | |||||
| 2315 | newIndex = maxIndex // Wrap around | 2343 | newIndex = maxIndex // Wrap around | ||
| 2316 | } | 2344 | } | ||
| 2317 | o.selectIndex(newIndex) | 2345 | o.selectIndex(newIndex) | ||
| 2346 | + | o.scrollSelectionIntoView(newIndex) | |||
| 2318 | } | 2347 | } | ||
| 2319 | 2348 | ||||
| 2320 | // selectAndNavigate selects an index and navigates to it. | 2349 | // selectAndNavigate selects an index and navigates to it. | ||
| @@ -2327,14 +2356,14 @@ func (o *Omnibox) selectAndNavigate(index int) { | |||||
| 2327 | suggestions := o.suggestions | 2356 | suggestions := o.suggestions | ||
| 2328 | favorites := o.favorites | 2357 | favorites := o.favorites | ||
| 2329 | o.mu.RUnlock() | 2358 | o.mu.RUnlock() | ||
| 2330 | - | maxVisible := o.effectiveMaxRows() | 2359 | + | maxResults := OmniboxListDefaults.MaxResults |
| 2331 | 2360 | ||||
| 2332 | if bangMode { | 2361 | if bangMode { | ||
| 2333 | o.navigateToSelected() | 2362 | o.navigateToSelected() | ||
| 2334 | return | 2363 | return | ||
| 2335 | } | 2364 | } | ||
| 2336 | 2365 | ||||
| 2337 | - | targetURL := resolveTargetURLForSelection(mode, index, maxVisible, suggestions, favorites) | 2366 | + | targetURL := resolveTargetURLForSelection(mode, index, maxResults, suggestions, favorites) |
| 2338 | if targetURL == "" { | 2367 | if targetURL == "" { | ||
| 2339 | return | 2368 | return | ||
| 2340 | } | 2369 | } | ||
| @@ -2400,7 +2429,7 @@ func (o *Omnibox) navigateToSelected() { | |||||
| 2400 | var targetURL string | 2429 | var targetURL string | ||
| 2401 | if idx >= 0 { | 2430 | if idx >= 0 { | ||
| 2402 | // If user has selected a result, navigate to that result. | 2431 | // If user has selected a result, navigate to that result. | ||
| 2403 | - | targetURL = resolveTargetURLForSelection(mode, idx, o.effectiveMaxRows(), suggestions, favorites) | 2432 | + | targetURL = resolveTargetURLForSelection(mode, idx, OmniboxListDefaults.MaxResults, suggestions, favorites) |
| 2404 | } else { | 2433 | } else { | ||
| 2405 | targetURL = o.buildURL(entryText) | 2434 | targetURL = o.buildURL(entryText) | ||
| 2406 | } | 2435 | } | ||
M internal/ui/component/omnibox_initial_behavior_test.go +1 -1
| @@ -287,7 +287,7 @@ func TestOmniboxLoadInitialHistory_MostVisitedBoundsSQL(t *testing.T) { | |||
| 287 | 287 | repo := repomocks.NewMockHistoryRepository(t) | |
| 288 | 288 | done := make(chan struct{}) | |
| 289 | 289 | repo.EXPECT(). | |
| 290 | - | GetMostVisitedWithin(mock.Anything, OmniboxListDefaults.MaxVisibleRows, mock.Anything). | |
| 290 | + | GetMostVisitedWithin(mock.Anything, OmniboxListDefaults.MaxResults, mock.Anything). | |
| 291 | 291 | Run(func(context.Context, int, repository.HistoryScope) { close(done) }). | |
| 292 | 292 | Return([]*entity.HistoryEntry{{URL: "https://example.com"}}, nil) | |
| 293 | 293 | ||
| @@ -287,7 +287,7 @@ func TestOmniboxLoadInitialHistory_MostVisitedBoundsSQL(t *testing.T) { | |||||
| 287 | repo := repomocks.NewMockHistoryRepository(t) | 287 | repo := repomocks.NewMockHistoryRepository(t) | ||
| 288 | done := make(chan struct{}) | 288 | done := make(chan struct{}) | ||
| 289 | repo.EXPECT(). | 289 | repo.EXPECT(). | ||
| 290 | - | GetMostVisitedWithin(mock.Anything, OmniboxListDefaults.MaxVisibleRows, mock.Anything). | 290 | + | GetMostVisitedWithin(mock.Anything, OmniboxListDefaults.MaxResults, mock.Anything). |
| 291 | Run(func(context.Context, int, repository.HistoryScope) { close(done) }). | 291 | Run(func(context.Context, int, repository.HistoryScope) { close(done) }). | ||
| 292 | Return([]*entity.HistoryEntry{{URL: "https://example.com"}}, nil) | 292 | Return([]*entity.HistoryEntry{{URL: "https://example.com"}}, nil) | ||
| 293 | 293 | ||||
M internal/ui/component/omnibox_navigation_state_test.go +1 -1
| @@ -360,7 +360,7 @@ func TestVisibleGhostSuggestion(t *testing.T) { | |||
| 360 | 360 | } | |
| 361 | 361 | } | |
| 362 | 362 | ||
| 363 | - | func TestVisibleGhostSuggestionRespectsVisibleLimit(t *testing.T) { | |
| 363 | + | func TestVisibleGhostSuggestionRespectsResultLimit(t *testing.T) { | |
| 364 | 364 | suggestions := []Suggestion{ | |
| 365 | 365 | {URL: "https://github.com/bnema/dumber"}, | |
| 366 | 366 | {URL: "https://gitlab.com/team/project"}, | |
| @@ -360,7 +360,7 @@ func TestVisibleGhostSuggestion(t *testing.T) { | |||||
| 360 | } | 360 | } | ||
| 361 | } | 361 | } | ||
| 362 | 362 | ||||
| 363 | - | func TestVisibleGhostSuggestionRespectsVisibleLimit(t *testing.T) { | 363 | + | func TestVisibleGhostSuggestionRespectsResultLimit(t *testing.T) { |
| 364 | suggestions := []Suggestion{ | 364 | suggestions := []Suggestion{ | ||
| 365 | {URL: "https://github.com/bnema/dumber"}, | 365 | {URL: "https://github.com/bnema/dumber"}, | ||
| 366 | {URL: "https://gitlab.com/team/project"}, | 366 | {URL: "https://gitlab.com/team/project"}, | ||
M internal/ui/component/omnibox_suggestions.go +35 -22
| @@ -19,7 +19,7 @@ type ghostCompletionRequest struct { | |||
| 19 | 19 | SelectedURL string | |
| 20 | 20 | HasExplicitSelection bool | |
| 21 | 21 | Mode ViewMode | |
| 22 | - | MaxVisible int | |
| 22 | + | MaxResults int | |
| 23 | 23 | Suggestions []Suggestion | |
| 24 | 24 | Favorites []Favorite | |
| 25 | 25 | } | |
| @@ -34,7 +34,7 @@ func ghostCompletion(req ghostCompletionRequest) (fullText, suffix string, ok bo | |||
| 34 | 34 | return "", "", false | |
| 35 | 35 | } | |
| 36 | 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 | 39 | if !ok { | |
| 40 | 40 | return "", "", false | |
| @@ -122,7 +122,7 @@ func visibleGhostSuggestion( | |||
| 122 | 122 | query, selectedURL string, | |
| 123 | 123 | hasExplicitSelection bool, | |
| 124 | 124 | mode ViewMode, | |
| 125 | - | maxVisible int, | |
| 125 | + | maxResults int, | |
| 126 | 126 | suggestions []Suggestion, | |
| 127 | 127 | favorites []Favorite, | |
| 128 | 128 | ) (fullText, suffix string, ok bool) { | |
| @@ -131,16 +131,16 @@ func visibleGhostSuggestion( | |||
| 131 | 131 | return fullText, suffix, ok | |
| 132 | 132 | } | |
| 133 | 133 | ||
| 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) | |
| 136 | 136 | 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 | 140 | 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] { | |
| 144 | 144 | if s.URL != "" { | |
| 145 | 145 | urls = append(urls, s.URL) | |
| 146 | 146 | } | |
| @@ -148,9 +148,9 @@ func visibleURLsForMode(mode ViewMode, maxVisible int, suggestions []Suggestion, | |||
| 148 | 148 | return urls | |
| 149 | 149 | } | |
| 150 | 150 | ||
| 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] { | |
| 154 | 154 | if f.URL != "" { | |
| 155 | 155 | urls = append(urls, f.URL) | |
| 156 | 156 | } | |
| @@ -158,11 +158,11 @@ func visibleURLsForMode(mode ViewMode, maxVisible int, suggestions []Suggestion, | |||
| 158 | 158 | 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 | 162 | if idx < 0 { | |
| 163 | 163 | return "", false | |
| 164 | 164 | } | |
| 165 | - | return resolveTargetURLForSelection(mode, idx, maxVisible, suggestions, favorites), true | |
| 165 | + | return resolveTargetURLForSelection(mode, idx, maxResults, suggestions, favorites), true | |
| 166 | 166 | } | |
| 167 | 167 | ||
| 168 | 168 | func bangSuggestionTextAt(idx int, bangSuggestions []BangSuggestion) (string, bool) { | |
| @@ -172,14 +172,27 @@ func bangSuggestionTextAt(idx int, bangSuggestions []BangSuggestion) (string, bo | |||
| 172 | 172 | return "!" + bangSuggestions[idx].Key + " ", true | |
| 173 | 173 | } | |
| 174 | 174 | ||
| 175 | - | func visibleResultCount(total, maxVisible int) int { | |
| 175 | + | func limitedCount(total, maxResults int) int { | |
| 176 | 176 | if total <= 0 { | |
| 177 | 177 | return 0 | |
| 178 | 178 | } | |
| 179 | - | if maxVisible <= 0 || total < maxVisible { | |
| 179 | + | if maxResults <= 0 || total < maxResults { | |
| 180 | 180 | return total | |
| 181 | 181 | } | |
| 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) | |
| 183 | 196 | } | |
| 184 | 197 | ||
| 185 | 198 | func effectiveSearchQuery(entryText, realInput string, hasGhost bool) string { | |
| @@ -189,16 +202,16 @@ func effectiveSearchQuery(entryText, realInput string, hasGhost bool) string { | |||
| 189 | 202 | 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 | 206 | 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 { | |
| 196 | 209 | return suggestions[idx].URL | |
| 197 | 210 | } | |
| 198 | 211 | return "" | |
| 199 | 212 | } | |
| 200 | - | visibleCount := visibleResultCount(len(favorites), maxVisible) | |
| 201 | - | if idx >= 0 && idx < visibleCount { | |
| 213 | + | count := limitedCount(len(favorites), maxResults) | |
| 214 | + | if idx >= 0 && idx < count { | |
| 202 | 215 | return favorites[idx].URL | |
| 203 | 216 | } | |
| 204 | 217 | return "" | |
| @@ -19,7 +19,7 @@ type ghostCompletionRequest struct { | |||||
| 19 | SelectedURL string | 19 | SelectedURL string | ||
| 20 | HasExplicitSelection bool | 20 | HasExplicitSelection bool | ||
| 21 | Mode ViewMode | 21 | Mode ViewMode | ||
| 22 | - | MaxVisible int | 22 | + | MaxResults int |
| 23 | Suggestions []Suggestion | 23 | Suggestions []Suggestion | ||
| 24 | Favorites []Favorite | 24 | Favorites []Favorite | ||
| 25 | } | 25 | } | ||
| @@ -34,7 +34,7 @@ func ghostCompletion(req ghostCompletionRequest) (fullText, suffix string, ok bo | |||||
| 34 | return "", "", false | 34 | 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 "", "", false | 40 | 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, ok | 131 | 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, ok | 136 | 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 urls | 148 | 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 urls | 158 | 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 "", false | 163 | return "", false | ||
| 164 | } | 164 | } | ||
| 165 | - | return resolveTargetURLForSelection(mode, idx, maxVisible, suggestions, favorites), true | 165 | + | 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 + " ", true | 172 | 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 0 | 177 | return 0 | ||
| 178 | } | 178 | } | ||
| 179 | - | if maxVisible <= 0 || total < maxVisible { | 179 | + | if maxResults <= 0 || total < maxResults { |
| 180 | return total | 180 | return total | ||
| 181 | } | 181 | } | ||
| 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) | |||
| 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 entryText | 202 | 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].URL | 209 | 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].URL | 215 | 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) { | |||
| 18 | 18 | }{ | |
| 19 | 19 | { | |
| 20 | 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 | 22 | wantFull: "google.com", | |
| 23 | 23 | wantSuffix: "gle.com", | |
| 24 | 24 | wantOK: true, | |
| 25 | 25 | }, | |
| 26 | 26 | { | |
| 27 | 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 | 29 | wantFull: "github.com", | |
| 30 | 30 | wantSuffix: "ub.com", | |
| 31 | 31 | wantOK: true, | |
| 32 | 32 | }, | |
| 33 | 33 | { | |
| 34 | 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 | 36 | wantOK: false, | |
| 37 | 37 | }, | |
| 38 | 38 | { | |
| 39 | 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 | 41 | wantFull: "gitlab.com", | |
| 42 | 42 | wantSuffix: "ab.com", | |
| 43 | 43 | wantOK: true, | |
| @@ -46,7 +46,7 @@ func TestGhostCompletion(t *testing.T) { | |||
| 46 | 46 | name: "explicit selection completes from the selected URL, not the best match", | |
| 47 | 47 | req: ghostCompletionRequest{ | |
| 48 | 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 | 51 | wantFull: "gitlab.com", | |
| 52 | 52 | wantSuffix: "tlab.com", | |
| @@ -54,7 +54,7 @@ func TestGhostCompletion(t *testing.T) { | |||
| 54 | 54 | }, | |
| 55 | 55 | { | |
| 56 | 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 | 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 capped at results limit", false, ViewModeHistory, 0, limit + 5, 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 capped at results limit", false, ViewModeHistory, 0, limit + 5, 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 | + | } | |||