Adding a Search Filter
The search bar dispatches on the query string in
src/ui/search/filter.rs::handle_update_filter. Each query syntax is a
parser that either matches and returns a modified view, or returns
None and lets the next parser try. Order of evaluation is load-bearing
and is not enforced by any test.
Read src/ui/search/filter.rs in full before starting.
Checklist
-
src/utils/search/<syntax>.rs- new parser module, if the syntax is non-trivial. Follow the shape ofsize.rsortag.rs: aparse_<syntax>(query: &str) -> Option<(Payload, String)>that returns the parsed payload and the remainder of the query. The parser is pure (no I/O, no allocator stress) and unit-tested in the same file under#[cfg(test)]. -
src/utils/search/mod.rs-mod <syntax>;andpub use <syntax>::parse_<syntax>;. -
src/ui/search/filter.rs- insert the guard inhandle_update_filterat the correct position in the chain. The current order is:empty → ':' (content search) → '*' or '?' (glob/walk) → # / :tag: / :t: (tag filter) → >/< / .. (size filter) → fuzzy matchInsert your guard before the more permissive parsers:
- If your syntax uses a prefix character that could also appear in a
filename (e.g.
date:), insert it before the fuzzy matcher but after any parser that uses a conflicting prefix. - If your syntax uses a prefix character that cannot appear in a
filename (e.g.
@), you may insert it earlier, but still after the:content-search guard, because users type:to switch into content search and any preceding parser that matches a bare:breaks it. - Never insert your parser after the fuzzy matcher. The fuzzy
matcher returns
truefor almost everything, so anything after it is unreachable.
- If your syntax uses a prefix character that could also appear in a
filename (e.g.
-
src/utils/search/doc.rs- add the new syntax torender_search_markdown. The doc is regenerated intodocs/search.mdbycargo run --bin gen-docs, andcrate::utils::search::tests::doc_is_in_syncfails CI if the committed file drifts. If the syntax accepts units or a fixed set of operators (likesize.rs'sACCEPTED_SIZE_UNITS), expose the table as apub constso the doc and the parser share one source of truth. -
docs/search.md- do not edit by hand. Runcargo run --bin gen-docs. CI regenerates it. -
po/pt_BR.po- any new user-facing message (e.g. "Invalid date filter") the parser surfaces through a toast.
Parser shape
A parser that is well-behaved under the dispatch chain:
- is case-insensitive (query is already lowercased by the caller)
- strips the prefix it consumed and returns the remainder untouched so the next parser can run on it
- returns
Nonefor malformed input (e.g.>10without a unit), so the fuzzy matcher can treat the whole string as a filename - never returnsSomewith a partial parse - never returns an error to the UI, if it cannot parse, it does not match and the next parser runs.
The size parser in src/utils/search/size.rs is the smallest complete
example. The tag parser in tag.rs shows how to expose a table
(TAG_PREFIXES) that both the parser and the doc renderer read.
Where the reference code lives
src/ui/search/filter.rs- the dispatch chain. Read every branch the wildcard/walk branch is the most subtle, because it kicks off a recursive walk (viaAppMsg::StartAdvancedSearch) rather than filtering in place.src/utils/search/size.rs- the smallest full parser. Good template if the syntax has a prefix and a numeric payload.src/utils/search/tag.rs- the prefix-set parser. Good template if the syntax accepts one of several prefixes.src/utils/search/content_query.rs- the multi-segment parser (:ext:term). Good template if the query has a second:in it.src/utils/search/doc.rs- the doc renderer. Everypub consttable referenced by a generated table indocs/search.mdis defined here.
Behaviour rules
- Case-insensitivity is the caller's job. Do not call
to_lowercase()inside a parser. The caller passes a lowercased query, assume it. - Return the remainder, never re-parse it. If your syntax is
foo:bar baz, return the payloadbarand the remainderbaz. Do not attempt to parsebazinside your parser - the dispatch chain will run it through the next parser in order. - Do not touch
self.filterbefore returning.handle_update_filterassignsself.filter = query.clone()at the branch that handles your syntax. Doing it inside the parser would race with the outer branch. - Empty remainder is fine.
>10MBalone should returnSome(op, ""), andhandle_update_filter'sparse_size_filterbranch handles the empty remainder (see the existing code - it filters the current view against the size alone). - A query that starts with your prefix but does not parse must return
None.>10(no unit) is not a size filter, it is a filename that happens to start with>. If your parser returnsSomefor a partial match, the fuzzy matcher never gets a chance and the file literally named>10becomes unreachable. - The
:prefix is reserved.parse_content_search_queryclaims anything that starts with:and does not start with:tag:or:t:. Do not add a syntax that starts with:without extending the content-query guard. - The
#prefix is reserved.parse_tag_filterclaims#. Do not add a syntax that starts with#.
Verifying the result
After wiring up the filter, it should:
- trigger for its prefix (
cargo test --lib utils::search::covers the parser) - not trigger for a filename that happens to start with the same
characters but does not parse (
>10must fall through to fuzzy) - preserve the remainder for the next parser (
@foo barwith an@foofilter should leavebarto the fuzzy matcher) - appear in
docs/search.mdaftercargo run --bin gen-docs - pass
cargo test --lib utils::search::tests::doc_is_in_sync - not break
:termcontent search,#tagfiltering,>sizefiltering, or wildcard walks - run each of those queries manually before committing.