Skip to main content

matrixcode_core/tools/codegraph/
ignore.rs

1//! Ignore pattern matching for CodeGraph watcher.
2
3use std::path::Path;
4
5/// Default ignore patterns for file watching.
6pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &[
7    "target", "dist", "build", "out", "bin", "obj", ".output",
8    "node_modules", "vendor", "Pods", ".venv", "venv", "__pycache__",
9    ".cache", ".tmp", ".temp", "tmp", "temp",
10    ".idea", ".vscode", ".eclipse", ".project", ".classpath",
11    ".generated", "generated", ".codegraph",
12    "package-lock.json", "yarn.lock", "Cargo.lock", "pnpm-lock.yaml",
13    "coverage", ".nyc_output", "test-results", "logs",
14];
15
16/// Extensions to watch (source files only).
17pub const WATCH_EXTENSIONS: &[&str] = &[
18    "rs", "ts", "tsx", "js", "jsx", "mjs", "py", "go",
19    "java", "kt", "kts", "c", "cpp", "cc", "h", "hpp",
20    "rb", "php", "swift", "cs", "scala", "lua", "sh",
21];
22
23/// Gitignore patterns loaded from file.
24pub struct IgnoreMatcher {
25    patterns: Vec<String>,
26    negation_patterns: Vec<String>,
27}
28
29impl IgnoreMatcher {
30    /// Load ignore patterns from .gitignore and defaults.
31    pub fn load(project_path: &Path) -> Self {
32        let mut patterns = Vec::new();
33        let mut negation_patterns = Vec::new();
34
35        // Add default patterns
36        for p in DEFAULT_IGNORE_PATTERNS {
37            patterns.push(p.to_string());
38        }
39
40        // Load .gitignore
41        let gitignore_path = project_path.join(".gitignore");
42        if gitignore_path.exists() {
43            if let Ok(content) = std::fs::read_to_string(&gitignore_path) {
44                for line in content.lines() {
45                    let line = line.trim();
46                    if line.is_empty() || line.starts_with('#') {
47                        continue;
48                    }
49                    if let Some(stripped) = line.strip_prefix('!') {
50                        negation_patterns.push(stripped.to_string());
51                    } else {
52                        patterns.push(line.to_string());
53                    }
54                }
55            }
56        }
57
58        Self { patterns, negation_patterns }
59    }
60
61    /// Check if a path should be ignored.
62    pub fn should_ignore(&self, path: &Path, project_path: &Path) -> bool {
63        let path_str = path.to_string_lossy();
64        let relative_path = path.strip_prefix(project_path)
65            .unwrap_or(path)
66            .to_string_lossy();
67
68        // Check negation patterns first (explicit inclusion)
69        for pattern in &self.negation_patterns {
70            if Self::matches_pattern(&relative_path, pattern) {
71                return false;
72            }
73        }
74
75        // Check ignore patterns
76        for pattern in &self.patterns {
77            if Self::matches_pattern(&relative_path, pattern) || path_str.contains(pattern) {
78                return true;
79            }
80        }
81
82        // Check hidden files (but allow .codegraph)
83        for component in path.components() {
84            if let std::path::Component::Normal(name) = component {
85                let name_str = name.to_string_lossy();
86                if name_str.starts_with('.')
87                    && name_str != ".codegraph"
88                    && !WATCH_EXTENSIONS.contains(&name_str.split('.').next_back().unwrap_or("")) {
89                    return true;
90                }
91            }
92        }
93
94        false
95    }
96
97    /// Check if path matches a gitignore pattern.
98    fn matches_pattern(path: &str, pattern: &str) -> bool {
99        let pattern = pattern.trim_start_matches('/');
100
101        // Directory match (pattern ends with /)
102        if let Some(dir_pattern) = pattern.strip_suffix('/') {
103            return path.contains(dir_pattern) || path.starts_with(dir_pattern);
104        }
105
106        // Wildcard match
107        if pattern.contains('*') {
108            let parts = pattern.split('*').collect::<Vec<_>>();
109            if parts.len() == 2 {
110                let prefix = parts[0];
111                let suffix = parts[1];
112                return (prefix.is_empty() || path.starts_with(prefix))
113                    && (suffix.is_empty() || path.ends_with(suffix));
114            }
115        }
116
117        // Exact match or contains
118        path == pattern || path.contains(pattern) || path.starts_with(&format!("{}/", pattern))
119    }
120}