use std::path::{Path, PathBuf};
use serde_json::Value;
use super::locate;
use super::query::{self, Answer, Outcome, Question};
use super::roots::{AnalysisRoot, Fleet};
use crate::output::emit;
use crate::output::record::Record;
pub fn code(worktree: &Path, repository: Option<&Path>, question: &Question) -> Result<(), String> {
let omni = locate::find().map_err(|e| e.to_string())?;
let fleet = Fleet::discover(worktree)?;
let only = match repository {
None => None,
Some(r) => {
let r = crate::platform::canonicalize(r).map_err(|e| format!("{}: {e}", r.display()))?;
if !fleet.repositories().contains(&r) {
return Err(format!(
"{} is not a repository of this worktree; its repositories are {}",
r.display(),
fleet.repositories().iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", ")
));
}
Some(r)
}
};
let roots = fleet.analysis_roots(only.as_deref())?;
let base = fleet.repositories()[0].clone();
let answers = query::ask(&omni, &roots, question)?;
let mut unanswered = Vec::new();
for answer in &answers {
let label = label(&base, &answer.root);
show(&label, answer);
if !answer.outcome.answered() {
unanswered.push(label);
}
}
if unanswered.is_empty() {
return Ok(());
}
let not_indexed = answers.iter().any(|a| a.outcome == Outcome::NotIndexed);
Err(format!(
"{} of {} analysis roots did not answer: {}{}",
unanswered.len(),
answers.len(),
unanswered.join(", "),
if not_indexed { " (`wsb code index` analyzes them)" } else { "" }
))
}
fn label(base: &Path, root: &AnalysisRoot) -> String {
match root.dir.strip_prefix(base) {
Ok(rel) if rel.as_os_str().is_empty() => ".".to_owned(),
Ok(rel) => rel.to_string_lossy().replace('\\', "/"),
Err(_) => root.dir.display().to_string(),
}
}
fn show(label: &str, answer: &Answer) {
let record = |outcome: &str, status: Option<String>, results: Option<usize>, detail: Option<String>| {
emit(Record::CodeRoot { root: label.to_owned(), outcome: outcome.to_owned(), status, results, detail })
};
match &answer.outcome {
Outcome::Indexed(v) => {
let issues: Vec<String> = v["issues"]
.as_array()
.map(|a| a.iter().map(|i| i["issue"].as_str().or(i["kind"].as_str()).unwrap_or("issue").to_owned()).collect())
.unwrap_or_default();
let status = v["status"].as_str().unwrap_or("indexed").to_owned();
let detail = (!issues.is_empty()).then(|| issues.join(", "));
crate::outln!(
"{label}: {status}{}",
detail.as_ref().map(|d| format!(" ({d})")).unwrap_or_default()
);
record("indexed", Some(status), None, detail);
}
Outcome::Results { items, status, omissions } => {
record("answered", Some(status.clone()), Some(items.len()), None);
for item in items {
emit(Record::CodeItem { root: label.to_owned(), item: item.clone() });
crate::outln!("{}", line(&answer.root.dir, label, item));
}
if status != "complete" {
crate::outln!("{label}: the answer is {status}");
}
for o in omissions {
crate::outln!("{label}: omitted: {}", o["detail"].as_str().or(o["reason"].as_str()).unwrap_or(&o.to_string()));
}
}
Outcome::NoMatch { message } => {
crate::outln!("{label}: {message}");
record("no_match", None, Some(0), Some(message.clone()));
}
Outcome::NotIndexed => {
crate::outln!("{label}: not indexed");
record("not_indexed", None, None, None);
}
Outcome::Failed { code, message } => {
crate::errln!("{label}: {code}: {message}");
record("failed", None, None, Some(format!("{code}: {message}")));
}
}
}
fn at(dir: &Path, label: &str, span: &Value) -> String {
let path = span["path"].as_str().unwrap_or("?");
let shown: PathBuf = if label == "." { PathBuf::from(path) } else { Path::new(label).join(path) };
let start = span["range_utf8"][0].as_u64().unwrap_or(0) as usize;
match query::position(dir, path, span["content_hash"].as_str().unwrap_or(""), start) {
Some((line, column)) => format!("{}:{line}:{column}", shown.display()),
None => format!("{}@{start} (changed since it was analyzed)", shown.display()),
}
}
fn line(dir: &Path, label: &str, item: &Value) -> String {
let symbol = |s: &Value| {
format!(
"{} {} {}{}",
at(dir, label, &s["location"]),
s["kind"].as_str().unwrap_or("?"),
s["path"].as_str().unwrap_or("?"),
s["signature"].as_str().map(|sig| format!(" {sig}")).unwrap_or_default()
)
};
match item["item"].as_str() {
Some("symbol") => symbol(item),
Some("reference") => format!("{} {}", at(dir, label, &item["span"]), item["role"].as_str().unwrap_or("?")),
Some("node") => format!(
"{} (depth {}, {})",
symbol(&item["symbol"]),
item["depth"],
item["category"].as_str().unwrap_or("?")
),
_ if item["snippet"].is_string() => {
let path = item["span"]["path"].as_str().unwrap_or("?");
let shown = if label == "." { path.to_owned() } else { format!("{label}/{path}") };
format!("{shown}:{}:{} {}", item["line"], item["column"], item["snippet"].as_str().unwrap_or(""))
}
_ if item["symbol"].is_object() && item["rank"].is_number() => symbol(&item["symbol"]),
_ if item["primary"].is_object() => format!(
"{} {} {}: {} [{}]",
at(dir, label, &item["primary"]),
item["severity"].as_str().unwrap_or("?"),
item["code"].as_str().unwrap_or("-"),
item["message"].as_str().unwrap_or(""),
item["source"].as_str().unwrap_or("?")
),
_ => format!("{label}: a result this wsb does not know how to show: {item}"),
}
}
#[cfg(test)]
#[path = "cli_tests.rs"]
mod tests;