Skip to main content

justerm_core/
search.rs

1//! Search results. The engine-side entry points are [`crate::Engine::search`] /
2//! [`crate::Engine::search_with`]; the consumer owns the query *policy* (ADR-0017).
3//!
4//! A `Match` is an inclusive range in **absolute buffer coordinates** (a line
5//! index into `[scrollback ++ screen]`, the same coordinate the selection model
6//! uses). The engine finds matches; the consumer drives next/prev navigation
7//! (holding the `Vec<Match>` and calling `scroll_to_match`), mirroring
8//! Alacritty's "engine finds, frontend navigates" split.
9
10/// Whether `pattern` is a regex [`Term::search_with`](crate::Term::search_with) can run
11/// (`opts.regex = true`) — a `true` guarantees `search_with` will *build* the pattern, and a
12/// `false` is exactly the case it silently swallows into an empty result (#316 D2).
13///
14/// Validated under **case-insensitive** compilation, the most expansive: Unicode case-folding
15/// grows the compiled program, so a `true` here holds whichever case mode smart-case / the
16/// `case_sensitive` override later picks for the search. A case-*sensitive*-only check could pass
17/// a pattern that then exceeds the `regex` size limit under `search_with`'s case-insensitive build
18/// (`CompiledTooBig`), reintroducing the silent swallow for an all-lowercase near-limit pattern.
19/// Grammar validity itself is case-flag-independent, so an invalid pattern (unbalanced group,
20/// lookaround / backreferences the `regex` crate lacks) is rejected regardless.
21///
22/// A consumer surfaces invalid-regex with this rather than JS `RegExp`: the `regex` crate's grammar
23/// differs (no lookaround/backreferences, Unicode-aware `\w \d \b`), so a JS-side check would
24/// misjudge patterns and reproduce the D2 gap. Pattern-only (no `SearchOptions`) — the case flag
25/// changes only compile size, covered here by validating the worst case.
26pub fn is_valid_regex(pattern: &str) -> bool {
27    regex::RegexBuilder::new(pattern)
28        .case_insensitive(true)
29        .build()
30        .is_ok()
31}
32
33/// One literal match, inclusive on both ends, in absolute buffer coordinates.
34///
35/// **No `#[non_exhaustive]` (#844).** 12 out-of-crate literal sites, and a consumer receives one
36/// from `search` and hands it back to `match_spans` rather than building it — round-trip, not
37/// construction, so the attribute would restrict a use that does not exist.
38#[derive(Clone, Copy, PartialEq, Eq, Debug)]
39pub struct Match {
40    pub start_line: usize,
41    pub start_col: usize,
42    pub end_line: usize,
43    pub end_col: usize,
44}
45
46/// Search modes beyond the default literal + smart-case (see [`Term::search_with`](crate::Term::search_with)).
47/// Mirrors xterm.js's `ISearchOptions` (#314). The default (all off / smart-case) is exactly
48/// [`Term::search`](crate::Term::search).
49///
50/// **No `#[non_exhaustive]` (#844).** 13 out-of-crate literal sites over a derived `Default`, so a
51/// new option lands through `..Default::default()`. The mode set is also this crate's own to
52/// define, so there is no outside growth cause to defend against.
53#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
54pub struct SearchOptions {
55    /// Treat the query as a regular expression (the `regex` crate) instead of a literal substring.
56    ///
57    /// Caveats vs a JS `RegExp` (xterm.js): the `regex` crate has **no lookaround/backreferences**
58    /// and its `\w \d \b` are **Unicode-aware** by default. An **invalid or unsupported pattern
59    /// yields no matches** (an empty result) rather than an error — the current API has no error
60    /// channel, so a consumer cannot distinguish a bad pattern from a genuine no-match (#314).
61    /// Smart-case (see [`case_sensitive`](Self::case_sensitive)) infers case from the *raw* pattern,
62    /// so an uppercase metacharacter (`\B`, `\D`, `\x1B`…) can flip case-sensitivity — set
63    /// `case_sensitive` explicitly, or use an inline `(?i)`/`(?-i)`, to be sure.
64    pub regex: bool,
65    /// Match only where the run is bounded by non-word characters (a word char is alphanumeric or
66    /// `_`) — the `\bword\b` sense, applied to both literal and regex queries.
67    pub whole_word: bool,
68    /// `None` = smart-case (case-insensitive iff the query has no uppercase); `Some(true)` =
69    /// case-sensitive; `Some(false)` = force case-insensitive.
70    pub case_sensitive: Option<bool>,
71}