use std::path::Path;
pub const DEFAULT_IGNORE_PATTERNS: &[&str] = &[
"target",
"dist",
"build",
"out",
"bin",
"obj",
".output",
"node_modules",
"vendor",
"Pods",
".venv",
"venv",
"__pycache__",
".cache",
".tmp",
".temp",
"tmp",
"temp",
".idea",
".vscode",
".eclipse",
".project",
".classpath",
".generated",
"generated",
".codegraph",
"package-lock.json",
"yarn.lock",
"Cargo.lock",
"pnpm-lock.yaml",
"coverage",
".nyc_output",
"test-results",
"logs",
];
pub const WATCH_EXTENSIONS: &[&str] = &[
"rs", "ts", "tsx", "js", "jsx", "mjs", "py", "go", "java", "kt", "kts", "c", "cpp", "cc", "h",
"hpp", "rb", "php", "swift", "cs", "scala", "lua", "sh",
];
pub struct IgnoreMatcher {
patterns: Vec<String>,
negation_patterns: Vec<String>,
}
impl IgnoreMatcher {
pub fn load(project_path: &Path) -> Self {
let mut patterns = Vec::new();
let mut negation_patterns = Vec::new();
for p in DEFAULT_IGNORE_PATTERNS {
patterns.push(p.to_string());
}
let gitignore_path = project_path.join(".gitignore");
if gitignore_path.exists() {
if let Ok(content) = std::fs::read_to_string(&gitignore_path) {
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(stripped) = line.strip_prefix('!') {
negation_patterns.push(stripped.to_string());
} else {
patterns.push(line.to_string());
}
}
}
}
Self {
patterns,
negation_patterns,
}
}
pub fn should_ignore(&self, path: &Path, project_path: &Path) -> bool {
let path_str = path.to_string_lossy();
let relative_path = path
.strip_prefix(project_path)
.unwrap_or(path)
.to_string_lossy();
for pattern in &self.negation_patterns {
if Self::matches_pattern(&relative_path, pattern) {
return false;
}
}
for pattern in &self.patterns {
if Self::matches_pattern(&relative_path, pattern) || path_str.contains(pattern) {
return true;
}
}
for component in path.components() {
if let std::path::Component::Normal(name) = component {
let name_str = name.to_string_lossy();
if name_str.starts_with('.')
&& name_str != ".codegraph"
&& !WATCH_EXTENSIONS.contains(&name_str.split('.').next_back().unwrap_or(""))
{
return true;
}
}
}
false
}
fn matches_pattern(path: &str, pattern: &str) -> bool {
let pattern = pattern.trim_start_matches('/');
if let Some(dir_pattern) = pattern.strip_suffix('/') {
return path.contains(dir_pattern) || path.starts_with(dir_pattern);
}
if pattern.contains('*') {
let parts = pattern.split('*').collect::<Vec<_>>();
if parts.len() == 2 {
let prefix = parts[0];
let suffix = parts[1];
return (prefix.is_empty() || path.starts_with(prefix))
&& (suffix.is_empty() || path.ends_with(suffix));
}
}
path == pattern || path.contains(pattern) || path.starts_with(&format!("{}/", pattern))
}
}