Merge pull request #130 from bnema/fix/omnibox-domain-boost
fix: boost domain matches in omnibox search results
15 files · +765 -531
A .github/CODEOWNERS +2 -0
| @@ -0,0 +1,2 @@ | |||
| 1 | + | # Default owner for everything | |
| 2 | + | * @bnema | |
| @@ -0,0 +1,2 @@ | |||||
| 1 | + | # Default owner for everything | |||
| 2 | + | * @bnema | |||
A .github/dependabot.yml +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | version: 2 | |
| 2 | + | updates: | |
| 3 | + | - package-ecosystem: gomod | |
| 4 | + | directory: / | |
| 5 | + | schedule: | |
| 6 | + | interval: weekly | |
| 7 | + | groups: | |
| 8 | + | go-dependencies: | |
| 9 | + | patterns: | |
| 10 | + | - "*" | |
| 11 | + | ||
| 12 | + | - package-ecosystem: npm | |
| 13 | + | directory: /webui | |
| 14 | + | schedule: | |
| 15 | + | interval: weekly | |
| 16 | + | groups: | |
| 17 | + | npm-dependencies: | |
| 18 | + | patterns: | |
| 19 | + | - "*" | |
| 20 | + | ||
| 21 | + | - package-ecosystem: github-actions | |
| 22 | + | directory: / | |
| 23 | + | schedule: | |
| 24 | + | interval: weekly | |
| 25 | + | groups: | |
| 26 | + | github-actions: | |
| 27 | + | patterns: | |
| 28 | + | - "*" | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | version: 2 | |||
| 2 | + | updates: | |||
| 3 | + | - package-ecosystem: gomod | |||
| 4 | + | directory: / | |||
| 5 | + | schedule: | |||
| 6 | + | interval: weekly | |||
| 7 | + | groups: | |||
| 8 | + | go-dependencies: | |||
| 9 | + | patterns: | |||
| 10 | + | - "*" | |||
| 11 | + | ||||
| 12 | + | - package-ecosystem: npm | |||
| 13 | + | directory: /webui | |||
| 14 | + | schedule: | |||
| 15 | + | interval: weekly | |||
| 16 | + | groups: | |||
| 17 | + | npm-dependencies: | |||
| 18 | + | patterns: | |||
| 19 | + | - "*" | |||
| 20 | + | ||||
| 21 | + | - package-ecosystem: github-actions | |||
| 22 | + | directory: / | |||
| 23 | + | schedule: | |||
| 24 | + | interval: weekly | |||
| 25 | + | groups: | |||
| 26 | + | github-actions: | |||
| 27 | + | patterns: | |||
| 28 | + | - "*" | |||
M .github/workflows/aur.yml +2 -2
| @@ -23,7 +23,7 @@ jobs: | |||
| 23 | 23 | cancel-in-progress: true | |
| 24 | 24 | ||
| 25 | 25 | steps: | |
| 26 | - | - uses: actions/checkout@v4 | |
| 26 | + | - uses: actions/checkout@v6 | |
| 27 | 27 | with: | |
| 28 | 28 | ref: ${{ github.event.workflow_run.head_sha }} | |
| 29 | 29 | fetch-depth: 0 | |
| @@ -119,7 +119,7 @@ jobs: | |||
| 119 | 119 | cancel-in-progress: true | |
| 120 | 120 | ||
| 121 | 121 | steps: | |
| 122 | - | - uses: actions/checkout@v4 | |
| 122 | + | - uses: actions/checkout@v6 | |
| 123 | 123 | with: | |
| 124 | 124 | fetch-depth: 0 | |
| 125 | 125 | ||
| @@ -23,7 +23,7 @@ jobs: | |||||
| 23 | cancel-in-progress: true | 23 | cancel-in-progress: true | ||
| 24 | 24 | ||||
| 25 | steps: | 25 | steps: | ||
| 26 | - | - uses: actions/checkout@v4 | 26 | + | - uses: actions/checkout@v6 |
| 27 | with: | 27 | with: | ||
| 28 | ref: ${{ github.event.workflow_run.head_sha }} | 28 | ref: ${{ github.event.workflow_run.head_sha }} | ||
| 29 | fetch-depth: 0 | 29 | fetch-depth: 0 | ||
| @@ -119,7 +119,7 @@ jobs: | |||||
| 119 | cancel-in-progress: true | 119 | cancel-in-progress: true | ||
| 120 | 120 | ||||
| 121 | steps: | 121 | steps: | ||
| 122 | - | - uses: actions/checkout@v4 | 122 | + | - uses: actions/checkout@v6 |
| 123 | with: | 123 | with: | ||
| 124 | fetch-depth: 0 | 124 | fetch-depth: 0 | ||
| 125 | 125 | ||||
M .github/workflows/ci.yml +6 -6
| @@ -41,21 +41,21 @@ jobs: | |||
| 41 | 41 | pacman -S --noconfirm git go nodejs npm webkitgtk-6.0 gtk4 gobject-introspection base-devel | |
| 42 | 42 | - uses: actions/checkout@v6 | |
| 43 | 43 | - name: Cache Go modules | |
| 44 | - | uses: actions/cache@v4 | |
| 44 | + | uses: actions/cache@v5 | |
| 45 | 45 | with: | |
| 46 | 46 | path: ~/go/pkg/mod | |
| 47 | 47 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} | |
| 48 | 48 | restore-keys: | | |
| 49 | 49 | ${{ runner.os }}-go-mod- | |
| 50 | 50 | - name: Cache Go build cache | |
| 51 | - | uses: actions/cache@v4 | |
| 51 | + | uses: actions/cache@v5 | |
| 52 | 52 | with: | |
| 53 | 53 | path: ~/.cache/go-build | |
| 54 | 54 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} | |
| 55 | 55 | restore-keys: | | |
| 56 | 56 | ${{ runner.os }}-go-build- | |
| 57 | 57 | - name: Cache npm | |
| 58 | - | uses: actions/cache@v4 | |
| 58 | + | uses: actions/cache@v5 | |
| 59 | 59 | with: | |
| 60 | 60 | path: ~/.npm | |
| 61 | 61 | key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }} | |
| @@ -82,21 +82,21 @@ jobs: | |||
| 82 | 82 | pacman -S --noconfirm git go nodejs npm webkitgtk-6.0 gtk4 libadwaita gobject-introspection base-devel | |
| 83 | 83 | - uses: actions/checkout@v6 | |
| 84 | 84 | - name: Cache Go modules | |
| 85 | - | uses: actions/cache@v4 | |
| 85 | + | uses: actions/cache@v5 | |
| 86 | 86 | with: | |
| 87 | 87 | path: ~/go/pkg/mod | |
| 88 | 88 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} | |
| 89 | 89 | restore-keys: | | |
| 90 | 90 | ${{ runner.os }}-go-mod- | |
| 91 | 91 | - name: Cache Go build cache | |
| 92 | - | uses: actions/cache@v4 | |
| 92 | + | uses: actions/cache@v5 | |
| 93 | 93 | with: | |
| 94 | 94 | path: ~/.cache/go-build | |
| 95 | 95 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} | |
| 96 | 96 | restore-keys: | | |
| 97 | 97 | ${{ runner.os }}-go-build- | |
| 98 | 98 | - name: Cache npm | |
| 99 | - | uses: actions/cache@v4 | |
| 99 | + | uses: actions/cache@v5 | |
| 100 | 100 | with: | |
| 101 | 101 | path: ~/.npm | |
| 102 | 102 | key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }} | |
| @@ -41,21 +41,21 @@ jobs: | |||||
| 41 | pacman -S --noconfirm git go nodejs npm webkitgtk-6.0 gtk4 gobject-introspection base-devel | 41 | pacman -S --noconfirm git go nodejs npm webkitgtk-6.0 gtk4 gobject-introspection base-devel | ||
| 42 | - uses: actions/checkout@v6 | 42 | - uses: actions/checkout@v6 | ||
| 43 | - name: Cache Go modules | 43 | - name: Cache Go modules | ||
| 44 | - | uses: actions/cache@v4 | 44 | + | uses: actions/cache@v5 |
| 45 | with: | 45 | with: | ||
| 46 | path: ~/go/pkg/mod | 46 | path: ~/go/pkg/mod | ||
| 47 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} | 47 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} | ||
| 48 | restore-keys: | | 48 | restore-keys: | | ||
| 49 | ${{ runner.os }}-go-mod- | 49 | ${{ runner.os }}-go-mod- | ||
| 50 | - name: Cache Go build cache | 50 | - name: Cache Go build cache | ||
| 51 | - | uses: actions/cache@v4 | 51 | + | uses: actions/cache@v5 |
| 52 | with: | 52 | with: | ||
| 53 | path: ~/.cache/go-build | 53 | path: ~/.cache/go-build | ||
| 54 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} | 54 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} | ||
| 55 | restore-keys: | | 55 | restore-keys: | | ||
| 56 | ${{ runner.os }}-go-build- | 56 | ${{ runner.os }}-go-build- | ||
| 57 | - name: Cache npm | 57 | - name: Cache npm | ||
| 58 | - | uses: actions/cache@v4 | 58 | + | uses: actions/cache@v5 |
| 59 | with: | 59 | with: | ||
| 60 | path: ~/.npm | 60 | path: ~/.npm | ||
| 61 | key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }} | 61 | key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }} | ||
| @@ -82,21 +82,21 @@ jobs: | |||||
| 82 | pacman -S --noconfirm git go nodejs npm webkitgtk-6.0 gtk4 libadwaita gobject-introspection base-devel | 82 | pacman -S --noconfirm git go nodejs npm webkitgtk-6.0 gtk4 libadwaita gobject-introspection base-devel | ||
| 83 | - uses: actions/checkout@v6 | 83 | - uses: actions/checkout@v6 | ||
| 84 | - name: Cache Go modules | 84 | - name: Cache Go modules | ||
| 85 | - | uses: actions/cache@v4 | 85 | + | uses: actions/cache@v5 |
| 86 | with: | 86 | with: | ||
| 87 | path: ~/go/pkg/mod | 87 | path: ~/go/pkg/mod | ||
| 88 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} | 88 | key: ${{ runner.os }}-go-mod-${{ hashFiles('**/go.sum') }} | ||
| 89 | restore-keys: | | 89 | restore-keys: | | ||
| 90 | ${{ runner.os }}-go-mod- | 90 | ${{ runner.os }}-go-mod- | ||
| 91 | - name: Cache Go build cache | 91 | - name: Cache Go build cache | ||
| 92 | - | uses: actions/cache@v4 | 92 | + | uses: actions/cache@v5 |
| 93 | with: | 93 | with: | ||
| 94 | path: ~/.cache/go-build | 94 | path: ~/.cache/go-build | ||
| 95 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} | 95 | key: ${{ runner.os }}-go-build-${{ hashFiles('**/go.sum') }} | ||
| 96 | restore-keys: | | 96 | restore-keys: | | ||
| 97 | ${{ runner.os }}-go-build- | 97 | ${{ runner.os }}-go-build- | ||
| 98 | - name: Cache npm | 98 | - name: Cache npm | ||
| 99 | - | uses: actions/cache@v4 | 99 | + | uses: actions/cache@v5 |
| 100 | with: | 100 | with: | ||
| 101 | path: ~/.npm | 101 | path: ~/.npm | ||
| 102 | key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }} | 102 | key: ${{ runner.os }}-npm-${{ hashFiles('webui/package-lock.json') }} | ||
A .github/workflows/dependabot-auto-merge.yml +25 -0
| @@ -0,0 +1,25 @@ | |||
| 1 | + | name: Dependabot auto-merge | |
| 2 | + | ||
| 3 | + | on: pull_request | |
| 4 | + | ||
| 5 | + | permissions: | |
| 6 | + | contents: write | |
| 7 | + | pull-requests: write | |
| 8 | + | ||
| 9 | + | jobs: | |
| 10 | + | dependabot: | |
| 11 | + | runs-on: ubuntu-latest | |
| 12 | + | if: github.actor == 'dependabot[bot]' | |
| 13 | + | steps: | |
| 14 | + | - name: Dependabot metadata | |
| 15 | + | id: metadata | |
| 16 | + | uses: dependabot/fetch-metadata@v2 | |
| 17 | + | with: | |
| 18 | + | github-token: "${{ secrets.GITHUB_TOKEN }}" | |
| 19 | + | ||
| 20 | + | - name: Auto-merge patch and minor updates | |
| 21 | + | if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' | |
| 22 | + | run: gh pr merge --auto --squash "$PR_URL" | |
| 23 | + | env: | |
| 24 | + | PR_URL: ${{ github.event.pull_request.html_url }} | |
| 25 | + | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| @@ -0,0 +1,25 @@ | |||||
| 1 | + | name: Dependabot auto-merge | |||
| 2 | + | ||||
| 3 | + | on: pull_request | |||
| 4 | + | ||||
| 5 | + | permissions: | |||
| 6 | + | contents: write | |||
| 7 | + | pull-requests: write | |||
| 8 | + | ||||
| 9 | + | jobs: | |||
| 10 | + | dependabot: | |||
| 11 | + | runs-on: ubuntu-latest | |||
| 12 | + | if: github.actor == 'dependabot[bot]' | |||
| 13 | + | steps: | |||
| 14 | + | - name: Dependabot metadata | |||
| 15 | + | id: metadata | |||
| 16 | + | uses: dependabot/fetch-metadata@v2 | |||
| 17 | + | with: | |||
| 18 | + | github-token: "${{ secrets.GITHUB_TOKEN }}" | |||
| 19 | + | ||||
| 20 | + | - name: Auto-merge patch and minor updates | |||
| 21 | + | if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' | |||
| 22 | + | run: gh pr merge --auto --squash "$PR_URL" | |||
| 23 | + | env: | |||
| 24 | + | PR_URL: ${{ github.event.pull_request.html_url }} | |||
| 25 | + | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |||
M .github/workflows/flatpak.yml +1 -1
| @@ -21,7 +21,7 @@ jobs: | |||
| 21 | 21 | image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 | |
| 22 | 22 | options: --privileged | |
| 23 | 23 | steps: | |
| 24 | - | - uses: actions/checkout@v4 | |
| 24 | + | - uses: actions/checkout@v6 | |
| 25 | 25 | ||
| 26 | 26 | - name: Get version | |
| 27 | 27 | id: version | |
| @@ -21,7 +21,7 @@ jobs: | |||||
| 21 | image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 | 21 | image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48 | ||
| 22 | options: --privileged | 22 | options: --privileged | ||
| 23 | steps: | 23 | steps: | ||
| 24 | - | - uses: actions/checkout@v4 | 24 | + | - uses: actions/checkout@v6 |
| 25 | 25 | ||||
| 26 | - name: Get version | 26 | - name: Get version | ||
| 27 | id: version | 27 | id: version | ||
M go.mod +23 -20
| @@ -8,17 +8,17 @@ require ( | |||
| 8 | 8 | github.com/charmbracelet/bubbletea v1.3.10 | |
| 9 | 9 | github.com/charmbracelet/lipgloss v1.1.0 | |
| 10 | 10 | github.com/fsnotify/fsnotify v1.9.0 | |
| 11 | - | github.com/godbus/dbus/v5 v5.2.1 | |
| 12 | - | github.com/jwijenbergh/puregotk v0.0.0-20251222174431-73930527faa3 | |
| 13 | - | github.com/ncruces/go-sqlite3 v0.30.3 | |
| 11 | + | github.com/godbus/dbus/v5 v5.2.2 | |
| 12 | + | github.com/jwijenbergh/puregotk v0.0.0-20260115100645-c78e1521129b | |
| 13 | + | github.com/ncruces/go-sqlite3 v0.30.4 | |
| 14 | 14 | github.com/pelletier/go-toml/v2 v2.2.4 | |
| 15 | 15 | github.com/pressly/goose/v3 v3.26.0 | |
| 16 | - | github.com/rs/zerolog v1.33.0 | |
| 16 | + | github.com/rs/zerolog v1.34.0 | |
| 17 | 17 | github.com/spf13/cobra v1.10.2 | |
| 18 | 18 | github.com/spf13/viper v1.21.0 | |
| 19 | 19 | github.com/stretchr/testify v1.11.1 | |
| 20 | - | golang.org/x/image v0.34.0 | |
| 21 | - | golang.org/x/sys v0.38.0 | |
| 20 | + | golang.org/x/image v0.35.0 | |
| 21 | + | golang.org/x/sys v0.40.0 | |
| 22 | 22 | gopkg.in/yaml.v3 v3.0.1 | |
| 23 | 23 | ) | |
| 24 | 24 | ||
| @@ -27,21 +27,24 @@ replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-2025 | |||
| 27 | 27 | require ( | |
| 28 | 28 | github.com/atotto/clipboard v0.1.4 // indirect | |
| 29 | 29 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | |
| 30 | - | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect | |
| 31 | - | github.com/charmbracelet/x/ansi v0.10.1 // indirect | |
| 32 | - | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect | |
| 33 | - | github.com/charmbracelet/x/term v0.2.1 // indirect | |
| 34 | - | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect | |
| 30 | + | github.com/charmbracelet/colorprofile v0.4.1 // indirect | |
| 31 | + | github.com/charmbracelet/x/ansi v0.11.4 // indirect | |
| 32 | + | github.com/charmbracelet/x/cellbuf v0.0.14 // indirect | |
| 33 | + | github.com/charmbracelet/x/term v0.2.2 // indirect | |
| 34 | + | github.com/clipperhouse/displaywidth v0.7.0 // indirect | |
| 35 | + | github.com/clipperhouse/stringish v0.1.1 // indirect | |
| 36 | + | github.com/clipperhouse/uax29/v2 v2.3.1 // indirect | |
| 37 | + | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect | |
| 35 | 38 | github.com/davecgh/go-spew v1.1.1 // indirect | |
| 36 | 39 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | |
| 37 | - | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect | |
| 40 | + | github.com/go-viper/mapstructure/v2 v2.5.0 // indirect | |
| 38 | 41 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | |
| 39 | 42 | github.com/jwijenbergh/purego v0.0.0-20251017112123-b71757b9ba42 // indirect | |
| 40 | - | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | |
| 41 | - | github.com/mattn/go-colorable v0.1.13 // indirect | |
| 43 | + | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect | |
| 44 | + | github.com/mattn/go-colorable v0.1.14 // indirect | |
| 42 | 45 | github.com/mattn/go-isatty v0.0.20 // indirect | |
| 43 | 46 | github.com/mattn/go-localereader v0.0.1 // indirect | |
| 44 | - | github.com/mattn/go-runewidth v0.0.16 // indirect | |
| 47 | + | github.com/mattn/go-runewidth v0.0.19 // indirect | |
| 45 | 48 | github.com/mfridman/interpolate v0.0.2 // indirect | |
| 46 | 49 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | |
| 47 | 50 | github.com/muesli/cancelreader v0.2.2 // indirect | |
| @@ -50,19 +53,19 @@ require ( | |||
| 50 | 53 | github.com/pmezard/go-difflib v1.0.0 // indirect | |
| 51 | 54 | github.com/rivo/uniseg v0.4.7 // indirect | |
| 52 | 55 | github.com/russross/blackfriday/v2 v2.1.0 // indirect | |
| 53 | - | github.com/sagikazarmark/locafero v0.11.0 // indirect | |
| 56 | + | github.com/sagikazarmark/locafero v0.12.0 // indirect | |
| 54 | 57 | github.com/sahilm/fuzzy v0.1.1 // indirect | |
| 55 | 58 | github.com/sethvargo/go-retry v0.3.0 // indirect | |
| 56 | - | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect | |
| 57 | 59 | github.com/spf13/afero v1.15.0 // indirect | |
| 58 | 60 | github.com/spf13/cast v1.10.0 // indirect | |
| 59 | 61 | github.com/spf13/pflag v1.0.10 // indirect | |
| 60 | - | github.com/stretchr/objx v0.5.2 // indirect | |
| 62 | + | github.com/stretchr/objx v0.5.3 // indirect | |
| 61 | 63 | github.com/subosito/gotenv v1.6.0 // indirect | |
| 62 | - | github.com/tetratelabs/wazero v1.10.1 // indirect | |
| 64 | + | github.com/tetratelabs/wazero v1.11.0 // indirect | |
| 63 | 65 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect | |
| 64 | 66 | go.uber.org/multierr v1.11.0 // indirect | |
| 65 | 67 | go.yaml.in/yaml/v3 v3.0.4 // indirect | |
| 66 | 68 | golang.org/x/sync v0.19.0 // indirect | |
| 67 | - | golang.org/x/text v0.32.0 // indirect | |
| 69 | + | golang.org/x/text v0.33.0 // indirect | |
| 70 | + | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect | |
| 68 | 71 | ) | |
| @@ -8,17 +8,17 @@ require ( | |||||
| 8 | github.com/charmbracelet/bubbletea v1.3.10 | 8 | github.com/charmbracelet/bubbletea v1.3.10 | ||
| 9 | github.com/charmbracelet/lipgloss v1.1.0 | 9 | github.com/charmbracelet/lipgloss v1.1.0 | ||
| 10 | github.com/fsnotify/fsnotify v1.9.0 | 10 | github.com/fsnotify/fsnotify v1.9.0 | ||
| 11 | - | github.com/godbus/dbus/v5 v5.2.1 | 11 | + | github.com/godbus/dbus/v5 v5.2.2 |
| 12 | - | github.com/jwijenbergh/puregotk v0.0.0-20251222174431-73930527faa3 | 12 | + | github.com/jwijenbergh/puregotk v0.0.0-20260115100645-c78e1521129b |
| 13 | - | github.com/ncruces/go-sqlite3 v0.30.3 | 13 | + | github.com/ncruces/go-sqlite3 v0.30.4 |
| 14 | github.com/pelletier/go-toml/v2 v2.2.4 | 14 | github.com/pelletier/go-toml/v2 v2.2.4 | ||
| 15 | github.com/pressly/goose/v3 v3.26.0 | 15 | github.com/pressly/goose/v3 v3.26.0 | ||
| 16 | - | github.com/rs/zerolog v1.33.0 | 16 | + | github.com/rs/zerolog v1.34.0 |
| 17 | github.com/spf13/cobra v1.10.2 | 17 | github.com/spf13/cobra v1.10.2 | ||
| 18 | github.com/spf13/viper v1.21.0 | 18 | github.com/spf13/viper v1.21.0 | ||
| 19 | github.com/stretchr/testify v1.11.1 | 19 | github.com/stretchr/testify v1.11.1 | ||
| 20 | - | golang.org/x/image v0.34.0 | 20 | + | golang.org/x/image v0.35.0 |
| 21 | - | golang.org/x/sys v0.38.0 | 21 | + | golang.org/x/sys v0.40.0 |
| 22 | gopkg.in/yaml.v3 v3.0.1 | 22 | gopkg.in/yaml.v3 v3.0.1 | ||
| 23 | ) | 23 | ) | ||
| 24 | 24 | ||||
| @@ -27,21 +27,24 @@ replace github.com/jwijenbergh/puregotk => github.com/bnema/puregotk v0.0.0-2025 | |||||
| 27 | require ( | 27 | require ( | ||
| 28 | github.com/atotto/clipboard v0.1.4 // indirect | 28 | github.com/atotto/clipboard v0.1.4 // indirect | ||
| 29 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | 29 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect | ||
| 30 | - | github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect | 30 | + | github.com/charmbracelet/colorprofile v0.4.1 // indirect |
| 31 | - | github.com/charmbracelet/x/ansi v0.10.1 // indirect | 31 | + | github.com/charmbracelet/x/ansi v0.11.4 // indirect |
| 32 | - | github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect | 32 | + | github.com/charmbracelet/x/cellbuf v0.0.14 // indirect |
| 33 | - | github.com/charmbracelet/x/term v0.2.1 // indirect | 33 | + | github.com/charmbracelet/x/term v0.2.2 // indirect |
| 34 | - | github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect | 34 | + | github.com/clipperhouse/displaywidth v0.7.0 // indirect |
| 35 | + | github.com/clipperhouse/stringish v0.1.1 // indirect | |||
| 36 | + | github.com/clipperhouse/uax29/v2 v2.3.1 // indirect | |||
| 37 | + | github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect | |||
| 35 | github.com/davecgh/go-spew v1.1.1 // indirect | 38 | github.com/davecgh/go-spew v1.1.1 // indirect | ||
| 36 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | 39 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect | ||
| 37 | - | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect | 40 | + | github.com/go-viper/mapstructure/v2 v2.5.0 // indirect |
| 38 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | 41 | github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||
| 39 | github.com/jwijenbergh/purego v0.0.0-20251017112123-b71757b9ba42 // indirect | 42 | github.com/jwijenbergh/purego v0.0.0-20251017112123-b71757b9ba42 // indirect | ||
| 40 | - | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | 43 | + | github.com/lucasb-eyer/go-colorful v1.3.0 // indirect |
| 41 | - | github.com/mattn/go-colorable v0.1.13 // indirect | 44 | + | github.com/mattn/go-colorable v0.1.14 // indirect |
| 42 | github.com/mattn/go-isatty v0.0.20 // indirect | 45 | github.com/mattn/go-isatty v0.0.20 // indirect | ||
| 43 | github.com/mattn/go-localereader v0.0.1 // indirect | 46 | github.com/mattn/go-localereader v0.0.1 // indirect | ||
| 44 | - | github.com/mattn/go-runewidth v0.0.16 // indirect | 47 | + | github.com/mattn/go-runewidth v0.0.19 // indirect |
| 45 | github.com/mfridman/interpolate v0.0.2 // indirect | 48 | github.com/mfridman/interpolate v0.0.2 // indirect | ||
| 46 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | 49 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect | ||
| 47 | github.com/muesli/cancelreader v0.2.2 // indirect | 50 | github.com/muesli/cancelreader v0.2.2 // indirect | ||
| @@ -50,19 +53,19 @@ require ( | |||||
| 50 | github.com/pmezard/go-difflib v1.0.0 // indirect | 53 | github.com/pmezard/go-difflib v1.0.0 // indirect | ||
| 51 | github.com/rivo/uniseg v0.4.7 // indirect | 54 | github.com/rivo/uniseg v0.4.7 // indirect | ||
| 52 | github.com/russross/blackfriday/v2 v2.1.0 // indirect | 55 | github.com/russross/blackfriday/v2 v2.1.0 // indirect | ||
| 53 | - | github.com/sagikazarmark/locafero v0.11.0 // indirect | 56 | + | github.com/sagikazarmark/locafero v0.12.0 // indirect |
| 54 | github.com/sahilm/fuzzy v0.1.1 // indirect | 57 | github.com/sahilm/fuzzy v0.1.1 // indirect | ||
| 55 | github.com/sethvargo/go-retry v0.3.0 // indirect | 58 | github.com/sethvargo/go-retry v0.3.0 // indirect | ||
| 56 | - | github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect | |||
| 57 | github.com/spf13/afero v1.15.0 // indirect | 59 | github.com/spf13/afero v1.15.0 // indirect | ||
| 58 | github.com/spf13/cast v1.10.0 // indirect | 60 | github.com/spf13/cast v1.10.0 // indirect | ||
| 59 | github.com/spf13/pflag v1.0.10 // indirect | 61 | github.com/spf13/pflag v1.0.10 // indirect | ||
| 60 | - | github.com/stretchr/objx v0.5.2 // indirect | 62 | + | github.com/stretchr/objx v0.5.3 // indirect |
| 61 | github.com/subosito/gotenv v1.6.0 // indirect | 63 | github.com/subosito/gotenv v1.6.0 // indirect | ||
| 62 | - | github.com/tetratelabs/wazero v1.10.1 // indirect | 64 | + | github.com/tetratelabs/wazero v1.11.0 // indirect |
| 63 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect | 65 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect | ||
| 64 | go.uber.org/multierr v1.11.0 // indirect | 66 | go.uber.org/multierr v1.11.0 // indirect | ||
| 65 | go.yaml.in/yaml/v3 v3.0.4 // indirect | 67 | go.yaml.in/yaml/v3 v3.0.4 // indirect | ||
| 66 | golang.org/x/sync v0.19.0 // indirect | 68 | golang.org/x/sync v0.19.0 // indirect | ||
| 67 | - | golang.org/x/text v0.32.0 // indirect | 69 | + | golang.org/x/text v0.33.0 // indirect |
| 70 | + | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect | |||
| 68 | ) | 71 | ) | ||
M go.sum +43 -38 generated file: collapsed
M internal/infrastructure/persistence/sqlite/history_repo.go +25 -4
| @@ -50,9 +50,14 @@ func (r *historyRepo) FindByURL(ctx context.Context, url string) (*entity.Histor | |||
| 50 | 50 | func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) { | |
| 51 | 51 | log := logging.FromContext(ctx) | |
| 52 | 52 | ||
| 53 | + | // Replace periods with spaces before splitting - FTS5 tokenizer treats periods as separators | |
| 54 | + | // so "gordon.bne" in URLs is indexed as "gordon" + "bne" tokens | |
| 55 | + | // This allows searching "gordon.bne" to match URLs containing both terms | |
| 56 | + | normalizedQuery := strings.ReplaceAll(query, ".", " ") | |
| 57 | + | ||
| 53 | 58 | // Split query into words and add prefix matching to each | |
| 54 | 59 | // This enables multi-word searches like "github issues" -> "github* issues*" | |
| 55 | - | words := strings.Fields(query) | |
| 60 | + | words := strings.Fields(normalizedQuery) | |
| 56 | 61 | if len(words) == 0 { | |
| 57 | 62 | return []entity.HistoryMatch{}, nil | |
| 58 | 63 | } | |
| @@ -66,13 +71,16 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||
| 66 | 71 | ftsQuery := strings.Join(parts, " ") | |
| 67 | 72 | ||
| 68 | 73 | // Search both URL and title columns, then merge results | |
| 69 | - | urlRows, urlErr := r.queries.SearchHistoryFTSUrl(ctx, sqlc.SearchHistoryFTSUrlParams{ | |
| 74 | + | // Use domain-boosted query for URL search to prioritize domain matches | |
| 75 | + | // Use sanitized first word for domain boost (intentional: domain boost targets first search term) | |
| 76 | + | urlRows, urlErr := r.queries.SearchHistoryFTSUrlWithDomainBoost(ctx, sqlc.SearchHistoryFTSUrlWithDomainBoostParams{ | |
| 77 | + | Term: sql.NullString{String: sanitizeFTS5Word(words[0]), Valid: true}, | |
| 70 | 78 | Query: ftsQuery, | |
| 71 | 79 | Limit: int64(limit), | |
| 72 | 80 | }) | |
| 73 | 81 | if urlErr != nil { | |
| 74 | 82 | log.Debug().Err(urlErr).Str("query", query).Msg("FTS URL search failed") | |
| 75 | - | urlRows = []sqlc.History{} | |
| 83 | + | urlRows = []sqlc.SearchHistoryFTSUrlWithDomainBoostRow{} | |
| 76 | 84 | } | |
| 77 | 85 | ||
| 78 | 86 | titleRows, titleErr := r.queries.SearchHistoryFTSTitle(ctx, sqlc.SearchHistoryFTSTitleParams{ | |
| @@ -97,7 +105,7 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||
| 97 | 105 | if !seen[row.ID] { | |
| 98 | 106 | seen[row.ID] = true | |
| 99 | 107 | matches = append(matches, entity.HistoryMatch{ | |
| 100 | - | Entry: historyFromRow(row), | |
| 108 | + | Entry: historyFromDomainBoostRow(row), | |
| 101 | 109 | Score: 1.0, | |
| 102 | 110 | }) | |
| 103 | 111 | } | |
| @@ -120,8 +128,21 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||
| 120 | 128 | return matches, nil | |
| 121 | 129 | } | |
| 122 | 130 | ||
| 131 | + | func historyFromDomainBoostRow(row sqlc.SearchHistoryFTSUrlWithDomainBoostRow) *entity.HistoryEntry { | |
| 132 | + | return &entity.HistoryEntry{ | |
| 133 | + | ID: row.ID, | |
| 134 | + | URL: row.Url, | |
| 135 | + | Title: row.Title.String, | |
| 136 | + | FaviconURL: row.FaviconUrl.String, | |
| 137 | + | VisitCount: row.VisitCount.Int64, | |
| 138 | + | LastVisited: row.LastVisited.Time, | |
| 139 | + | CreatedAt: row.CreatedAt.Time, | |
| 140 | + | } | |
| 141 | + | } | |
| 142 | + | ||
| 123 | 143 | // sanitizeFTS5Word removes FTS5 special characters from a search word. | |
| 124 | 144 | // FTS5 special chars: " * ( ) : ^ - AND OR NOT NEAR | |
| 145 | + | // Note: Periods are handled earlier by replacing with spaces (tokenizer separator) | |
| 125 | 146 | func sanitizeFTS5Word(word string) string { | |
| 126 | 147 | // Remove characters that have special meaning in FTS5 | |
| 127 | 148 | var result strings.Builder | |
| @@ -50,9 +50,14 @@ func (r *historyRepo) FindByURL(ctx context.Context, url string) (*entity.Histor | |||||
| 50 | func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) { | 50 | func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]entity.HistoryMatch, error) { | ||
| 51 | log := logging.FromContext(ctx) | 51 | log := logging.FromContext(ctx) | ||
| 52 | 52 | ||||
| 53 | + | // Replace periods with spaces before splitting - FTS5 tokenizer treats periods as separators | |||
| 54 | + | // so "gordon.bne" in URLs is indexed as "gordon" + "bne" tokens | |||
| 55 | + | // This allows searching "gordon.bne" to match URLs containing both terms | |||
| 56 | + | normalizedQuery := strings.ReplaceAll(query, ".", " ") | |||
| 57 | + | ||||
| 53 | // Split query into words and add prefix matching to each | 58 | // Split query into words and add prefix matching to each | ||
| 54 | // This enables multi-word searches like "github issues" -> "github* issues*" | 59 | // This enables multi-word searches like "github issues" -> "github* issues*" | ||
| 55 | - | words := strings.Fields(query) | 60 | + | words := strings.Fields(normalizedQuery) |
| 56 | if len(words) == 0 { | 61 | if len(words) == 0 { | ||
| 57 | return []entity.HistoryMatch{}, nil | 62 | return []entity.HistoryMatch{}, nil | ||
| 58 | } | 63 | } | ||
| @@ -66,13 +71,16 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||||
| 66 | ftsQuery := strings.Join(parts, " ") | 71 | ftsQuery := strings.Join(parts, " ") | ||
| 67 | 72 | ||||
| 68 | // Search both URL and title columns, then merge results | 73 | // Search both URL and title columns, then merge results | ||
| 69 | - | urlRows, urlErr := r.queries.SearchHistoryFTSUrl(ctx, sqlc.SearchHistoryFTSUrlParams{ | 74 | + | // Use domain-boosted query for URL search to prioritize domain matches |
| 75 | + | // Use sanitized first word for domain boost (intentional: domain boost targets first search term) | |||
| 76 | + | urlRows, urlErr := r.queries.SearchHistoryFTSUrlWithDomainBoost(ctx, sqlc.SearchHistoryFTSUrlWithDomainBoostParams{ | |||
| 77 | + | Term: sql.NullString{String: sanitizeFTS5Word(words[0]), Valid: true}, | |||
| 70 | Query: ftsQuery, | 78 | Query: ftsQuery, | ||
| 71 | Limit: int64(limit), | 79 | Limit: int64(limit), | ||
| 72 | }) | 80 | }) | ||
| 73 | if urlErr != nil { | 81 | if urlErr != nil { | ||
| 74 | log.Debug().Err(urlErr).Str("query", query).Msg("FTS URL search failed") | 82 | log.Debug().Err(urlErr).Str("query", query).Msg("FTS URL search failed") | ||
| 75 | - | urlRows = []sqlc.History{} | 83 | + | urlRows = []sqlc.SearchHistoryFTSUrlWithDomainBoostRow{} |
| 76 | } | 84 | } | ||
| 77 | 85 | ||||
| 78 | titleRows, titleErr := r.queries.SearchHistoryFTSTitle(ctx, sqlc.SearchHistoryFTSTitleParams{ | 86 | titleRows, titleErr := r.queries.SearchHistoryFTSTitle(ctx, sqlc.SearchHistoryFTSTitleParams{ | ||
| @@ -97,7 +105,7 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||||
| 97 | if !seen[row.ID] { | 105 | if !seen[row.ID] { | ||
| 98 | seen[row.ID] = true | 106 | seen[row.ID] = true | ||
| 99 | matches = append(matches, entity.HistoryMatch{ | 107 | matches = append(matches, entity.HistoryMatch{ | ||
| 100 | - | Entry: historyFromRow(row), | 108 | + | Entry: historyFromDomainBoostRow(row), |
| 101 | Score: 1.0, | 109 | Score: 1.0, | ||
| 102 | }) | 110 | }) | ||
| 103 | } | 111 | } | ||
| @@ -120,8 +128,21 @@ func (r *historyRepo) Search(ctx context.Context, query string, limit int) ([]en | |||||
| 120 | return matches, nil | 128 | return matches, nil | ||
| 121 | } | 129 | } | ||
| 122 | 130 | ||||
| 131 | + | func historyFromDomainBoostRow(row sqlc.SearchHistoryFTSUrlWithDomainBoostRow) *entity.HistoryEntry { | |||
| 132 | + | return &entity.HistoryEntry{ | |||
| 133 | + | ID: row.ID, | |||
| 134 | + | URL: row.Url, | |||
| 135 | + | Title: row.Title.String, | |||
| 136 | + | FaviconURL: row.FaviconUrl.String, | |||
| 137 | + | VisitCount: row.VisitCount.Int64, | |||
| 138 | + | LastVisited: row.LastVisited.Time, | |||
| 139 | + | CreatedAt: row.CreatedAt.Time, | |||
| 140 | + | } | |||
| 141 | + | } | |||
| 142 | + | ||||
| 123 | // sanitizeFTS5Word removes FTS5 special characters from a search word. | 143 | // sanitizeFTS5Word removes FTS5 special characters from a search word. | ||
| 124 | // FTS5 special chars: " * ( ) : ^ - AND OR NOT NEAR | 144 | // FTS5 special chars: " * ( ) : ^ - AND OR NOT NEAR | ||
| 145 | + | // Note: Periods are handled earlier by replacing with spaces (tokenizer separator) | |||
| 125 | func sanitizeFTS5Word(word string) string { | 146 | func sanitizeFTS5Word(word string) string { | ||
| 126 | // Remove characters that have special meaning in FTS5 | 147 | // Remove characters that have special meaning in FTS5 | ||
| 127 | var result strings.Builder | 148 | var result strings.Builder | ||
M internal/infrastructure/persistence/sqlite/history_repo_test.go +71 -0
| @@ -484,3 +484,74 @@ func TestHistoryRepository_Search_PrefixMatching(t *testing.T) { | |||
| 484 | 484 | assert.True(t, urls["https://github.com"]) | |
| 485 | 485 | assert.True(t, urls["https://gitlab.com"]) | |
| 486 | 486 | } | |
| 487 | + | ||
| 488 | + | func TestHistoryRepository_Search_DomainLikeQuery(t *testing.T) { | |
| 489 | + | ctx := historyTestCtx() | |
| 490 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 491 | + | ||
| 492 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 493 | + | require.NoError(t, err) | |
| 494 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 495 | + | ||
| 496 | + | repo := sqlite.NewHistoryRepository(db) | |
| 497 | + | ||
| 498 | + | // Save entries with domain-like URLs | |
| 499 | + | entries := []*entity.HistoryEntry{ | |
| 500 | + | {URL: "https://gordon.bnema.dev/", Title: "Gordon - Self-hosted Deployment Platform"}, | |
| 501 | + | {URL: "https://example.com", Title: "Example Site"}, | |
| 502 | + | {URL: "https://other.gordon.io", Title: "Another Gordon Site"}, | |
| 503 | + | } | |
| 504 | + | for _, e := range entries { | |
| 505 | + | require.NoError(t, repo.Save(ctx, e)) | |
| 506 | + | } | |
| 507 | + | ||
| 508 | + | // Search with period in query (domain-like) - periods should be treated as separators | |
| 509 | + | // "gordon.bnem" should match "gordon.bnema.dev" (tokens: gordon, bnem -> gordon, bnema) | |
| 510 | + | results, err := repo.Search(ctx, "gordon.bnem", 10) | |
| 511 | + | require.NoError(t, err) | |
| 512 | + | require.NotEmpty(t, results, "domain-like query 'gordon.bnem' should match gordon.bnema.dev") | |
| 513 | + | ||
| 514 | + | // Verify the gordon.bnema.dev entry is found | |
| 515 | + | found := false | |
| 516 | + | for _, r := range results { | |
| 517 | + | if r.Entry.URL == "https://gordon.bnema.dev/" { | |
| 518 | + | found = true | |
| 519 | + | break | |
| 520 | + | } | |
| 521 | + | } | |
| 522 | + | assert.True(t, found, "gordon.bnema.dev should be in results") | |
| 523 | + | } | |
| 524 | + | ||
| 525 | + | func TestHistoryRepository_Search_SpecialCharacters(t *testing.T) { | |
| 526 | + | ctx := historyTestCtx() | |
| 527 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |
| 528 | + | ||
| 529 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |
| 530 | + | require.NoError(t, err) | |
| 531 | + | t.Cleanup(func() { _ = db.Close() }) | |
| 532 | + | ||
| 533 | + | repo := sqlite.NewHistoryRepository(db) | |
| 534 | + | ||
| 535 | + | // Save entry | |
| 536 | + | require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{ | |
| 537 | + | URL: "https://example.com/path", | |
| 538 | + | Title: "Example Page", | |
| 539 | + | })) | |
| 540 | + | ||
| 541 | + | // Search with FTS5 special characters - should not cause errors | |
| 542 | + | specialQueries := []string{ | |
| 543 | + | "example:", // colon | |
| 544 | + | "example*", // asterisk | |
| 545 | + | "example()", // parentheses | |
| 546 | + | `example"test`, // quotes | |
| 547 | + | "example^", // caret | |
| 548 | + | "example-test", // hyphen | |
| 549 | + | } | |
| 550 | + | ||
| 551 | + | for _, q := range specialQueries { | |
| 552 | + | results, err := repo.Search(ctx, q, 10) | |
| 553 | + | require.NoError(t, err, "query %q should not cause error", q) | |
| 554 | + | // May or may not find results, but should not error | |
| 555 | + | _ = results | |
| 556 | + | } | |
| 557 | + | } | |
| @@ -484,3 +484,74 @@ func TestHistoryRepository_Search_PrefixMatching(t *testing.T) { | |||||
| 484 | assert.True(t, urls["https://github.com"]) | 484 | assert.True(t, urls["https://github.com"]) | ||
| 485 | assert.True(t, urls["https://gitlab.com"]) | 485 | assert.True(t, urls["https://gitlab.com"]) | ||
| 486 | } | 486 | } | ||
| 487 | + | ||||
| 488 | + | func TestHistoryRepository_Search_DomainLikeQuery(t *testing.T) { | |||
| 489 | + | ctx := historyTestCtx() | |||
| 490 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 491 | + | ||||
| 492 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 493 | + | require.NoError(t, err) | |||
| 494 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 495 | + | ||||
| 496 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 497 | + | ||||
| 498 | + | // Save entries with domain-like URLs | |||
| 499 | + | entries := []*entity.HistoryEntry{ | |||
| 500 | + | {URL: "https://gordon.bnema.dev/", Title: "Gordon - Self-hosted Deployment Platform"}, | |||
| 501 | + | {URL: "https://example.com", Title: "Example Site"}, | |||
| 502 | + | {URL: "https://other.gordon.io", Title: "Another Gordon Site"}, | |||
| 503 | + | } | |||
| 504 | + | for _, e := range entries { | |||
| 505 | + | require.NoError(t, repo.Save(ctx, e)) | |||
| 506 | + | } | |||
| 507 | + | ||||
| 508 | + | // Search with period in query (domain-like) - periods should be treated as separators | |||
| 509 | + | // "gordon.bnem" should match "gordon.bnema.dev" (tokens: gordon, bnem -> gordon, bnema) | |||
| 510 | + | results, err := repo.Search(ctx, "gordon.bnem", 10) | |||
| 511 | + | require.NoError(t, err) | |||
| 512 | + | require.NotEmpty(t, results, "domain-like query 'gordon.bnem' should match gordon.bnema.dev") | |||
| 513 | + | ||||
| 514 | + | // Verify the gordon.bnema.dev entry is found | |||
| 515 | + | found := false | |||
| 516 | + | for _, r := range results { | |||
| 517 | + | if r.Entry.URL == "https://gordon.bnema.dev/" { | |||
| 518 | + | found = true | |||
| 519 | + | break | |||
| 520 | + | } | |||
| 521 | + | } | |||
| 522 | + | assert.True(t, found, "gordon.bnema.dev should be in results") | |||
| 523 | + | } | |||
| 524 | + | ||||
| 525 | + | func TestHistoryRepository_Search_SpecialCharacters(t *testing.T) { | |||
| 526 | + | ctx := historyTestCtx() | |||
| 527 | + | dbPath := filepath.Join(t.TempDir(), "dumber.db") | |||
| 528 | + | ||||
| 529 | + | db, err := sqlite.NewConnection(ctx, dbPath) | |||
| 530 | + | require.NoError(t, err) | |||
| 531 | + | t.Cleanup(func() { _ = db.Close() }) | |||
| 532 | + | ||||
| 533 | + | repo := sqlite.NewHistoryRepository(db) | |||
| 534 | + | ||||
| 535 | + | // Save entry | |||
| 536 | + | require.NoError(t, repo.Save(ctx, &entity.HistoryEntry{ | |||
| 537 | + | URL: "https://example.com/path", | |||
| 538 | + | Title: "Example Page", | |||
| 539 | + | })) | |||
| 540 | + | ||||
| 541 | + | // Search with FTS5 special characters - should not cause errors | |||
| 542 | + | specialQueries := []string{ | |||
| 543 | + | "example:", // colon | |||
| 544 | + | "example*", // asterisk | |||
| 545 | + | "example()", // parentheses | |||
| 546 | + | `example"test`, // quotes | |||
| 547 | + | "example^", // caret | |||
| 548 | + | "example-test", // hyphen | |||
| 549 | + | } | |||
| 550 | + | ||||
| 551 | + | for _, q := range specialQueries { | |||
| 552 | + | results, err := repo.Search(ctx, q, 10) | |||
| 553 | + | require.NoError(t, err, "query %q should not cause error", q) | |||
| 554 | + | // May or may not find results, but should not error | |||
| 555 | + | _ = results | |||
| 556 | + | } | |||
| 557 | + | } | |||
M internal/infrastructure/persistence/sqlite/queries/history.sql +15 -0
| @@ -57,6 +57,21 @@ WHERE fts.url MATCH @query | |||
| 57 | 57 | ORDER BY h.visit_count DESC, h.last_visited DESC | |
| 58 | 58 | LIMIT @limit; | |
| 59 | 59 | ||
| 60 | + | -- name: SearchHistoryFTSUrlWithDomainBoost :many | |
| 61 | + | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at, | |
| 62 | + | CASE | |
| 63 | + | WHEN h.url LIKE '%://' || @term || '.%' THEN 2 | |
| 64 | + | WHEN h.url LIKE '%://%.' || @term || '.%' THEN 2 | |
| 65 | + | WHEN h.url LIKE '%://' || @term || '/%' THEN 2 | |
| 66 | + | WHEN h.url LIKE '%://%.' || @term || '/%' THEN 1 | |
| 67 | + | ELSE 0 | |
| 68 | + | END as domain_boost | |
| 69 | + | FROM history_fts fts | |
| 70 | + | JOIN history h ON fts.rowid = h.id | |
| 71 | + | WHERE fts.url MATCH @query | |
| 72 | + | ORDER BY domain_boost DESC, h.visit_count DESC, h.last_visited DESC | |
| 73 | + | LIMIT @limit; | |
| 74 | + | ||
| 60 | 75 | -- name: SearchHistoryFTSTitle :many | |
| 61 | 76 | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at | |
| 62 | 77 | FROM history_fts fts | |
| @@ -57,6 +57,21 @@ WHERE fts.url MATCH @query | |||||
| 57 | ORDER BY h.visit_count DESC, h.last_visited DESC | 57 | ORDER BY h.visit_count DESC, h.last_visited DESC | ||
| 58 | LIMIT @limit; | 58 | LIMIT @limit; | ||
| 59 | 59 | ||||
| 60 | + | -- name: SearchHistoryFTSUrlWithDomainBoost :many | |||
| 61 | + | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at, | |||
| 62 | + | CASE | |||
| 63 | + | WHEN h.url LIKE '%://' || @term || '.%' THEN 2 | |||
| 64 | + | WHEN h.url LIKE '%://%.' || @term || '.%' THEN 2 | |||
| 65 | + | WHEN h.url LIKE '%://' || @term || '/%' THEN 2 | |||
| 66 | + | WHEN h.url LIKE '%://%.' || @term || '/%' THEN 1 | |||
| 67 | + | ELSE 0 | |||
| 68 | + | END as domain_boost | |||
| 69 | + | FROM history_fts fts | |||
| 70 | + | JOIN history h ON fts.rowid = h.id | |||
| 71 | + | WHERE fts.url MATCH @query | |||
| 72 | + | ORDER BY domain_boost DESC, h.visit_count DESC, h.last_visited DESC | |||
| 73 | + | LIMIT @limit; | |||
| 74 | + | ||||
| 60 | -- name: SearchHistoryFTSTitle :many | 75 | -- name: SearchHistoryFTSTitle :many | ||
| 61 | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at | 76 | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at | ||
| 62 | FROM history_fts fts | 77 | FROM history_fts fts | ||
M internal/infrastructure/persistence/sqlite/sqlc/history.sql.go +65 -0
| @@ -529,6 +529,71 @@ func (q *Queries) SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSU | |||
| 529 | 529 | return items, nil | |
| 530 | 530 | } | |
| 531 | 531 | ||
| 532 | + | const SearchHistoryFTSUrlWithDomainBoost = `-- name: SearchHistoryFTSUrlWithDomainBoost :many | |
| 533 | + | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at, | |
| 534 | + | CASE | |
| 535 | + | WHEN h.url LIKE '%://' || ?1 || '.%' THEN 2 | |
| 536 | + | WHEN h.url LIKE '%://%.' || ?1 || '.%' THEN 2 | |
| 537 | + | WHEN h.url LIKE '%://' || ?1 || '/%' THEN 2 | |
| 538 | + | WHEN h.url LIKE '%://%.' || ?1 || '/%' THEN 1 | |
| 539 | + | ELSE 0 | |
| 540 | + | END as domain_boost | |
| 541 | + | FROM history_fts fts | |
| 542 | + | JOIN history h ON fts.rowid = h.id | |
| 543 | + | WHERE fts.url MATCH ?2 | |
| 544 | + | ORDER BY domain_boost DESC, h.visit_count DESC, h.last_visited DESC | |
| 545 | + | LIMIT ?3 | |
| 546 | + | ` | |
| 547 | + | ||
| 548 | + | type SearchHistoryFTSUrlWithDomainBoostParams struct { | |
| 549 | + | Term sql.NullString `json:"term"` | |
| 550 | + | Query string `json:"query"` | |
| 551 | + | Limit int64 `json:"limit"` | |
| 552 | + | } | |
| 553 | + | ||
| 554 | + | type SearchHistoryFTSUrlWithDomainBoostRow struct { | |
| 555 | + | ID int64 `json:"id"` | |
| 556 | + | Url string `json:"url"` | |
| 557 | + | Title sql.NullString `json:"title"` | |
| 558 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |
| 559 | + | VisitCount sql.NullInt64 `json:"visit_count"` | |
| 560 | + | LastVisited sql.NullTime `json:"last_visited"` | |
| 561 | + | CreatedAt sql.NullTime `json:"created_at"` | |
| 562 | + | DomainBoost int64 `json:"domain_boost"` | |
| 563 | + | } | |
| 564 | + | ||
| 565 | + | func (q *Queries) SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error) { | |
| 566 | + | rows, err := q.db.QueryContext(ctx, SearchHistoryFTSUrlWithDomainBoost, arg.Term, arg.Query, arg.Limit) | |
| 567 | + | if err != nil { | |
| 568 | + | return nil, err | |
| 569 | + | } | |
| 570 | + | defer rows.Close() | |
| 571 | + | items := []SearchHistoryFTSUrlWithDomainBoostRow{} | |
| 572 | + | for rows.Next() { | |
| 573 | + | var i SearchHistoryFTSUrlWithDomainBoostRow | |
| 574 | + | if err := rows.Scan( | |
| 575 | + | &i.ID, | |
| 576 | + | &i.Url, | |
| 577 | + | &i.Title, | |
| 578 | + | &i.FaviconUrl, | |
| 579 | + | &i.VisitCount, | |
| 580 | + | &i.LastVisited, | |
| 581 | + | &i.CreatedAt, | |
| 582 | + | &i.DomainBoost, | |
| 583 | + | ); err != nil { | |
| 584 | + | return nil, err | |
| 585 | + | } | |
| 586 | + | items = append(items, i) | |
| 587 | + | } | |
| 588 | + | if err := rows.Close(); err != nil { | |
| 589 | + | return nil, err | |
| 590 | + | } | |
| 591 | + | if err := rows.Err(); err != nil { | |
| 592 | + | return nil, err | |
| 593 | + | } | |
| 594 | + | return items, nil | |
| 595 | + | } | |
| 596 | + | ||
| 532 | 597 | const UpsertHistory = `-- name: UpsertHistory :exec | |
| 533 | 598 | INSERT INTO history (url, title, favicon_url) | |
| 534 | 599 | VALUES (?, ?, ?) | |
| @@ -529,6 +529,71 @@ func (q *Queries) SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSU | |||||
| 529 | return items, nil | 529 | return items, nil | ||
| 530 | } | 530 | } | ||
| 531 | 531 | ||||
| 532 | + | const SearchHistoryFTSUrlWithDomainBoost = `-- name: SearchHistoryFTSUrlWithDomainBoost :many | |||
| 533 | + | SELECT h.id, h.url, h.title, h.favicon_url, h.visit_count, h.last_visited, h.created_at, | |||
| 534 | + | CASE | |||
| 535 | + | WHEN h.url LIKE '%://' || ?1 || '.%' THEN 2 | |||
| 536 | + | WHEN h.url LIKE '%://%.' || ?1 || '.%' THEN 2 | |||
| 537 | + | WHEN h.url LIKE '%://' || ?1 || '/%' THEN 2 | |||
| 538 | + | WHEN h.url LIKE '%://%.' || ?1 || '/%' THEN 1 | |||
| 539 | + | ELSE 0 | |||
| 540 | + | END as domain_boost | |||
| 541 | + | FROM history_fts fts | |||
| 542 | + | JOIN history h ON fts.rowid = h.id | |||
| 543 | + | WHERE fts.url MATCH ?2 | |||
| 544 | + | ORDER BY domain_boost DESC, h.visit_count DESC, h.last_visited DESC | |||
| 545 | + | LIMIT ?3 | |||
| 546 | + | ` | |||
| 547 | + | ||||
| 548 | + | type SearchHistoryFTSUrlWithDomainBoostParams struct { | |||
| 549 | + | Term sql.NullString `json:"term"` | |||
| 550 | + | Query string `json:"query"` | |||
| 551 | + | Limit int64 `json:"limit"` | |||
| 552 | + | } | |||
| 553 | + | ||||
| 554 | + | type SearchHistoryFTSUrlWithDomainBoostRow struct { | |||
| 555 | + | ID int64 `json:"id"` | |||
| 556 | + | Url string `json:"url"` | |||
| 557 | + | Title sql.NullString `json:"title"` | |||
| 558 | + | FaviconUrl sql.NullString `json:"favicon_url"` | |||
| 559 | + | VisitCount sql.NullInt64 `json:"visit_count"` | |||
| 560 | + | LastVisited sql.NullTime `json:"last_visited"` | |||
| 561 | + | CreatedAt sql.NullTime `json:"created_at"` | |||
| 562 | + | DomainBoost int64 `json:"domain_boost"` | |||
| 563 | + | } | |||
| 564 | + | ||||
| 565 | + | func (q *Queries) SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error) { | |||
| 566 | + | rows, err := q.db.QueryContext(ctx, SearchHistoryFTSUrlWithDomainBoost, arg.Term, arg.Query, arg.Limit) | |||
| 567 | + | if err != nil { | |||
| 568 | + | return nil, err | |||
| 569 | + | } | |||
| 570 | + | defer rows.Close() | |||
| 571 | + | items := []SearchHistoryFTSUrlWithDomainBoostRow{} | |||
| 572 | + | for rows.Next() { | |||
| 573 | + | var i SearchHistoryFTSUrlWithDomainBoostRow | |||
| 574 | + | if err := rows.Scan( | |||
| 575 | + | &i.ID, | |||
| 576 | + | &i.Url, | |||
| 577 | + | &i.Title, | |||
| 578 | + | &i.FaviconUrl, | |||
| 579 | + | &i.VisitCount, | |||
| 580 | + | &i.LastVisited, | |||
| 581 | + | &i.CreatedAt, | |||
| 582 | + | &i.DomainBoost, | |||
| 583 | + | ); err != nil { | |||
| 584 | + | return nil, err | |||
| 585 | + | } | |||
| 586 | + | items = append(items, i) | |||
| 587 | + | } | |||
| 588 | + | if err := rows.Close(); err != nil { | |||
| 589 | + | return nil, err | |||
| 590 | + | } | |||
| 591 | + | if err := rows.Err(); err != nil { | |||
| 592 | + | return nil, err | |||
| 593 | + | } | |||
| 594 | + | return items, nil | |||
| 595 | + | } | |||
| 596 | + | ||||
| 532 | const UpsertHistory = `-- name: UpsertHistory :exec | 597 | const UpsertHistory = `-- name: UpsertHistory :exec | ||
| 533 | INSERT INTO history (url, title, favicon_url) | 598 | INSERT INTO history (url, title, favicon_url) | ||
| 534 | VALUES (?, ?, ?) | 599 | VALUES (?, ?, ?) | ||
M internal/infrastructure/persistence/sqlite/sqlc/querier.go +1 -0
| @@ -72,6 +72,7 @@ type Querier interface { | |||
| 72 | 72 | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | |
| 73 | 73 | SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error) | |
| 74 | 74 | SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error) | |
| 75 | + | SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error) | |
| 75 | 76 | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | |
| 76 | 77 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | |
| 77 | 78 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | |
| @@ -72,6 +72,7 @@ type Querier interface { | |||||
| 72 | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | 72 | SearchHistory(ctx context.Context, arg SearchHistoryParams) ([]History, error) | ||
| 73 | SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error) | 73 | SearchHistoryFTSTitle(ctx context.Context, arg SearchHistoryFTSTitleParams) ([]History, error) | ||
| 74 | SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error) | 74 | SearchHistoryFTSUrl(ctx context.Context, arg SearchHistoryFTSUrlParams) ([]History, error) | ||
| 75 | + | SearchHistoryFTSUrlWithDomainBoost(ctx context.Context, arg SearchHistoryFTSUrlWithDomainBoostParams) ([]SearchHistoryFTSUrlWithDomainBoostRow, error) | |||
| 75 | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | 76 | SetFavoriteFolder(ctx context.Context, arg SetFavoriteFolderParams) error | ||
| 76 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | 77 | SetFavoriteShortcut(ctx context.Context, arg SetFavoriteShortcutParams) error | ||
| 77 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | 78 | SetZoomLevel(ctx context.Context, arg SetZoomLevelParams) error | ||
M internal/ui/component/omnibox.go +13 -19
| @@ -1272,21 +1272,21 @@ func (o *Omnibox) navigateToSelected() { | |||
| 1272 | 1272 | ||
| 1273 | 1273 | var targetURL string | |
| 1274 | 1274 | ||
| 1275 | - | // Check if user typed a URL-like string (contains . and no spaces) | |
| 1276 | - | // If so, navigate to that URL directly instead of the selection | |
| 1277 | - | if o.looksLikeURL(entryText) { | |
| 1278 | - | targetURL = o.buildURL(entryText) | |
| 1279 | - | } else if idx < 0 { | |
| 1280 | - | // No selection - use entry text as URL/search | |
| 1281 | - | targetURL = o.buildURL(entryText) | |
| 1282 | - | } else if mode == ViewModeHistory { | |
| 1283 | - | if idx < len(suggestions) { | |
| 1284 | - | targetURL = suggestions[idx].URL | |
| 1275 | + | // If user has selected a result, navigate to that result | |
| 1276 | + | // Otherwise, treat entry text as URL/search | |
| 1277 | + | if idx >= 0 { | |
| 1278 | + | if mode == ViewModeHistory { | |
| 1279 | + | if idx < len(suggestions) { | |
| 1280 | + | targetURL = suggestions[idx].URL | |
| 1281 | + | } | |
| 1282 | + | } else { | |
| 1283 | + | if idx < len(favorites) { | |
| 1284 | + | targetURL = favorites[idx].URL | |
| 1285 | + | } | |
| 1285 | 1286 | } | |
| 1286 | 1287 | } else { | |
| 1287 | - | if idx < len(favorites) { | |
| 1288 | - | targetURL = favorites[idx].URL | |
| 1289 | - | } | |
| 1288 | + | // No selection - use entry text as URL/search | |
| 1289 | + | targetURL = o.buildURL(entryText) | |
| 1290 | 1290 | } | |
| 1291 | 1291 | ||
| 1292 | 1292 | if targetURL == "" { | |
| @@ -1300,12 +1300,6 @@ func (o *Omnibox) navigateToSelected() { | |||
| 1300 | 1300 | } | |
| 1301 | 1301 | } | |
| 1302 | 1302 | ||
| 1303 | - | // looksLikeURL checks if the text appears to be a URL (not a search query). | |
| 1304 | - | // Returns true for strings like "github.com", "google.com/search", etc. | |
| 1305 | - | func (o *Omnibox) looksLikeURL(text string) bool { | |
| 1306 | - | return url.LooksLikeURL(text) | |
| 1307 | - | } | |
| 1308 | - | ||
| 1309 | 1303 | // toggleFavorite adds or removes the selected item from favorites. | |
| 1310 | 1304 | // In History mode: adds the selected item to favorites | |
| 1311 | 1305 | // In Favorites mode: removes the selected item from favorites | |
| @@ -1272,21 +1272,21 @@ func (o *Omnibox) navigateToSelected() { | |||||
| 1272 | 1272 | ||||
| 1273 | var targetURL string | 1273 | var targetURL string | ||
| 1274 | 1274 | ||||
| 1275 | - | // Check if user typed a URL-like string (contains . and no spaces) | 1275 | + | // If user has selected a result, navigate to that result |
| 1276 | - | // If so, navigate to that URL directly instead of the selection | 1276 | + | // Otherwise, treat entry text as URL/search |
| 1277 | - | if o.looksLikeURL(entryText) { | 1277 | + | if idx >= 0 { |
| 1278 | - | targetURL = o.buildURL(entryText) | 1278 | + | if mode == ViewModeHistory { |
| 1279 | - | } else if idx < 0 { | 1279 | + | if idx < len(suggestions) { |
| 1280 | - | // No selection - use entry text as URL/search | 1280 | + | targetURL = suggestions[idx].URL |
| 1281 | - | targetURL = o.buildURL(entryText) | 1281 | + | } |
| 1282 | - | } else if mode == ViewModeHistory { | 1282 | + | } else { |
| 1283 | - | if idx < len(suggestions) { | 1283 | + | if idx < len(favorites) { |
| 1284 | - | targetURL = suggestions[idx].URL | 1284 | + | targetURL = favorites[idx].URL |
| 1285 | + | } | |||
| 1285 | } | 1286 | } | ||
| 1286 | } else { | 1287 | } else { | ||
| 1287 | - | if idx < len(favorites) { | 1288 | + | // No selection - use entry text as URL/search |
| 1288 | - | targetURL = favorites[idx].URL | 1289 | + | targetURL = o.buildURL(entryText) |
| 1289 | - | } | |||
| 1290 | } | 1290 | } | ||
| 1291 | 1291 | ||||
| 1292 | if targetURL == "" { | 1292 | if targetURL == "" { | ||
| @@ -1300,12 +1300,6 @@ func (o *Omnibox) navigateToSelected() { | |||||
| 1300 | } | 1300 | } | ||
| 1301 | } | 1301 | } | ||
| 1302 | 1302 | ||||
| 1303 | - | // looksLikeURL checks if the text appears to be a URL (not a search query). | |||
| 1304 | - | // Returns true for strings like "github.com", "google.com/search", etc. | |||
| 1305 | - | func (o *Omnibox) looksLikeURL(text string) bool { | |||
| 1306 | - | return url.LooksLikeURL(text) | |||
| 1307 | - | } | |||
| 1308 | - | ||||
| 1309 | // toggleFavorite adds or removes the selected item from favorites. | 1303 | // toggleFavorite adds or removes the selected item from favorites. | ||
| 1310 | // In History mode: adds the selected item to favorites | 1304 | // In History mode: adds the selected item to favorites | ||
| 1311 | // In Favorites mode: removes the selected item from favorites | 1305 | // In Favorites mode: removes the selected item from favorites | ||