use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::utils::path::PathNormalizer;
pub struct FileRegistry {
files_by_extension: HashMap<String, Vec<String>>,
all_files: Vec<String>,
}
impl FileRegistry {
pub fn new(all_files: &[String]) -> Self {
let mut files_by_extension = HashMap::new();
for file_path in all_files {
if let Some(extension) = Path::new(file_path).extension() {
if let Some(ext_str) = extension.to_str() {
files_by_extension
.entry(ext_str.to_lowercase())
.or_insert_with(Vec::new)
.push(file_path.clone());
}
}
}
Self {
files_by_extension,
all_files: all_files.to_vec(),
}
}
pub fn get_files_with_extensions(&self, extensions: &[&str]) -> Vec<String> {
let mut result = Vec::new();
for ext in extensions {
if let Some(files) = self.files_by_extension.get(&ext.to_lowercase()) {
result.extend(files.clone());
}
}
result
}
pub fn find_file_with_extensions(
&self,
base_path: &Path,
extensions: &[&str],
) -> Option<String> {
for ext in extensions {
let file_path = if ext.is_empty() {
base_path.to_path_buf()
} else {
PathBuf::from(format!("{}.{}", base_path.to_string_lossy(), ext))
};
if let Some(found) = self.find_exact_file(&file_path.to_string_lossy()) {
return Some(found);
}
}
None
}
pub fn find_exact_file(&self, target_path: &str) -> Option<String> {
if let Some(found) = PathNormalizer::find_path_in_collection(target_path, &self.all_files) {
return Some(found.to_string());
}
if let Ok(canonical_target) = std::path::Path::new(target_path).canonicalize() {
let canonical_str = canonical_target.to_string_lossy();
let normalized_canonical = if canonical_str.starts_with("//?/") {
if let Some(drive_pos) = canonical_str.find(":/") {
if let Some(relative_part) = canonical_str.get(drive_pos + 2..) {
PathNormalizer::normalize_separators(relative_part)
} else {
PathNormalizer::normalize_separators(&canonical_str)
}
} else {
PathNormalizer::normalize_separators(&canonical_str)
}
} else {
PathNormalizer::normalize_separators(&canonical_str)
};
for file_path in &self.all_files {
if let Ok(canonical_file) = std::path::Path::new(file_path).canonicalize() {
let canonical_file_str = canonical_file.to_string_lossy();
let normalized_file = if canonical_file_str.starts_with("//?/") {
if let Some(drive_pos) = canonical_file_str.find(":/") {
if let Some(relative_part) = canonical_file_str.get(drive_pos + 2..) {
PathNormalizer::normalize_separators(relative_part)
} else {
PathNormalizer::normalize_separators(&canonical_file_str)
}
} else {
PathNormalizer::normalize_separators(&canonical_file_str)
}
} else {
PathNormalizer::normalize_separators(&canonical_file_str)
};
if normalized_canonical == normalized_file {
return Some(file_path.clone());
}
}
}
}
None
}
pub fn find_files_by_pattern(&self, pattern: &str) -> Vec<String> {
self.all_files
.iter()
.filter(|file| file.contains(pattern))
.cloned()
.collect()
}
pub fn get_all_files(&self) -> &[String] {
&self.all_files
}
}
pub fn find_project_root(source_file: &str) -> Option<String> {
let source_path = Path::new(source_file);
let mut current_dir = source_path.parent()?;
loop {
let indicators = [
"Cargo.toml",
"package.json",
"setup.py",
"go.mod",
"composer.json",
"pyproject.toml",
"pom.xml",
"build.gradle",
".git",
];
for indicator in &indicators {
let indicator_path = current_dir.join(indicator);
if indicator_path.exists() {
return Some(current_dir.to_string_lossy().to_string());
}
}
if let Some(parent) = current_dir.parent() {
current_dir = parent;
} else {
break;
}
}
None
}
pub fn normalize_path(path: &str) -> String {
let path_buf = Path::new(path);
let mut components = Vec::new();
for component in path_buf.components() {
match component {
std::path::Component::ParentDir => {
if !components.is_empty() {
components.pop();
}
}
std::path::Component::CurDir => {
}
std::path::Component::Prefix(_) => {
}
std::path::Component::RootDir => {
}
std::path::Component::Normal(name) => {
components.push(name.to_string_lossy().to_string());
}
}
}
if !components.is_empty() {
let normalized: PathBuf = components.into_iter().collect();
return PathNormalizer::normalize_separators(&normalized.to_string_lossy());
}
if let Ok(canonical) = path_buf.canonicalize() {
if let Ok(current_dir) = std::env::current_dir() {
if let Ok(relative) = canonical.strip_prefix(¤t_dir) {
return PathNormalizer::normalize_separators(&relative.to_string_lossy());
}
}
let canonical_str = canonical.to_string_lossy();
if canonical_str.starts_with("//?/") {
if let Some(drive_pos) = canonical_str.find(":/") {
if let Some(relative_part) = canonical_str.get(drive_pos + 2..) {
return PathNormalizer::normalize_separators(relative_part);
}
}
}
return PathNormalizer::normalize_separators(&canonical_str);
}
PathNormalizer::normalize_separators(path)
}
pub fn detect_language_from_path(file_path: &str) -> Option<String> {
let path = Path::new(file_path);
let extension = path.extension()?.to_str()?;
match extension {
"rs" => Some("rust".to_string()),
"js" | "mjs" => Some("javascript".to_string()),
"ts" | "tsx" => Some("typescript".to_string()),
"py" => Some("python".to_string()),
"go" => Some("go".to_string()),
"php" => Some("php".to_string()),
"cpp" | "cc" | "cxx" | "c++" => Some("cpp".to_string()),
"c" | "h" => Some("c".to_string()),
"rb" => Some("ruby".to_string()),
"sh" | "bash" => Some("bash".to_string()),
"json" => Some("json".to_string()),
"css" | "scss" | "sass" => Some("css".to_string()),
"md" | "markdown" => Some("markdown".to_string()),
"svelte" => Some("svelte".to_string()),
_ => None,
}
}
pub fn resolve_relative_path(source_file: &str, relative_path: &str) -> Option<PathBuf> {
let source_path = Path::new(source_file);
let source_dir = source_path.parent()?;
let resolved = source_dir.join(relative_path);
let normalized_str = normalize_path(&resolved.to_string_lossy());
Some(PathBuf::from(normalized_str))
}
pub fn find_files_in_directory(
directory: &Path,
registry: &FileRegistry,
extensions: &[&str],
) -> Vec<String> {
let dir_str = directory.to_string_lossy();
registry
.get_files_with_extensions(extensions)
.into_iter()
.filter(|file| file.starts_with(&*dir_str))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_registry_creation() {
let files = vec![
"src/main.rs".to_string(),
"src/lib.rs".to_string(),
"package.json".to_string(),
"index.js".to_string(),
];
let registry = FileRegistry::new(&files);
let rust_files = registry.get_files_with_extensions(&["rs"]);
assert_eq!(rust_files.len(), 2);
assert!(rust_files.contains(&"src/main.rs".to_string()));
assert!(rust_files.contains(&"src/lib.rs".to_string()));
}
#[test]
fn test_find_file_with_extensions() {
let files = vec!["src/utils.rs".to_string(), "src/utils.js".to_string()];
let registry = FileRegistry::new(&files);
let result = registry.find_file_with_extensions(Path::new("src/utils"), &["rs", "js"]);
assert!(result.is_some());
let result_path = result.unwrap();
assert!(result_path.ends_with(".rs") || result_path.ends_with(".js"));
}
#[test]
fn test_detect_language_from_path() {
assert_eq!(
detect_language_from_path("main.rs"),
Some("rust".to_string())
);
assert_eq!(
detect_language_from_path("index.js"),
Some("javascript".to_string())
);
assert_eq!(
detect_language_from_path("app.py"),
Some("python".to_string())
);
assert_eq!(detect_language_from_path("unknown.xyz"), None);
}
#[test]
fn test_resolve_relative_path() {
let result = resolve_relative_path("src/main.rs", "../lib.rs");
assert!(result.is_some());
assert_eq!(result.unwrap().to_string_lossy(), "lib.rs");
}
#[test]
fn test_cross_platform_path_comparison() {
let files = vec![
"src/main.rs".to_string(),
"src\\utils\\helper.rs".to_string(), "lib/config.rs".to_string(),
];
let registry = FileRegistry::new(&files);
let result = registry.find_exact_file("src/utils/helper.rs");
assert!(result.is_some(), "Should find Windows path with Unix query");
let result = registry.find_exact_file("lib\\config.rs");
assert!(result.is_some(), "Should find Unix path with Windows query");
}
#[test]
fn test_normalize_path_separators() {
assert_eq!(
PathNormalizer::normalize_separators("src\\main.rs"),
"src/main.rs"
);
assert_eq!(
PathNormalizer::normalize_separators("src\\utils\\helper.rs"),
"src/utils/helper.rs"
);
assert_eq!(
PathNormalizer::normalize_separators("src/main.rs"),
"src/main.rs"
);
assert_eq!(
PathNormalizer::normalize_separators("src/utils/helper.rs"),
"src/utils/helper.rs"
);
assert_eq!(
PathNormalizer::normalize_separators("src\\utils/helper.rs"),
"src/utils/helper.rs"
);
assert_eq!(
PathNormalizer::normalize_separators("src/utils\\helper.rs"),
"src/utils/helper.rs"
);
assert_eq!(PathNormalizer::normalize_separators(""), "");
assert_eq!(PathNormalizer::normalize_separators("\\"), "/");
assert_eq!(PathNormalizer::normalize_separators("/"), "/");
}
}