use crate::args::Cli;
use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph_for_cli};
use crate::commands::impact::{emit_ambiguous_symbol_error, emit_symbol_not_found};
use crate::index_discovery::find_nearest_index;
use crate::output::OutputStreams;
use anyhow::{Context, Result, anyhow};
use serde::Serialize;
use sqry_core::graph::unified::FileScope;
use sqry_core::graph::unified::concurrent::GraphSnapshot;
use sqry_core::graph::unified::resolution::SymbolResolveError;
use sqry_core::graph::unified::storage::{FileRegistry, NodeEntry};
#[derive(Debug, Serialize)]
struct ExplainOutput {
name: String,
qualified_name: String,
kind: String,
file: String,
line: u32,
language: String,
visibility: Option<String>,
documentation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
context: Option<SymbolContext>,
}
#[derive(Debug, Serialize)]
struct SymbolContext {
code: String,
start_line: u32,
end_line: u32,
}
fn resolve_symbol_by_file_and_name(
snapshot: &GraphSnapshot,
file_path: &std::path::Path,
symbol_name: &str,
) -> Result<sqry_core::graph::unified::NodeId, SymbolResolveError> {
snapshot.resolve_global_symbol_ambiguity_aware(symbol_name, FileScope::Path(file_path))
}
fn build_symbol_context(
index_root: &std::path::Path,
files_registry: &FileRegistry,
entry: &NodeEntry,
) -> Option<SymbolContext> {
let file_to_read = files_registry
.resolve(entry.file)
.map(|p| index_root.join(p.as_ref()));
let file_to_read = file_to_read?;
let content = std::fs::read_to_string(&file_to_read).ok()?;
let lines: Vec<&str> = content.lines().collect();
let start = entry.start_line.saturating_sub(1) as usize;
let end = (entry.end_line as usize).min(lines.len());
if start < lines.len() {
let code_lines: Vec<&str> = lines[start..end].to_vec();
Some(SymbolContext {
code: code_lines.join("\n"),
start_line: entry.start_line,
end_line: entry.end_line,
})
} else {
None
}
}
pub fn run_explain(
cli: &Cli,
file_path: &str,
symbol_name: &str,
path: Option<&str>,
include_context: bool,
_include_relations: bool,
) -> Result<()> {
let mut streams = OutputStreams::new();
let search_path = path.map_or_else(
|| std::env::current_dir().unwrap_or_default(),
std::path::PathBuf::from,
);
let index_location = find_nearest_index(&search_path);
let Some(ref loc) = index_location else {
streams
.write_diagnostic("No .sqry-index found. Run 'sqry index' first to build the index.")?;
return Ok(());
};
let config = GraphLoadConfig::default();
let graph = load_unified_graph_for_cli(&loc.index_root, &config, cli)
.context("Failed to load graph. Run 'sqry index' to build the graph.")?;
let snapshot = graph.snapshot();
let strings = snapshot.strings();
let files_registry = snapshot.files();
let requested_file_path = if std::path::Path::new(file_path).is_absolute() {
std::path::PathBuf::from(file_path)
} else {
loc.index_root.join(file_path)
};
let node_id =
match resolve_symbol_by_file_and_name(&snapshot, &requested_file_path, symbol_name) {
Ok(id) => id,
Err(SymbolResolveError::Ambiguous(err)) => {
let exit_code = emit_ambiguous_symbol_error(&mut streams, &err, cli.json);
std::process::exit(exit_code);
}
Err(SymbolResolveError::NotFound { name }) => {
let exit_code = emit_symbol_not_found(&mut streams, &name, cli.json);
std::process::exit(exit_code);
}
};
let symbol_entry = snapshot
.get_node(node_id)
.ok_or_else(|| anyhow!("Symbol node not found in graph"))?;
let file_path_resolved = files_registry
.resolve(symbol_entry.file)
.map(|p| p.display().to_string())
.unwrap_or_default();
let language = files_registry
.language_for_file(symbol_entry.file)
.map_or_else(|| "Unknown".to_string(), |l| l.to_string());
let name = strings
.resolve(symbol_entry.name)
.map(|s| s.to_string())
.unwrap_or_default();
let qualified_name = symbol_entry
.qualified_name
.and_then(|id| strings.resolve(id))
.map_or_else(|| name.clone(), |s| s.to_string());
let visibility = symbol_entry
.visibility
.and_then(|id| strings.resolve(id))
.map(|s| s.to_string());
let documentation = symbol_entry
.doc
.and_then(|id| strings.resolve(id))
.map(|s| s.to_string());
let context = if include_context {
build_symbol_context(&loc.index_root, files_registry, symbol_entry)
} else {
None
};
let output = ExplainOutput {
name,
qualified_name,
kind: format!("{:?}", symbol_entry.kind),
file: file_path_resolved,
line: symbol_entry.start_line,
language,
visibility,
documentation,
context,
};
if cli.json {
let json = serde_json::to_string_pretty(&output).context("Failed to serialize to JSON")?;
streams.write_result(&json)?;
} else {
let text = format_explain_text(&output);
streams.write_result(&text)?;
}
Ok(())
}
fn format_explain_text(output: &ExplainOutput) -> String {
let mut lines = Vec::new();
lines.push(format!("Symbol: {}", output.qualified_name));
lines.push(format!(" Kind: {}", output.kind));
lines.push(format!(" File: {}:{}", output.file, output.line));
lines.push(format!(" Language: {}", output.language));
if let Some(ref vis) = output.visibility {
lines.push(format!(" Visibility: {vis}"));
}
if let Some(ref doc) = output.documentation {
lines.push(format!(" Documentation: {doc}"));
}
if let Some(ref ctx) = output.context {
lines.push(String::new());
lines.push(format!("Code (lines {}-{}):", ctx.start_line, ctx.end_line));
for (i, line) in ctx.code.lines().enumerate() {
lines.push(format!("{:4} | {}", ctx.start_line as usize + i, line));
}
}
lines.join("\n")
}