Skip to main content

tess/
grep.rs

1use regex::Regex;
2
3/// AND-combined regex predicate applied to raw line bytes. Lines that fail
4/// UTF-8 decoding never match (mirrors what the interactive `/` search does).
5#[derive(Debug)]
6pub struct GrepPredicate {
7    regexes: Vec<Regex>,
8}
9
10impl GrepPredicate {
11    /// Compile each pattern under the given case policy. Returns the first
12    /// invalid pattern's error, prefixed so the user can tell which
13    /// `--grep` argument was bad. The case policy is applied via
14    /// `CaseMode::apply_to_pattern`, so the `(?i)` flag is prepended when
15    /// insensitivity is wanted.
16    pub fn compile(
17        patterns: &[String],
18        case_mode: crate::viewport::CaseMode,
19    ) -> Result<Self, String> {
20        let mut regexes = Vec::with_capacity(patterns.len());
21        for p in patterns {
22            let compiled = case_mode.apply_to_pattern(p);
23            let r = Regex::new(&compiled).map_err(|e| format!("--grep `{p}`: {e}"))?;
24            regexes.push(r);
25        }
26        Ok(Self { regexes })
27    }
28
29    pub fn is_empty(&self) -> bool { self.regexes.is_empty() }
30
31    /// True iff every compiled pattern matches the line. Empty predicate
32    /// vacuously matches (callers should treat that as "no grep configured"
33    /// via `is_empty` and not even ask).
34    pub fn matches(&self, line: &[u8]) -> bool {
35        let s = match std::str::from_utf8(line) {
36            Ok(s) => s,
37            Err(_) => return false,
38        };
39        self.regexes.iter().all(|r| r.is_match(s))
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn empty_predicate_is_empty() {
49        let g = GrepPredicate::compile(&[], crate::viewport::CaseMode::Sensitive).unwrap();
50        assert!(g.is_empty());
51    }
52
53    #[test]
54    fn single_pattern_matches() {
55        let g = GrepPredicate::compile(&["error".to_string()], crate::viewport::CaseMode::Sensitive).unwrap();
56        assert!(g.matches(b"something failed: error 42"));
57        assert!(!g.matches(b"all good"));
58    }
59
60    #[test]
61    fn multiple_patterns_are_anded() {
62        let g = GrepPredicate::compile(
63            &["error".to_string(), r"^\[\d{4}".to_string()],
64            crate::viewport::CaseMode::Sensitive,
65        ).unwrap();
66        assert!(g.matches(b"[2026-05-13] error occurred"));
67        assert!(!g.matches(b"[2026-05-13] all good"));
68        assert!(!g.matches(b"error occurred (no timestamp)"));
69    }
70
71    #[test]
72    fn invalid_regex_is_reported_with_arg() {
73        let err = GrepPredicate::compile(&["[unclosed".to_string()], crate::viewport::CaseMode::Sensitive).unwrap_err();
74        assert!(err.contains("--grep `[unclosed`"), "{err}");
75    }
76
77    #[test]
78    fn non_utf8_line_never_matches() {
79        let g = GrepPredicate::compile(&[".".to_string()], crate::viewport::CaseMode::Sensitive).unwrap();
80        // Lone 0xFF is invalid UTF-8.
81        assert!(!g.matches(&[0xFF, b'a', b'b']));
82    }
83
84    #[test]
85    fn insensitive_mode_matches_uppercase_input() {
86        let g = GrepPredicate::compile(
87            &["foo".to_string()],
88            crate::viewport::CaseMode::Insensitive,
89        )
90        .unwrap();
91        assert!(g.matches(b"FooBar"));
92        assert!(g.matches(b"FOO"));
93        assert!(g.matches(b"foo"));
94    }
95
96    #[test]
97    fn smart_mode_lowercase_is_insensitive() {
98        let g = GrepPredicate::compile(
99            &["foo".to_string()],
100            crate::viewport::CaseMode::Smart,
101        )
102        .unwrap();
103        assert!(g.matches(b"FOO bar"));
104    }
105
106    #[test]
107    fn smart_mode_uppercase_is_sensitive() {
108        let g = GrepPredicate::compile(
109            &["Foo".to_string()],
110            crate::viewport::CaseMode::Smart,
111        )
112        .unwrap();
113        assert!(g.matches(b"Foo bar"));
114        assert!(!g.matches(b"foo bar"));
115        assert!(!g.matches(b"FOO bar"));
116    }
117}