use super::{CompletionItem, CompletionKind};
use std::path::Path;
use std::process::Stdio;
pub async fn fuzzy_file_search(query: &str, base_dir: &Path) -> Vec<CompletionItem> {
if query.is_empty() {
return Vec::new();
}
let fd_available = tokio::process::Command::new("fd")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.await
.is_ok();
if !fd_available {
return simple_file_search(query, base_dir);
}
let output = tokio::process::Command::new("fd")
.arg("--base-directory")
.arg(base_dir)
.arg("--max-results")
.arg("100")
.arg("--type")
.arg("f")
.arg("--type")
.arg("d")
.arg("--follow")
.arg("--hidden")
.arg("--exclude")
.arg(".git")
.arg("--exclude")
.arg("node_modules")
.arg("--exclude")
.arg("target")
.arg(query)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.output()
.await;
match output {
Ok(output) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout);
let mut results: Vec<CompletionItem> = stdout
.lines()
.filter_map(|line| {
if line.is_empty() {
return None;
}
let score = score_path(line, query);
Some((line.to_string(), score))
})
.filter(|(_, score)| *score > 0)
.map(|(path, _)| CompletionItem {
text: path.clone(),
label: path.clone(),
description: Some("file".to_string()),
kind: CompletionKind::FuzzyFile {
query: query.to_string(),
},
})
.collect();
results.sort_by(|a, b| {
let sa = score_path(&a.text, query);
let sb = score_path(&b.text, query);
sb.cmp(&sa)
});
results.truncate(20);
results
}
_ => Vec::new(),
}
}
fn simple_file_search(query: &str, base_dir: &Path) -> Vec<CompletionItem> {
let lower_query = query.to_lowercase();
let mut results = Vec::new();
if let Ok(entries) = std::fs::read_dir(base_dir) {
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().to_string();
if name.to_lowercase().contains(&lower_query) {
let is_dir = entry.file_type().map(|t| t.is_dir()).unwrap_or(false);
results.push(CompletionItem {
text: name.clone(),
label: if is_dir {
format!("{}/", name)
} else {
name.clone()
},
description: if is_dir {
Some("directory".to_string())
} else {
Some("file".to_string())
},
kind: CompletionKind::FuzzyFile {
query: query.to_string(),
},
});
}
if entry.file_type().map(|t| t.is_dir()).unwrap_or(false)
&& !name.starts_with('.')
&& name != "target"
&& name != "node_modules"
{
if let Ok(sub_entries) = std::fs::read_dir(entry.path()) {
for sub_entry in sub_entries.flatten() {
let sub_name = sub_entry.file_name().to_string_lossy().to_string();
let full_path = format!("{}/{}", name, sub_name);
if full_path.to_lowercase().contains(&lower_query) {
results.push(CompletionItem {
text: full_path.clone(),
label: full_path,
description: Some("file".to_string()),
kind: CompletionKind::FuzzyFile {
query: query.to_string(),
},
});
}
}
}
}
if results.len() >= 20 {
break;
}
}
}
results
}
fn score_path(path: &str, query: &str) -> i32 {
let lower_path = path.to_lowercase();
let lower_query = query.to_lowercase();
let basename = std::path::Path::new(path)
.file_name()
.map(|f| f.to_string_lossy().to_lowercase())
.unwrap_or_default();
if basename == lower_query {
100
} else if basename.starts_with(&lower_query) {
80
} else if basename.contains(&lower_query) {
50
} else if lower_path.contains(&lower_query) {
30
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_score_exact_match() {
assert_eq!(score_path("main.rs", "main.rs"), 100);
}
#[test]
fn test_score_starts_with() {
assert_eq!(score_path("main.rs", "main"), 80);
}
#[test]
fn test_score_contains() {
assert_eq!(score_path("lib_main.rs", "main"), 50);
}
#[test]
fn test_score_path_contains() {
assert_eq!(score_path("src/main.rs", "main"), 80); }
#[test]
fn test_score_no_match() {
assert_eq!(score_path("foo.rs", "bar"), 0);
}
#[test]
fn test_score_case_insensitive() {
assert_eq!(score_path("Main.rs", "main"), 80);
assert_eq!(score_path("Main.rs", "main.rs"), 100);
}
#[test]
fn test_simple_search() {
let cwd = std::env::current_dir().unwrap();
let results = simple_file_search("cargo", &cwd);
assert!(!results.is_empty());
}
#[tokio::test]
async fn test_fuzzy_search_empty_query() {
let cwd = std::env::current_dir().unwrap();
let results = fuzzy_file_search("", &cwd).await;
assert!(results.is_empty());
}
}