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