use std::path::Path;
use std::time::Instant;
use crate::graph;
use crate::manifest;
use crate::query;
use super::answer::{finish_query, EXIT_NO_RESULT};
use super::root::require_repo;
const FIND_FULL_CAP: usize = 25;
const FIND_FALLBACK_CAP: usize = 10;
const FIND_NAMES_CAP: usize = 25;
pub(crate) fn cmd_find(cwd: &Path, query_str: &str, resources: bool) -> (i32, String) {
let start = Instant::now();
let root = match require_repo(cwd) {
Ok(r) => r,
Err(e) => return (1, format!("error: {e}")),
};
if query_str.is_empty() {
return (2, "usage: devscout find <query> [--resources]".to_string());
}
let graph = graph::read_graph(&root);
let inbound_counts = graph
.as_ref()
.map(query::file_inbound_counts)
.unwrap_or_default();
let (decl_lines, resource_count): (Vec<String>, usize) = match graph.as_ref() {
None => (Vec::new(), 0),
Some(g) => {
let named = query::find_names(g, query_str);
let mut primary: Vec<&graph::GraphName> = Vec::new();
let mut resource_count = 0usize;
for n in named.iter() {
if query::name_tier(&n.kind) <= 2 {
primary.push(n);
} else {
resource_count += 1;
}
}
let pool: Vec<&graph::GraphName> = if resources { named } else { primary };
let mut out: Vec<String> = pool
.iter()
.take(FIND_NAMES_CAP)
.map(|n| {
let text = query::source_line(&root, &n.file, n.line);
if text.is_empty() {
format!("{}:{}", n.file, n.line)
} else {
format!("{}:{} {text}", n.file, n.line)
}
})
.collect();
if pool.len() > FIND_NAMES_CAP {
out.push(format!(
"… +{} more declarations (refine query)",
pool.len() - FIND_NAMES_CAP
));
}
(out, resource_count)
}
};
match manifest::find_in_manifest_detailed(&root, query_str, &inbound_counts) {
Ok(r) => {
let answer = find_result_out(
graph.as_ref(),
query_str,
resources,
resource_count,
decl_lines,
r,
);
finish_query(&root, "find", query_str, start, false, answer)
}
Err(e) => (1, format!("error: {e}")),
}
}
fn find_result_out(
graph: Option<&graph::Graph>,
query_str: &str,
resources: bool,
resource_count: usize,
decl_lines: Vec<String>,
r: manifest::FindResult,
) -> (i32, String, query::Outcome, usize) {
if r.hits.is_empty() && decl_lines.is_empty() {
let out =
format!("no matches for \"{query_str}\" (run 'devscout map' if manifest is missing)");
return (EXIT_NO_RESULT, out, query::Outcome::FallbackAdvised, 0);
}
let cap = if r.fallback {
FIND_FALLBACK_CAP
} else {
FIND_FULL_CAP
};
let candidate_count = decl_lines.len().min(FIND_NAMES_CAP) + r.hits.len().min(cap);
let mut lines: Vec<String> = decl_lines;
if !resources && resource_count > 0 {
lines.push(format!(
"+{resource_count} resource-key hits, use --resources"
));
}
let decl_line_by_file = graph
.map(query::first_decl_line_by_file)
.unwrap_or_default();
lines.extend(r.hits.iter().take(cap).map(|h| {
let purpose = h.purpose.as_deref().unwrap_or("undefined");
let agent = if h.source == "agent" { " [agent]" } else { "" };
let line = decl_line_by_file.get(&h.path).copied().unwrap_or(1);
format!("{}:{line}: {purpose}{agent}", h.path)
}));
if r.hits.len() > cap {
lines.push(format!("… +{} more (refine query)", r.hits.len() - cap));
}
(0, lines.join("\n"), query::Outcome::Hit, candidate_count)
}