use anyhow::{Context, Result};
use ignore::WalkBuilder;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
pub struct SearchConfig<'a> {
pub pattern: &'a str,
pub path: &'a Path,
pub case_insensitive: bool,
pub include_globs: &'a [String],
pub exclude_globs: &'a [String],
}
const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
pub fn find_files_with_matches(config: &SearchConfig) -> Result<Vec<PathBuf>> {
let mut builder = WalkBuilder::new(config.path);
builder
.hidden(true) .git_ignore(true) .git_global(true) .git_exclude(true) .ignore(true) .parents(true);
builder.threads(num_cpus::get());
if !config.include_globs.is_empty() || !config.exclude_globs.is_empty() {
let mut overrides = ignore::overrides::OverrideBuilder::new(config.path);
if !config.include_globs.is_empty() {
for pattern in config.include_globs {
overrides.add(pattern)?;
}
} else if !config.exclude_globs.is_empty() {
overrides.add("**/*")?;
}
for pattern in config.exclude_globs {
let exclude_pattern = format!("!{pattern}");
overrides.add(&exclude_pattern)?;
}
builder.overrides(overrides.build()?);
}
let pattern_lower = if config.case_insensitive {
Some(config.pattern.to_lowercase())
} else {
None
};
let matches = Arc::new(Mutex::new(Vec::new()));
let matches_clone = matches.clone();
builder.build_parallel().run(|| {
let matches = matches_clone.clone();
let pattern = config.pattern;
let pattern_lower = pattern_lower.clone();
Box::new(move |entry| {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_dir() {
return ignore::WalkState::Continue;
}
if should_search_file(path, pattern, pattern_lower.as_deref()) {
matches.lock().unwrap().push(path.to_path_buf());
}
}
ignore::WalkState::Continue
})
});
let results = Arc::try_unwrap(matches)
.map(|mutex| mutex.into_inner().unwrap())
.unwrap_or_else(|arc| arc.lock().unwrap().clone());
Ok(results)
}
fn should_search_file(path: &Path, pattern: &str, pattern_lower: Option<&str>) -> bool {
if let Ok(metadata) = path.metadata() {
if metadata.len() > MAX_FILE_SIZE {
return false;
}
}
let file = match File::open(path)
.with_context(|| format!("Failed to open file: {}", path.display()))
{
Ok(f) => f,
Err(_) => return false,
};
let reader = BufReader::new(file);
if let Some(pattern_lower) = pattern_lower {
for line in reader.lines().map_while(Result::ok) {
if line.to_lowercase().contains(pattern_lower) {
return true;
}
}
} else {
if pattern.len() <= 32 {
for line in reader.lines().map_while(Result::ok) {
if line.contains(pattern) {
return true;
}
}
} else {
for line in reader.lines().map_while(Result::ok) {
if fast_substring_search(&line, pattern) {
return true;
}
}
}
}
false
}
fn fast_substring_search(haystack: &str, needle: &str) -> bool {
haystack.contains(needle)
}