use anyhow::{bail, Result};
use clap::Args;
use std::path::Path;
use crate::commands::warn_if_stale;
use crate::core::graph::{Graph, Symbol};
use crate::output::formatter;
use crate::output::json::JsonOutput;
fn compact_symbol(s: &Symbol) -> serde_json::Value {
serde_json::json!({
"name": s.name,
"kind": s.kind,
"signature": s.signature,
"file_path": s.file_path,
"line_start": s.line_start,
"line_end": s.line_end,
})
}
fn compact_symbols(syms: &[&Symbol]) -> Vec<serde_json::Value> {
syms.iter().map(|s| compact_symbol(s)).collect()
}
#[derive(Args, Debug)]
pub struct SketchArgs {
pub symbol: String,
#[arg(long, short = 'j')]
pub json: bool,
#[arg(long, default_value = "50")]
pub limit: usize,
#[arg(long)]
pub no_docs: bool,
#[arg(long)]
pub file: bool,
#[arg(long)]
pub compact: bool,
}
use super::looks_like_file_path;
pub fn run(args: &SketchArgs, project_root: &Path) -> Result<()> {
let scope_dir = project_root.join(".scope");
if !scope_dir.exists() {
bail!("No .scope/ directory found. Run 'scope init' first.");
}
let db_path = scope_dir.join("graph.db");
if !db_path.exists() {
bail!("No index found. Run 'scope index' to build one first.");
}
let graph = Graph::open(&db_path)?;
warn_if_stale(&graph, project_root);
if args.file || looks_like_file_path(&args.symbol) {
return run_file_sketch(args, &graph);
}
run_symbol_sketch(args, &graph)
}
fn run_symbol_sketch(args: &SketchArgs, graph: &Graph) -> Result<()> {
let symbol = graph.find_symbol(&args.symbol)?.ok_or_else(|| {
anyhow::anyhow!(
"Symbol '{}' not found in index.\n\
Tip: Check spelling, or use 'scope find \"{}\"' for semantic search.",
args.symbol,
args.symbol
)
})?;
match symbol.kind.as_str() {
"class" | "struct" => sketch_class(args, graph, &symbol),
"method" | "function" => sketch_method(args, graph, &symbol),
"interface" => sketch_interface(args, graph, &symbol),
"enum" => sketch_enum(args, graph, &symbol),
_ => sketch_generic(args, &symbol),
}
}
fn sketch_class(
args: &SketchArgs,
graph: &Graph,
symbol: &crate::core::graph::Symbol,
) -> Result<()> {
let methods = graph.get_methods(&symbol.id)?;
let relationships = graph.get_class_relationships(&symbol.id)?;
let method_ids: Vec<&str> = methods.iter().map(|m| m.id.as_str()).collect();
let caller_counts = graph.get_caller_counts(&method_ids)?;
if args.json || args.compact {
let (fields, all_methods): (Vec<_>, Vec<_>) =
methods.iter().partition(|m| m.kind == "property");
let truncated = all_methods.len() > args.limit;
let total = all_methods.len();
let actual_methods: Vec<_> = all_methods.into_iter().take(args.limit).collect();
let field_data: Vec<serde_json::Value> = fields
.iter()
.map(|f| {
serde_json::json!({
"name": f.name,
"signature": f.signature,
"line_start": f.line_start,
})
})
.collect();
let (sym_data, method_data) = if args.compact {
(
compact_symbol(symbol),
serde_json::json!(compact_symbols(&actual_methods)),
)
} else {
(serde_json::json!(symbol), serde_json::json!(actual_methods))
};
let data = serde_json::json!({
"symbol": sym_data,
"methods": method_data,
"fields": field_data,
"caller_counts": caller_counts,
"relationships": relationships,
});
let output = JsonOutput {
command: "sketch",
symbol: Some(symbol.name.clone()),
data,
truncated,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_class_sketch(
symbol,
&methods,
&caller_counts,
&relationships,
args.limit,
!args.no_docs,
);
}
Ok(())
}
fn sketch_method(
args: &SketchArgs,
graph: &Graph,
symbol: &crate::core::graph::Symbol,
) -> Result<()> {
let outgoing_calls = graph.get_outgoing_calls(&symbol.id)?;
let incoming_callers = graph.get_incoming_callers(&symbol.id)?;
if args.json || args.compact {
let sym = if args.compact {
compact_symbol(symbol)
} else {
serde_json::json!(symbol)
};
let data = serde_json::json!({
"symbol": sym,
"calls": outgoing_calls,
"called_by": incoming_callers,
});
let output = JsonOutput {
command: "sketch",
symbol: Some(symbol.name.clone()),
data,
truncated: false,
total: 1,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_method_sketch(symbol, &outgoing_calls, &incoming_callers);
}
Ok(())
}
fn sketch_interface(
args: &SketchArgs,
graph: &Graph,
symbol: &crate::core::graph::Symbol,
) -> Result<()> {
let methods = graph.get_methods(&symbol.id)?;
let implementors = graph.get_implementors(&symbol.id)?;
if args.json || args.compact {
let sym = if args.compact {
compact_symbol(symbol)
} else {
serde_json::json!(symbol)
};
let truncated = methods.len() > args.limit;
let total = methods.len();
let limited: Vec<_> = methods.iter().take(args.limit).collect();
let meths = if args.compact {
serde_json::json!(compact_symbols(&limited))
} else {
serde_json::json!(limited)
};
let data = serde_json::json!({
"symbol": sym,
"methods": meths,
"implementors": implementors,
});
let output = JsonOutput {
command: "sketch",
symbol: Some(symbol.name.clone()),
data,
truncated,
total,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_interface_sketch(symbol, &methods, &implementors, args.limit);
}
Ok(())
}
fn sketch_enum(
args: &SketchArgs,
graph: &Graph,
symbol: &crate::core::graph::Symbol,
) -> Result<()> {
let children = graph.get_methods(&symbol.id)?;
let variants: Vec<&crate::core::graph::Symbol> =
children.iter().filter(|c| c.kind == "variant").collect();
let caller_count = graph.get_caller_count(&symbol.id)?;
if args.json || args.compact {
let variant_data: Vec<serde_json::Value> = variants
.iter()
.map(|v| {
serde_json::json!({
"name": v.name,
"signature": v.signature,
"line_start": v.line_start,
"line_end": v.line_end,
})
})
.collect();
let sym = if args.compact {
compact_symbol(symbol)
} else {
serde_json::json!(symbol)
};
let data = serde_json::json!({
"symbol": sym,
"variants": variant_data,
"caller_count": caller_count,
});
let output = JsonOutput {
command: "sketch",
symbol: Some(symbol.name.clone()),
data,
truncated: false,
total: 1,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_enum_sketch(symbol, &variants, caller_count);
}
Ok(())
}
fn sketch_generic(args: &SketchArgs, symbol: &crate::core::graph::Symbol) -> Result<()> {
if args.json || args.compact {
let sym = if args.compact {
compact_symbol(symbol)
} else {
serde_json::json!(symbol)
};
let data = serde_json::json!({
"symbol": sym,
});
let output = JsonOutput {
command: "sketch",
symbol: Some(symbol.name.clone()),
data,
truncated: false,
total: 1,
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_generic_sketch(symbol);
}
Ok(())
}
fn run_file_sketch(args: &SketchArgs, graph: &Graph) -> Result<()> {
let file_path = formatter::normalize_path(&args.symbol);
let symbols = graph.get_file_symbols(&file_path)?;
if symbols.is_empty() {
bail!(
"No symbols found for file '{}'.\n\
Tip: Check the path is relative to the project root. Run 'scope index' if the file is new.",
file_path
);
}
let symbol_ids: Vec<&str> = symbols.iter().map(|s| s.id.as_str()).collect();
let caller_counts = graph.get_caller_counts(&symbol_ids)?;
if args.json || args.compact {
let sym_data = if args.compact {
serde_json::json!(symbols.iter().map(compact_symbol).collect::<Vec<_>>())
} else {
serde_json::json!(symbols)
};
let data = serde_json::json!({
"file_path": file_path,
"symbols": sym_data,
"caller_counts": caller_counts,
});
let output = JsonOutput {
command: "sketch",
symbol: Some(file_path.clone()),
data,
truncated: false,
total: symbols.len(),
};
println!("{}", serde_json::to_string_pretty(&output)?);
} else {
formatter::print_file_sketch(&file_path, &symbols, &caller_counts);
}
Ok(())
}