use std::path::Path;
use serde_json::Value;
use super::locate::Omni;
use super::roots::AnalysisRoot;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SearchMode {
Literal,
Regex,
Symbols,
Structural,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Question {
Index,
Search { pattern: String, mode: SearchMode, include: Vec<String>, exclude: Vec<String> },
Symbols { pattern: String, kind: Option<String> },
Refs { target: String },
Callers { target: String, depth: u32 },
Callees { target: String, depth: u32 },
Diagnostics { file: Option<String> },
}
#[derive(Debug, Clone, PartialEq)]
pub enum Outcome {
Indexed(Value),
Results { items: Vec<Value>, status: String, omissions: Vec<Value> },
NoMatch { message: String },
NotIndexed,
Failed { code: String, message: String },
}
impl Outcome {
pub fn answered(&self) -> bool {
matches!(self, Self::Indexed(_) | Self::Results { .. } | Self::NoMatch { .. })
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Answer {
pub root: AnalysisRoot,
pub outcome: Outcome,
}
const QUALIFIED_EXITS: [i32; 4] = [3, 10, 11, 12];
enum Printed {
Ok(Value),
Error { code: String, message: String },
}
fn run(omni: &Omni, args: &[String]) -> Result<Printed, String> {
let out = crate::git_backend::command_without_repo_scope(&omni.path)
.arg("--json")
.args(args)
.output()
.map_err(|e| format!("cannot run {}: {e}", omni.path.display()))?;
let text = String::from_utf8_lossy(&out.stdout);
let value: Value = serde_json::from_str(&text).map_err(|e| {
format!(
"`omni {}` did not answer in JSON ({e}): {}",
args.join(" "),
String::from_utf8_lossy(&out.stderr).trim()
)
})?;
if out.status.success() {
return Ok(Printed::Ok(value));
}
match (value["code"].as_str(), value["message"].as_str()) {
(Some(code), Some(message)) => Ok(Printed::Error { code: code.to_owned(), message: message.to_owned() }),
_ if out.status.code().is_some_and(|c| QUALIFIED_EXITS.contains(&c)) => Ok(Printed::Ok(value)),
_ => Err(format!("`omni {}` failed ({}) with no error envelope: {value}", args.join(" "), out.status)),
}
}
fn pages(omni: &Omni, args: Vec<String>) -> Result<Outcome, String> {
let mut items = Vec::new();
let mut omissions = Vec::new();
let mut status = String::from("complete");
let mut cursor: Option<String> = None;
loop {
let mut page = args.clone();
if let Some(c) = &cursor {
page.extend(["--cursor".to_owned(), c.clone()]);
}
let v = match run(omni, &page)? {
Printed::Ok(v) => v,
Printed::Error { code, message } if code == "not_found" => return Ok(Outcome::NoMatch { message }),
Printed::Error { code, message } => return Ok(Outcome::Failed { code, message }),
};
let results = v["results"]
.as_array()
.ok_or_else(|| format!("`omni {}` answered without results: {v}", args.join(" ")))?;
items.extend(results.iter().cloned());
omissions.extend(v["omissions"].as_array().cloned().unwrap_or_default());
match v["status"].as_str() {
Some(s) if s != "complete" => status = s.to_owned(),
_ => {}
}
match v["next_cursor"].as_str() {
Some(next) => cursor = Some(next.to_owned()),
None => return Ok(Outcome::Results { items, status, omissions }),
}
}
}
fn root_arg(root: &AnalysisRoot) -> Vec<String> {
vec!["-C".to_owned(), root.dir.display().to_string()]
}
fn indexed(omni: &Omni, root: &AnalysisRoot) -> Result<bool, String> {
let mut args = vec!["status".to_owned()];
args.extend(root_arg(root));
match run(omni, &args)? {
Printed::Ok(v) => Ok(v["analyses"].as_array().is_some_and(|a| !a.is_empty())),
Printed::Error { code, message } => Err(format!("`omni status` at {}: {code}: {message}", root.dir.display())),
}
}
fn ask_one(omni: &Omni, root: &AnalysisRoot, question: &Question) -> Result<Outcome, String> {
if let Question::Index = question {
let mut args = vec!["index".to_owned(), root.dir.display().to_string()];
for g in &root.exclude {
args.extend(["--exclude".to_owned(), g.clone()]);
}
return Ok(match run(omni, &args)? {
Printed::Ok(v) => Outcome::Indexed(v),
Printed::Error { code, message } => Outcome::Failed { code, message },
});
}
if !indexed(omni, root)? {
return Ok(Outcome::NotIndexed);
}
let mut args: Vec<String> = match question {
Question::Index => unreachable!("answered above"),
Question::Search { pattern, mode, include, exclude } => {
let mut a = vec!["search".to_owned(), pattern.clone()];
match mode {
SearchMode::Literal => {}
SearchMode::Regex => a.push("--regex".to_owned()),
SearchMode::Symbols => a.push("--symbols".to_owned()),
SearchMode::Structural => a.push("--structural".to_owned()),
}
for g in include {
a.extend(["--include".to_owned(), g.clone()]);
}
for g in exclude {
a.extend(["--exclude".to_owned(), g.clone()]);
}
a
}
Question::Symbols { pattern, kind } => {
let mut a = vec!["symbols".to_owned(), pattern.clone()];
if let Some(k) = kind {
a.extend(["--kind".to_owned(), k.clone()]);
}
a
}
Question::Refs { target } => vec!["refs".to_owned(), target.clone()],
Question::Callers { target, depth } => {
vec!["callers".to_owned(), target.clone(), "--depth".to_owned(), depth.to_string()]
}
Question::Callees { target, depth } => {
vec!["callees".to_owned(), target.clone(), "--depth".to_owned(), depth.to_string()]
}
Question::Diagnostics { file } => {
let mut a = vec!["diagnostics".to_owned()];
if let Some(f) = file {
a.extend(["--file".to_owned(), f.clone()]);
}
a
}
};
args.extend(root_arg(root));
match question {
Question::Callers { .. } | Question::Callees { .. } => match run(omni, &args)? {
Printed::Ok(v) => Ok(Outcome::Results {
items: v["results"].as_array().cloned().unwrap_or_default(),
status: v["status"].as_str().unwrap_or("complete").to_owned(),
omissions: v["omissions"].as_array().cloned().unwrap_or_default(),
}),
Printed::Error { code, message } if code == "not_found" => Ok(Outcome::NoMatch { message }),
Printed::Error { code, message } => Ok(Outcome::Failed { code, message }),
},
Question::Diagnostics { .. } => match run(omni, &args)? {
Printed::Ok(Value::Array(items)) => {
Ok(Outcome::Results { items, status: "complete".to_owned(), omissions: Vec::new() })
}
Printed::Ok(other) => Err(format!("`omni diagnostics` answered with no list: {other}")),
Printed::Error { code, message } => Ok(Outcome::Failed { code, message }),
},
_ => pages(omni, args),
}
}
pub fn ask(omni: &Omni, roots: &[AnalysisRoot], question: &Question) -> Result<Vec<Answer>, String> {
roots
.iter()
.map(|root| Ok(Answer { root: root.clone(), outcome: ask_one(omni, root, question)? }))
.collect()
}
pub fn position(dir: &Path, path: &str, content_hash: &str, offset: usize) -> Option<(usize, usize)> {
let bytes = std::fs::read(dir.join(path)).ok()?;
let hex = content_hash.strip_prefix("sha256:").unwrap_or(content_hash);
use sha2::{Digest, Sha256};
if format!("{:x}", Sha256::digest(&bytes)) != hex {
return None;
}
let text = std::str::from_utf8(&bytes).ok()?;
let before = text.get(..offset)?;
let line = before.matches('\n').count() + 1;
let column = before.rsplit('\n').next().unwrap_or("").chars().count() + 1;
Some((line, column))
}
#[cfg(test)]
#[path = "query_tests.rs"]
mod tests;