use std::env;
use std::path::PathBuf;
use std::time::Instant;
use anyhow::Result;
use colored::Colorize;
use crate::colors::{colorize_language, colorize_purpose};
use crate::config::{find_sysmap_root, map_path};
use crate::map::{FileNode, SystemMap};
use crate::scanner::{estimate_tokens, format_tokens};
pub fn execute(
query: String,
file_type: Option<String>,
language: Option<String>,
purpose: Option<String>,
counts: bool,
deps: bool,
) -> Result<()> {
let start = Instant::now();
let cwd = env::current_dir()?;
let root = find_sysmap_root(&cwd)
.ok_or_else(|| anyhow::anyhow!(
"No sysmap found. Run 'sysmap init' first."
))?;
let map = SystemMap::load(&map_path(&root))?;
let query_lower = query.to_lowercase();
let file_type_normalized = file_type.map(|ft| {
ft.strip_prefix('.').unwrap_or(&ft).to_lowercase()
});
let language_normalized = language.map(|l| l.to_lowercase());
let purpose_normalized = purpose.map(|p| p.to_lowercase());
let mut matches = Vec::new();
find_matches(
&map.tree,
&query_lower,
&file_type_normalized,
&language_normalized,
&purpose_normalized,
&mut matches
);
let elapsed = start.elapsed();
if matches.is_empty() {
println!("{}", "No matches found.".yellow());
println!("{}", format!("Search completed in {:?}", elapsed).dimmed());
return Ok(());
}
println!("{} {} matches:", "Found".green().bold(), matches.len());
println!();
for (path, name, file_purpose, file_language, lines, chars) in &matches {
let mut info_parts = Vec::new();
if counts {
if let (Some(l), Some(c)) = (lines, chars) {
let tokens = estimate_tokens(*c);
info_parts.push(format!("{} lines, {} chars ({})", l, c, format_tokens(tokens)));
} else if let Some(l) = lines {
info_parts.push(format!("{} lines", l));
}
} else if let Some(l) = lines {
info_parts.push(format!("{} lines", l));
}
if let Some(p) = file_purpose {
info_parts.push(format!("[{}]", colorize_purpose(p)));
}
if let Some(l) = file_language {
info_parts.push(colorize_language(l).to_string());
}
let info = if info_parts.is_empty() {
String::new()
} else {
format!(" ({})", info_parts.join(", "))
};
let highlighted = highlight_match(name, &query);
println!(" {}{}{}",
if path.is_empty() { String::new() } else { format!("{}/", path).dimmed().to_string() },
highlighted,
info
);
if deps {
let file_path = if path.is_empty() {
PathBuf::from(name)
} else {
PathBuf::from(format!("{}/{}", path, name))
};
let internal = map.dependencies.internal.get(&file_path);
let external = map.dependencies.external.get(&file_path);
let has_internal = internal.map(|v| !v.is_empty()).unwrap_or(false);
let has_external = external.map(|v| !v.is_empty()).unwrap_or(false);
if has_internal || has_external {
let mut dep_parts = Vec::new();
if has_internal {
let int_deps: Vec<String> = internal.unwrap()
.iter()
.map(|p| {
let path_str = p.with_extension("").display().to_string();
path_str.strip_prefix("src/").unwrap_or(&path_str).to_string()
})
.collect();
dep_parts.push(format!("{}: {}", "internal".cyan(), int_deps.join(", ")));
}
if has_external {
let ext_deps = external.unwrap().join(", ");
dep_parts.push(format!("{}: {}", "external".yellow(), ext_deps));
}
for part in dep_parts {
println!(" {}", part);
}
}
}
}
println!();
println!("{}", format!("Search completed in {:?}", elapsed).dimmed());
Ok(())
}
fn find_matches(
node: &FileNode,
query: &str,
file_type: &Option<String>,
language: &Option<String>,
purpose: &Option<String>,
matches: &mut Vec<(String, String, Option<String>, Option<String>, Option<usize>, Option<usize>)>,
) {
match node {
FileNode::File { name, path, purpose: file_purpose, language: file_language, lines, chars, .. } => {
let name_lower = name.to_lowercase();
if let Some(ft) = file_type {
let ext = path.extension()
.and_then(|e| e.to_str())
.unwrap_or("");
if ext.to_lowercase() != *ft {
return;
}
}
if let Some(lang_filter) = language {
match file_language {
Some(file_lang) if file_lang.to_lowercase() == *lang_filter => {}
_ => return,
}
}
if let Some(purpose_filter) = purpose {
match file_purpose {
Some(fp) if fp.to_lowercase() == *purpose_filter => {}
_ => return,
}
}
if name_lower.contains(query) {
let parent = path.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
matches.push((
parent,
name.clone(),
file_purpose.clone(),
file_language.clone(),
*lines,
*chars,
));
}
}
FileNode::Directory { name, path, children, .. } => {
if file_type.is_none() && language.is_none() && purpose.is_none() {
let name_lower = name.to_lowercase();
if name_lower.contains(query) {
let parent = path.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
matches.push((
parent,
format!("{}/", name),
None,
None,
None,
None,
));
}
}
for child in children {
find_matches(child, query, file_type, language, purpose, matches);
}
}
FileNode::Collapsed { .. } => {
}
}
}
fn highlight_match(text: &str, query: &str) -> String {
let text_lower = text.to_lowercase();
if let Some(start) = text_lower.find(query) {
let end = start + query.len();
format!(
"{}{}{}",
&text[..start],
text[start..end].yellow().bold(),
&text[end..]
)
} else {
text.to_string()
}
}