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#[derive(Clone, Copy, PartialEq, Eq, Debug)]
35pub struct Match {
36    pub start_line: usize,
37    pub start_col: usize,
38    pub end_line: usize,
39    pub end_col: usize,
40}
41
42/// Search modes beyond the default literal + smart-case (see [`Term::search_with`](crate::Term::search_with)).
43/// Mirrors xterm.js's `ISearchOptions` (#314). The default (all off / smart-case) is exactly
44/// [`Term::search`](crate::Term::search).
45#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
46pub struct SearchOptions {
47    /// Treat the query as a regular expression (the `regex` crate) instead of a literal substring.
48    ///
49    /// Caveats vs a JS `RegExp` (xterm.js): the `regex` crate has **no lookaround/backreferences**
50    /// and its `\w \d \b` are **Unicode-aware** by default. An **invalid or unsupported pattern
51    /// yields no matches** (an empty result) rather than an error — the current API has no error
52    /// channel, so a consumer cannot distinguish a bad pattern from a genuine no-match (#314).
53    /// Smart-case (see [`case_sensitive`](Self::case_sensitive)) infers case from the *raw* pattern,
54    /// so an uppercase metacharacter (`\B`, `\D`, `\x1B`…) can flip case-sensitivity — set
55    /// `case_sensitive` explicitly, or use an inline `(?i)`/`(?-i)`, to be sure.
56    pub regex: bool,
57    /// Match only where the run is bounded by non-word characters (a word char is alphanumeric or
58    /// `_`) — the `\bword\b` sense, applied to both literal and regex queries.
59    pub whole_word: bool,
60    /// `None` = smart-case (case-insensitive iff the query has no uppercase); `Some(true)` =
61    /// case-sensitive; `Some(false)` = force case-insensitive.
62    pub case_sensitive: Option<bool>,
63}