Skip to main content

justerm_core/
search.rs

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