use std::io::Write;
use std::path::Path;
use std::time::Instant;
use crate::graph;
use crate::manifest;
use crate::query;
use crate::query::json::J;
use crate::suggest;
use crate::telemetry;
use super::root::require_repo;
pub(crate) const EXIT_NO_RESULT: i32 = 3;
pub(crate) const ZERO_HIT_FIND: &str = "devscout find: zero hits — the manifest was searched and nothing matched. Not an error; fall back to text search (rg/grep) rather than rephrasing.";
pub(crate) const ZERO_HIT_REFS: &str = "devscout refs: zero hits — the graph was searched and no symbol matched. Not an error; fall back to text search (rg/grep) rather than rephrasing.";
pub(crate) const ZERO_HIT_READ: &str = "devscout read: zero hits — the graph was searched and no symbol matched. Not an error; fall back to text search (rg/grep) rather than rephrasing.";
pub(crate) const ZERO_HIT_IMPACT: &str = "devscout impact: zero hits — the graph was searched and no affected file came back. Not an error; fall back to text search (rg/grep) rather than rephrasing.";
pub(crate) const ZERO_HIT_TESTS: &str = "devscout tests: zero hits — the graph was searched and no symbol matched. Not an error; fall back to text search (rg/grep) rather than rephrasing.";
pub(crate) fn emit_zero_hit_note(code: i32, note: Option<&str>, cwd: &Path, query: Option<&str>) {
let Some(note) = note else { return };
if code != EXIT_NO_RESULT {
return;
}
let mut text = String::from(note);
let rows = query.map(|q| nearest_names(cwd, q)).unwrap_or_default();
if !rows.is_empty() {
text.push_str("\ndid you mean:");
for row in rows {
text.push('\n');
text.push_str(&row);
}
}
let stderr = std::io::stderr();
let mut lock = stderr.lock();
let _ = lock
.write_all(text.as_bytes())
.and_then(|()| lock.write_all(b"\n"));
}
pub(crate) fn emit_freshness_warning(cwd: &Path) {
let Some(root) = crate::repo::find_scout_root(cwd).or_else(|| crate::repo::find_repo_root(cwd))
else {
return;
};
let Some(warning) = manifest::freshness_warning(&root) else {
return;
};
let stderr = std::io::stderr();
let mut lock = stderr.lock();
let _ = lock
.write_all(warning.as_bytes())
.and_then(|()| lock.write_all(b"\n"));
}
fn nearest_names(cwd: &Path, query: &str) -> Vec<String> {
let Ok(root) = require_repo(cwd) else {
return Vec::new();
};
let Some(g) = graph::read_graph(&root) else {
return Vec::new();
};
suggest::suggestion_lines(&g.names, query)
}
fn ambiguous_json(q: &str, candidates: Vec<J>) -> String {
J::Obj(vec![
("schema_version", J::UInt(query::SCHEMA_VERSION)),
(
"outcome",
J::Str(query::Outcome::Ambiguous.as_str().to_string()),
),
("query", J::Str(q.to_string())),
("candidates", J::Arr(candidates)),
])
.to_json_string()
}
pub(crate) fn ambiguous_candidates_out(
index: &query::GraphIndex,
q: &str,
ids: &[String],
json: bool,
) -> (i32, String) {
let mut rows: Vec<(String, &String, &graph::Def)> = ids
.iter()
.map(|id| {
let d = index
.def(id)
.expect("ambiguous candidate id must resolve to a graph def");
(format!("{id} {}:{} {}", d.file, d.line, d.kind), id, d)
})
.collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
if json {
let candidates = rows
.iter()
.map(|(_, id, d)| {
J::Obj(vec![
("id", J::Str((*id).clone())),
("file", J::Str(d.file.clone())),
("line", J::UInt(d.line as u64)),
("kind", J::Str(d.kind.clone())),
])
})
.collect();
return (1, ambiguous_json(q, candidates));
}
let mut out = vec![format!(
"ambiguous symbol \"{q}\" — {} candidates:",
rows.len()
)];
out.extend(rows.into_iter().map(|(row, _, _)| row));
(1, out.join("\n"))
}
pub(crate) fn member_ambiguous_out(
q: &str,
candidates: &[query::MemberCandidate],
json: bool,
) -> (i32, String) {
if json {
let rows = candidates
.iter()
.map(|c| {
J::Obj(vec![
("owner", J::Str(c.owner.clone())),
("name", J::Str(c.name.clone())),
("file", J::Str(c.file.clone())),
("line", J::UInt(c.line as u64)),
])
})
.collect();
return (1, ambiguous_json(q, rows));
}
let mut out = vec![format!(
"ambiguous member \"{q}\" — {} candidates:",
candidates.len()
)];
for c in candidates {
out.push(format!("{}.{} {}:{}", c.owner, c.name, c.file, c.line));
}
(1, out.join("\n"))
}
pub(crate) fn fallback_advised_out(q: &str, json: bool, plain: String) -> (i32, String) {
if json {
let out = J::Obj(vec![
("schema_version", J::UInt(query::SCHEMA_VERSION)),
(
"outcome",
J::Str(query::Outcome::FallbackAdvised.as_str().to_string()),
),
("query", J::Str(q.to_string())),
])
.to_json_string();
(EXIT_NO_RESULT, out)
} else {
(EXIT_NO_RESULT, plain)
}
}
pub(crate) fn finish_query(
root: &Path,
verb: &'static str,
seed: &str,
start: Instant,
answer: (i32, String, query::Outcome, usize),
) -> (i32, String) {
let (code, out, outcome, candidate_count) = answer;
telemetry::record(
root,
&telemetry::QueryEvent {
verb,
seed,
outcome,
candidate_count,
},
start.elapsed(),
out.len(),
);
(code, out)
}