use std::env;
use ctx::error::Result;
use ctx::index;
pub fn run_source(
symbol: &str,
file_pattern: Option<&str>,
kind_filter: Option<&str>,
) -> Result<()> {
let root = env::current_dir()?;
let db = index::open_database(&root)?;
if let Some(src) = db.get_source(symbol)? {
println!("// Source: {}", symbol);
println!("{}", src);
return Ok(());
}
let symbols = db.find_symbols_filtered(symbol, 100, file_pattern, kind_filter)?;
if symbols.is_empty() {
eprintln!("Symbol '{}' not found", symbol);
if file_pattern.is_some() || kind_filter.is_some() {
eprintln!(
"Try removing filters or use 'ctx query find {}' to see all matches",
symbol
);
}
return Ok(());
}
if symbols.len() > 1 && file_pattern.is_none() && kind_filter.is_none() {
eprintln!(
"Found {} symbols named '{}'. Use --file or --kind to disambiguate:\n",
symbols.len(),
symbol
);
for s in symbols.iter().take(10) {
eprintln!(
" {} ({}) - {}:{}",
s.name,
s.kind.as_str(),
s.file_path,
s.line_start
);
}
if symbols.len() > 10 {
eprintln!(" ... and {} more", symbols.len() - 10);
}
eprintln!(
"\nExample: ctx source {} --file \"{}\"",
symbol, symbols[0].file_path
);
return Ok(());
}
let sym = &symbols[0];
match db.get_source(&sym.id)? {
Some(src) => {
println!("// Source: {}", sym.id);
println!("{}", src);
}
None => {
eprintln!("Source code not available for '{}'", sym.id);
}
}
Ok(())
}
pub fn run_explain(
symbol: &str,
file_pattern: Option<&str>,
kind_filter: Option<&str>,
json: bool,
) -> Result<()> {
let root = env::current_dir()?;
let db = index::open_database(&root)?;
let symbols = db.find_symbols_filtered(symbol, 100, file_pattern, kind_filter)?;
if symbols.is_empty() {
if json {
return ctx::json::emit("explain", explain_not_found_data(&[]));
}
eprintln!("Symbol '{}' not found", symbol);
if file_pattern.is_some() || kind_filter.is_some() {
eprintln!(
"Try removing filters or use 'ctx query find {}' to see all matches",
symbol
);
}
return Ok(());
}
if symbols.len() > 1 && file_pattern.is_none() && kind_filter.is_none() {
if json {
return ctx::json::emit("explain", explain_not_found_data(&symbols));
}
eprintln!(
"Found {} symbols named '{}'. Use --file or --kind to disambiguate:\n",
symbols.len(),
symbol
);
for s in symbols.iter().take(10) {
eprintln!(
" {} ({}) - {}:{}",
s.name,
s.kind.as_str(),
s.file_path,
s.line_start
);
}
if symbols.len() > 10 {
eprintln!(" ... and {} more", symbols.len() - 10);
}
eprintln!(
"\nExample: ctx explain {} --file \"{}\"",
symbol, symbols[0].file_path
);
return Ok(());
}
let sym = &symbols[0];
if json {
let callers = db.get_incoming_edges(&sym.name)?;
let deps = db.get_outgoing_edges(&sym.id)?;
return ctx::json::emit("explain", explain_data(sym, callers.len(), deps.len()));
}
println!("Symbol: {}", sym.name);
println!("{}", "=".repeat(60));
println!("Kind: {}", sym.kind.as_str());
println!("File: {}:{}", sym.file_path, sym.line_start);
println!("Visibility: {}", sym.visibility.as_str());
if let Some(ref sig) = sym.signature {
println!("\nSignature:");
println!(" {}", sig);
}
if let Some(ref brief) = sym.brief {
println!("\nDescription:");
println!(" {}", brief);
}
let callers = db.get_incoming_edges(&sym.name)?;
if !callers.is_empty() {
println!("\nCalled by ({}):", callers.len());
for edge in callers.iter().take(10) {
if let Some(caller) = db.get_symbol(&edge.source_id)? {
println!(
" {} ({}:{})",
caller.name,
caller.file_path,
edge.line.unwrap_or(0)
);
}
}
if callers.len() > 10 {
println!(" ... and {} more", callers.len() - 10);
}
}
let deps = db.get_outgoing_edges(&sym.id)?;
if !deps.is_empty() {
println!("\nCalls ({}):", deps.len());
for edge in deps.iter().take(10) {
println!(" {} [{}]", edge.target_name, edge.kind.as_str());
}
if deps.len() > 10 {
println!(" ... and {} more", deps.len() - 10);
}
}
Ok(())
}
fn explain_data(
sym: &ctx::db::Symbol,
callers_count: usize,
deps_count: usize,
) -> serde_json::Value {
serde_json::json!({
"symbol": ctx::json::SymbolRef::from(sym),
"visibility": sym.visibility.as_str(),
"signature": sym.signature,
"brief": sym.brief,
"docstring": sym.docstring,
"callers_count": callers_count,
"deps_count": deps_count,
"ambiguous": [],
})
}
fn explain_not_found_data(ambiguous: &[ctx::db::Symbol]) -> serde_json::Value {
serde_json::json!({
"symbol": serde_json::Value::Null,
"visibility": serde_json::Value::Null,
"signature": serde_json::Value::Null,
"brief": serde_json::Value::Null,
"docstring": serde_json::Value::Null,
"callers_count": 0,
"deps_count": 0,
"ambiguous": ambiguous
.iter()
.map(ctx::json::SymbolRef::from)
.collect::<Vec<_>>(),
})
}
#[cfg(test)]
mod tests {
use super::*;
use ctx::db::{Symbol, SymbolKind, Visibility};
#[test]
fn test_explain_data_payload() {
let sym = Symbol {
id: "src/a.rs::run".to_string(),
file_path: "src/a.rs".to_string(),
name: "run".to_string(),
qualified_name: Some("App::run".to_string()),
kind: SymbolKind::Method,
visibility: Visibility::Public,
signature: Some("fn run(&self) -> Result<()>".to_string()),
brief: Some("Run the app".to_string()),
docstring: None,
line_start: 7,
line_end: 30,
col_start: 0,
col_end: 1,
parent_id: None,
source: None,
};
let data = explain_data(&sym, 3, 5);
assert_eq!(data["symbol"]["name"], "run");
assert_eq!(data["symbol"]["qualified_name"], "App::run");
assert_eq!(data["symbol"]["kind"], "method");
assert_eq!(data["visibility"], "public");
assert_eq!(data["signature"], "fn run(&self) -> Result<()>");
assert_eq!(data["callers_count"], 3);
assert_eq!(data["deps_count"], 5);
assert_eq!(data["ambiguous"], serde_json::json!([]));
let not_found = explain_not_found_data(&[]);
assert!(not_found["symbol"].is_null());
assert_eq!(not_found["callers_count"], 0);
let ambiguous = explain_not_found_data(std::slice::from_ref(&sym));
assert_eq!(ambiguous["ambiguous"][0]["name"], "run");
}
}