use anyhow::{Context, Result};
use grep::regex::RegexMatcher;
use grep::searcher::{sinks::UTF8, SearcherBuilder};
use ignore::WalkBuilder;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct WorkspaceSearchMatch {
pub path: PathBuf,
pub line_number: u64,
pub line: String,
}
pub fn discover_files_with_suffix(root: &Path, suffix: &str) -> Result<Vec<PathBuf>> {
let mut paths = Vec::new();
let walker = WalkBuilder::new(root)
.hidden(false)
.standard_filters(true)
.build();
for entry in walker {
let entry = entry.with_context(|| {
format!("Failed to walk workspace while searching in {}", root.display())
})?;
let path = entry.path();
if entry
.file_type()
.map(|kind| kind.is_file())
.unwrap_or(false)
&& path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.ends_with(suffix))
{
paths.push(path.to_path_buf());
}
}
paths.sort();
paths.dedup();
Ok(paths)
}
pub fn search_workspace(root: &Path, pattern: &str) -> Result<Vec<WorkspaceSearchMatch>> {
let matcher = RegexMatcher::new(pattern)
.with_context(|| format!("Invalid search pattern '{}'", pattern))?;
let mut searcher = SearcherBuilder::new()
.line_number(true)
.build();
let mut matches = Vec::new();
let walker = WalkBuilder::new(root)
.hidden(false)
.standard_filters(true)
.build();
for entry in walker {
let entry = entry.with_context(|| {
format!("Failed to walk workspace while searching in {}", root.display())
})?;
let path = entry.path();
if !entry
.file_type()
.map(|kind| kind.is_file())
.unwrap_or(false)
{
continue;
}
let path_buf = path.to_path_buf();
searcher
.search_path(
&matcher,
path,
UTF8(|line_number, line| {
matches.push(WorkspaceSearchMatch {
path: path_buf.clone(),
line_number,
line: line.to_string(),
});
Ok(true)
}),
)
.with_context(|| format!("Failed to search {}", path.display()))?;
}
Ok(matches)
}