magi-code 0.96.1

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

#[derive(Debug)]
pub(super) struct MatcherBuildResult {
    pub(super) matchers: Vec<RegexMatcher>,
    pub(super) patterns_adjusted: usize,
    pub(super) literal_fallbacks: usize,
}

pub(super) fn build_matchers(patterns: &[String]) -> anyhow::Result<MatcherBuildResult> {
    let mut result = MatcherBuildResult {
        matchers: Vec::with_capacity(patterns.len()),
        patterns_adjusted: 0,
        literal_fallbacks: 0,
    };

    for pattern in patterns {
        let built = build_matcher_with_stats(pattern)?;
        result.patterns_adjusted += usize::from(built.adjusted);
        result.literal_fallbacks += usize::from(built.literal_fallback);
        result.matchers.push(built.matcher);
    }

    Ok(result)
}

struct BuiltMatcher {
    matcher: RegexMatcher,
    adjusted: bool,
    literal_fallback: bool,
}

fn build_matcher_with_stats(pattern: &str) -> anyhow::Result<BuiltMatcher> {
    let sanitized = sanitize_braces(pattern);
    match compile(&sanitized) {
        Ok(matcher) => Ok(BuiltMatcher {
            matcher,
            adjusted: sanitized != pattern,
            literal_fallback: false,
        }),
        Err(error) if is_group_error(&error.to_string()) => {
            let escaped = escape_unescaped_parentheses(&sanitized);
            match compile(&escaped) {
                Ok(matcher) => Ok(BuiltMatcher {
                    matcher,
                    adjusted: escaped != pattern,
                    literal_fallback: false,
                }),
                Err(_) => Ok(BuiltMatcher {
                    matcher: compile(&regex::escape(pattern))?,
                    adjusted: true,
                    literal_fallback: true,
                }),
            }
        }
        Err(_) => Ok(BuiltMatcher {
            matcher: compile(&regex::escape(pattern))?,
            adjusted: true,
            literal_fallback: true,
        }),
    }
}

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
}