use std::collections::HashSet;
use std::path::{Path, PathBuf};
pub(crate) fn resolve_search_root(path: &str) -> Result<(PathBuf, Option<String>), String> {
let raw = Path::new(path);
if !raw.exists() {
return Err(format!("path does not exist: {path}"));
}
let raw_dir = if raw.is_file() {
raw.parent().unwrap_or(raw)
} else {
raw
};
let root = PathBuf::from(crate::core::protocol::detect_project_root_or_cwd(
&raw_dir.to_string_lossy(),
));
let subdir = search_subdir_filter(&root, raw_dir);
Ok((root, subdir))
}
pub(crate) fn search_subdir_filter(root: &Path, requested: &Path) -> Option<String> {
let root_c = crate::core::pathutil::safe_canonicalize_or_self(root);
let req_c = crate::core::pathutil::safe_canonicalize_or_self(requested);
let rel = req_c.strip_prefix(&root_c).ok()?;
let rel = rel.to_string_lossy().replace('\\', "/");
let rel = rel.trim_matches('/').to_string();
if rel.is_empty() { None } else { Some(rel) }
}
pub(crate) struct SearchFilter {
allowed_exts: Option<HashSet<String>>,
path_glob: Option<glob::Pattern>,
subdir: Option<String>,
}
impl SearchFilter {
pub(crate) fn new(
languages: Option<&[String]>,
path_glob: Option<&str>,
) -> Result<Self, String> {
let allowed_exts = languages.map(normalize_languages);
let path_glob = match path_glob {
None => None,
Some(s) if s.trim().is_empty() => None,
Some(s) => Some(glob::Pattern::new(s).map_err(|e| e.msg.to_string())?),
};
Ok(Self {
allowed_exts,
path_glob,
subdir: None,
})
}
pub(crate) fn with_subdir(mut self, subdir: Option<String>) -> Self {
self.subdir = subdir.filter(|s| !s.is_empty());
self
}
pub(crate) fn is_active(&self) -> bool {
self.allowed_exts.is_some() || self.path_glob.is_some() || self.subdir.is_some()
}
pub(crate) fn matches(&self, rel_path: &str) -> bool {
let rel_path = rel_path.replace('\\', "/");
if let Some(prefix) = &self.subdir
&& rel_path != *prefix
&& !rel_path.starts_with(&format!("{prefix}/"))
{
return false;
}
if let Some(p) = &self.path_glob
&& !p.matches(&rel_path)
{
return false;
}
if let Some(exts) = &self.allowed_exts {
let ext = Path::new(&rel_path)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if ext.is_empty() || !exts.contains(&ext) {
return false;
}
}
true
}
}
pub(crate) fn normalize_languages(langs: &[String]) -> HashSet<String> {
let mut out = HashSet::new();
for l in langs {
let raw = l.trim().trim_start_matches('.').to_lowercase();
match raw.as_str() {
"rust" | "rs" => {
out.insert("rs".to_string());
}
"ts" | "typescript" => {
out.insert("ts".to_string());
out.insert("tsx".to_string());
}
"js" | "javascript" => {
out.insert("js".to_string());
out.insert("jsx".to_string());
out.insert("mjs".to_string());
out.insert("cjs".to_string());
}
"py" | "python" => {
out.insert("py".to_string());
}
"go" => {
out.insert("go".to_string());
}
"java" => {
out.insert("java".to_string());
}
"ruby" | "rb" => {
out.insert("rb".to_string());
}
"php" => {
out.insert("php".to_string());
}
"c" => {
out.insert("c".to_string());
out.insert("h".to_string());
}
"cpp" | "c++" | "cc" => {
out.insert("cpp".to_string());
out.insert("hpp".to_string());
out.insert("cc".to_string());
out.insert("hh".to_string());
}
"cs" | "csharp" => {
out.insert("cs".to_string());
}
"swift" => {
out.insert("swift".to_string());
}
"kt" | "kotlin" => {
out.insert("kt".to_string());
out.insert("kts".to_string());
}
"json" => {
out.insert("json".to_string());
}
"yaml" | "yml" => {
out.insert("yaml".to_string());
out.insert("yml".to_string());
}
other if !other.is_empty() => {
out.insert(other.to_string());
}
_ => {}
}
}
out
}