magi-code 0.63.3

Repository-aware CLI coding agent for terminal work
Documentation
use grep_regex::{RegexMatcher, RegexMatcherBuilder};

pub(super) fn build_matcher(pattern: &str) -> anyhow::Result<RegexMatcher> {
    let sanitized = sanitize_braces(pattern);
    match compile(&sanitized) {
        Ok(matcher) => Ok(matcher),
        Err(error) if is_group_error(&error.to_string()) => {
            let escaped = escape_unescaped_parentheses(&sanitized);
            compile(&escaped).or_else(|_| compile(&regex::escape(pattern)))
        }
        Err(_) => compile(&regex::escape(pattern)),
    }
}

fn compile(pattern: &str) -> anyhow::Result<RegexMatcher> {
    RegexMatcherBuilder::new()
        .case_insensitive(false)
        .multi_line(true)
        .line_terminator(Some(b'\n'))
        .build(pattern)
        .map_err(Into::into)
}

fn is_group_error(message: &str) -> bool {
    let lower = message.to_ascii_lowercase();
    lower.contains("unclosed group")
        || lower.contains("unopened group")
        || lower.contains("unclosed") && lower.contains("(")
        || lower.contains("unopened") && lower.contains(")")
}

pub(super) fn sanitize_braces(pattern: &str) -> String {
    let chars = pattern.chars().collect::<Vec<_>>();
    let mut out = String::with_capacity(pattern.len());
    let mut index = 0;
    let mut escaped = false;
    while index < chars.len() {
        let ch = chars[index];
        if escaped {
            out.push(ch);
            if matches!(ch, 'x' | 'p' | 'P')
                && chars.get(index + 1) == Some(&'{')
                && let Some(end) = find_closing_brace(&chars, index + 2)
            {
                for c in &chars[index + 1..=end] {
                    out.push(*c);
                }
                index = end + 1;
                escaped = false;
                continue;
            }
            escaped = false;
            index += 1;
            continue;
        }
        match ch {
            '\\' => {
                out.push(ch);
                escaped = true;
            }
            '{' if is_valid_quantifier_at(&chars, index) => {
                let end = find_closing_brace(&chars, index + 1).unwrap_or(index);
                for c in &chars[index..=end] {
                    out.push(*c);
                }
                index = end + 1;
                continue;
            }
            '{' | '}' => {
                out.push('\\');
                out.push(ch);
            }
            _ => out.push(ch),
        }
        index += 1;
    }
    out
}

fn find_closing_brace(chars: &[char], mut index: usize) -> Option<usize> {
    while index < chars.len() {
        if chars[index] == '}' {
            return Some(index);
        }
        index += 1;
    }
    None
}

fn is_valid_quantifier_at(chars: &[char], index: usize) -> bool {
    if index == 0 || !can_quantify(chars[index - 1]) {
        return false;
    }
    let mut cursor = index + 1;
    let first_digits = cursor;
    while chars.get(cursor).is_some_and(|ch| ch.is_ascii_digit()) {
        cursor += 1;
    }
    if cursor == first_digits {
        return false;
    }
    if chars.get(cursor) == Some(&',') {
        cursor += 1;
        while chars.get(cursor).is_some_and(|ch| ch.is_ascii_digit()) {
            cursor += 1;
        }
    }
    chars.get(cursor) == Some(&'}')
}

fn can_quantify(ch: char) -> bool {
    !matches!(ch, '|' | '(' | '^')
}

pub(super) fn escape_unescaped_parentheses(pattern: &str) -> String {
    let mut out = String::with_capacity(pattern.len());
    let mut escaped = false;
    for ch in pattern.chars() {
        if escaped {
            out.push(ch);
            escaped = false;
            continue;
        }
        match ch {
            '\\' => {
                out.push(ch);
                escaped = true;
            }
            '(' | ')' => {
                out.push('\\');
                out.push(ch);
            }
            _ => out.push(ch),
        }
    }
    out
}

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

    fn is_match(pattern: &str, haystack: &str) -> bool {
        build_matcher(pattern)
            .unwrap()
            .is_match(haystack.as_bytes())
            .unwrap()
    }

    #[test]
    fn sanitize_braces_preserves_valid_regex_braces() {
        assert_eq!(sanitize_braces("a{2,4}"), "a{2,4}");
        assert_eq!(sanitize_braces("a{2}"), "a{2}");
        assert_eq!(sanitize_braces("a{2,}"), "a{2,}");
        assert_eq!(sanitize_braces(r"\x{1F600}"), r"\x{1F600}");
        assert_eq!(sanitize_braces(r"\p{L}"), r"\p{L}");
        assert_eq!(sanitize_braces(r"\P{Greek}"), r"\P{Greek}");
    }

    #[test]
    fn sanitize_braces_escapes_stray_braces() {
        assert_eq!(sanitize_braces("${x}"), r"$\{x\}");
        assert_eq!(sanitize_braces("a } b"), r"a \} b");
    }

    #[test]
    fn escape_unescaped_parentheses_rescues_group_errors() {
        assert_eq!(escape_unescaped_parentheses("foo(bar"), r"foo\(bar");
        assert!(is_match("foo(bar", "foo(bar"));
        assert!(is_match("foo)bar", "foo)bar"));
    }

    #[test]
    fn literal_fallback_after_total_regex_failure() {
        assert!(is_match("[", "["));
    }

    #[test]
    fn line_anchor_pattern_compiles() {
        assert!(is_match("^needle$", "needle"));
        assert!(!is_match("^needle$", "xneedle"));
    }
}