jtool-grep 0.2.0

notebook-specific grep tool for jtool
Documentation
//! Pattern matching logic for grep

use anyhow::{Context, Result};
use regex::Regex;

/// Pattern matcher for grep searches
pub struct Matcher {
    regex: Regex,
    only_matching: bool,
    invert_match: bool,
}

impl Matcher {
    /// Create a new matcher from a pattern string
    pub fn new(
        pattern: &str,
        case_insensitive: bool,
        word_regexp: bool,
        fixed_strings: bool,
        only_matching: bool,
        invert_match: bool,
    ) -> Result<Self> {
        // Build the pattern
        let mut final_pattern = if fixed_strings {
            regex::escape(pattern)
        } else {
            pattern.to_string()
        };

        // Add word boundaries if needed
        if word_regexp {
            final_pattern = format!(r"\b{final_pattern}\b");
        }

        // Add case insensitive flag if needed
        if case_insensitive {
            final_pattern = format!("(?i){final_pattern}");
        }

        let regex = Regex::new(&final_pattern).context("Failed to compile regex pattern")?;

        Ok(Self {
            regex,
            only_matching,
            invert_match,
        })
    }

    /// Find a match in a line of text
    /// Returns Some((matched_text, start, end)) if found
    pub fn find<'a>(&self, text: &'a str) -> Option<(&'a str, usize, usize)> {
        let has_match = self.regex.is_match(text);

        // Handle invert match
        if self.invert_match {
            if !has_match {
                // For inverted match, return the whole line as the "match"
                return Some((text, 0, text.len()));
            } else {
                return None;
            }
        }

        // Normal matching
        if !has_match {
            return None;
        }

        // If only_matching, return just the matched portion
        if self.only_matching {
            self.regex
                .find(text)
                .map(|m| (m.as_str(), m.start(), m.end()))
        } else {
            // Return the full line but with match position
            self.regex.find(text).map(|m| (text, m.start(), m.end()))
        }
    }

    /// Check if the pattern matches the given text
    pub fn is_match(&self, text: &str) -> bool {
        if self.invert_match {
            !self.regex.is_match(text)
        } else {
            self.regex.is_match(text)
        }
    }

    /// Find all matches in a line of text
    pub fn find_all<'a>(&self, text: &'a str) -> Vec<(&'a str, usize, usize)> {
        self.regex
            .find_iter(text)
            .map(|m| (m.as_str(), m.start(), m.end()))
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_matcher_basic() {
        let matcher = Matcher::new("import", false, false, false, false, false);
        assert!(matcher.is_ok());
        if let Ok(matcher) = matcher {
            assert!(matcher.is_match("import pandas"));
            assert!(!matcher.is_match("define function"));
        }
    }

    #[test]
    fn test_matcher_case_insensitive() {
        let matcher = Matcher::new("IMPORT", true, false, false, false, false);
        assert!(matcher.is_ok());
        if let Ok(matcher) = matcher {
            assert!(matcher.is_match("import pandas"));
            assert!(matcher.is_match("Import numpy"));
            assert!(matcher.is_match("IMPORT os"));
        }
    }

    #[test]
    fn test_matcher_find() {
        let matcher = Matcher::new("import", false, false, false, true, false);
        assert!(matcher.is_ok());
        if let Ok(matcher) = matcher {
            let result = matcher.find("import pandas as pd");
            assert!(result.is_some());
            if let Some((matched, start, end)) = result {
                assert_eq!(matched, "import");
                assert_eq!(start, 0);
                assert_eq!(end, 6);
            }
        }
    }

    #[test]
    fn test_matcher_regex() {
        let matcher = Matcher::new(r"import \w+", false, false, false, true, false);
        assert!(matcher.is_ok());
        if let Ok(matcher) = matcher {
            let result = matcher.find("import pandas as pd");
            assert!(result.is_some());
            if let Some((matched, _, _)) = result {
                assert_eq!(matched, "import pandas");
            }
        }
    }
}