Skip to content

Surface the search filters as controls, extract snippets in the engine, and fix the test suite on modern Node - #226

Open
franciscoxc wants to merge 5 commits into
cardisoft:masterfrom
franciscoxc:upstream-visible-search
Open

Surface the search filters as controls, extract snippets in the engine, and fix the test suite on modern Node#226
franciscoxc wants to merge 5 commits into
cardisoft:masterfrom
franciscoxc:upstream-visible-search

Conversation

@franciscoxc

Copy link
Copy Markdown

Cardinal's engine is excellent and almost none of it is visible: nothing in the window suggests that type:image, content:"…" or in: exist. This PR adds controls for the common filters, plus a fix for the test suite, which is currently red on any recent Node.

I maintain a fork, but this branch carries none of it: no bundle identifier, signing config, icon, version bump, README or CHANGELOG change. tauri.conf.json and every icon file are untouched. It is only the parts that seemed useful to Cardinal itself. Happy to split it into smaller PRs if that is easier to review.

1. The test suite is red on Node 22+ (7c8eedb)

51 tests fail with Cannot read properties of undefined (reading 'clear'). Node 22+ defines its own global localStorage accessor that returns undefined unless --localstorage-file is passed, and Vitest's populateGlobal skips copying a jsdom key when one already exists on globalThis unless it is in an internal allowlist — navigator is on it, localStorage and sessionStorage are not. So jsdom's storage never reaches window. CI stays green only because it pins Node 20, which has no such global.

The test script now passes --no-experimental-webstorage, and CI moves to Node 24 since Node 20 rejects that flag outright (and went EOL in April 2026).

Same commit fixes AGENTS.md, which names nightly-2025-05-09 while rust-toolchain.toml pins nightly-2025-12-11. Following the doc and putting that older toolchain on PATH builds fine from a warm target/, then fails as soon as a proc-macro has to be rebuilt: rustc 1.88 emits dylibs that recent macOS dyld rejects with mis-aligned LINKEDIT string pool. That cost me an afternoon.

2. Content snippets in the engine, and type:email (30ee8b3)

To show why a content: search matched, the app needs the text around the match. Rather than a second scanner in the app, content_snippet sits next to node_content_matches in search-cache and shares its chunked read and Rabin-Karp finder, so a snippet is found wherever the filter found a match. Case folding goes to a scratch buffer instead of in place, because the snippet is cut from those same bytes.

The highlighted terms come from content_terms_of_query, which walks the parsed query instead of re-parsing the text in the UI. Two behaviours fall out of using the real parser: a negated !content: no longer highlights anything, and content:"Bearer " keeps its trailing space.

type:email covers .eml, .emlx, .emlxpart, .msg and .mbox, with mail/message as synonyms. Apple Mail's own formats are the ones nobody finds by name.

3. Filters as controls in the search bar (f3f67b5)

The rule these follow: a control edits the query text, it does not hold filter state of its own. So there is no second filtering path to keep in sync, the search bar stays the single source of truth, and the syntax stays visible — the user sees type:image appear and learns it.

  • File-type dropdown — Image, Video, Audio, Document, Email, Archive, Code, App, Folder. "Document" writes an OR of doc/pdf/presentation/spreadsheet rather than widening type:doc, which would silently change that filter for everyone.
  • A "Contains" field writing content:"…", beside the one for names.
  • A context column with the matching text from inside each file, term highlighted.
  • Reorderable columns, dragging their titles. The order is stored, and repaired on read so a column added later still appears.
  • The folder icon opens a folder picker. It used to fold the field away, which is not what an icon shaped like a folder promises; folding moved to a chevron beside it. This adds tauri-plugin-dialog with only dialog:allow-open.
  • The scope placeholder names its default: "Search in… (whole disk)".

Each control reads back only the token it authored. A query it cannot represent faithfully — negated, duplicated, or inside a boolean group — reads as "custom" and is left untouched rather than misreported.

One thing to push back on: renaming "folder scope" to "search in" across the 15 locales is opinionated. It is confined to the i18n JSON files and trivial to drop if you would rather keep the original term.

Checks

  • npm test — 34 files, 292 tests, default pool and parallelism
  • npm run typecheck, npx prettier --check .
  • cargo test -p search-cache — 880 + the integration suites
  • cargo check for the app, with Cargo.lock regenerated on the current version

New tests cover the snippet scanner across a chunk boundary, type:email and its aliases, the query-token round trip for both controls, and the column-order storage.

franciscoxc and others added 5 commits August 2, 2026 08:20
… pinned toolchain

Node 22+ defines its own global `localStorage` accessor that returns
undefined unless `--localstorage-file` is passed. Vitest's populateGlobal
skips copying a jsdom key when one already exists on globalThis unless it is
in an internal allowlist, and `localStorage`/`sessionStorage` are not on it
(`navigator` is). So jsdom's storage never reaches window, and every test
touching it fails — 51 of them — while CI on Node 20 stays green. CI moves
to Node 24 because Node 20 rejects the flag outright, and reached end of
life in April 2026.

AGENTS.md also named nightly-2025-05-09 while rust-toolchain.toml pins
nightly-2025-12-11. Following the doc and forcing the old toolchain onto
PATH builds fine from a warm target dir, then fails the moment a proc-macro
has to be rebuilt: rustc 1.88 emits dylibs that recent macOS dyld rejects
with "mis-aligned LINKEDIT string pool".
…ype:email

The app was reimplementing the content scan to show a snippet: a second
chunked reader with a naive substring search, next to `node_content_matches`
which already does it with a Rabin-Karp finder. `content_snippet` now lives
beside that matcher and shares its chunking, so a snippet is found wherever
the filter found a match. Case folding goes to a scratch buffer rather than
in place, because the snippet is cut from those same bytes.

The highlighted terms come from `content_terms_of_query`, which walks the
parsed query rather than re-parsing the text in the UI. Two behaviours fall
out of using the real parser: a negated `!content:` no longer highlights
anything, and `content:"Bearer "` keeps its trailing space.

`type:email` covers .eml, .emlx, .emlxpart, .msg and .mbox, with mail and
message as synonyms. Apple Mail's formats are the ones nobody finds by name.
Cardinal's syntax is powerful and undiscoverable: nothing in the window
suggests that `type:image` or `content:"…"` exist. These controls do not add
a second filtering path — each one edits the query text, so the search bar
stays the single source of truth and the syntax it writes stays visible and
learnable.

- A file-type dropdown: Image, Video, Audio, Document, Email, Archive, Code,
  App, Folder. "Document" writes an OR of doc/pdf/presentation/spreadsheet
  rather than widening `type:doc`, which would change that filter's meaning.
- A "Contains" field writing `content:"…"`.
- A context column with the matching text from inside each file, so a
  content search shows why every result is there.
- Columns reorderable by dragging their titles, snippet next to the name.
- The folder icon in the scope field opens a folder picker; folding moved to
  a chevron beside it. The icon looked like "choose a folder" and folded the
  field instead.
- The scope placeholder names its default: "Search in… (whole disk)".

Each control reads back only the token it authored; a query it cannot
represent faithfully (negated, duplicated, or inside a boolean group) reads
as "custom" and is left alone rather than misreported.

The wording change from "folder scope" to "search in" is opinionated and
easy to drop if you would rather keep the original term.
…lves

Adding tauri-plugin-dialog pulled @tauri-apps/api up to 2.11.1, while the
crate stayed on 2.10.3. `tauri build` refuses that combination outright:
"Found version mismatched Tauri packages". `cargo check` does not, so the
mismatch only shows up when you package.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@franciscoxc

Copy link
Copy Markdown
Author

CI was red on two checks. One was mine, one was not.

Tauri fmt — my content_terms line needed breaking. Fixed in d1748ee.

Rust testtest_globstar_dedup_trailing_expansion is flaky on master, independent of this PR. It intermittently returns the search root itself (a) alongside its descendants:

left:  ["a", "a/a", "a/a/file.txt", "a/file.txt"]
right: ["a/a", "a/a/file.txt", "a/file.txt"]

Running the test binary 400 times on each tree:

tree failures in 400
master (4c50734), unmodified 5
this PR's branch 7

Same rate within noise, and this PR touches no globstar code — its search-cache changes are the content-snippet extractor, content_terms_of_query, and the type:email group. Happy to open a separate issue for it if that is useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant