sqry_cli/commands/
explain.rs1use crate::args::Cli;
6use crate::commands::graph::loader::{GraphLoadConfig, load_unified_graph_for_cli};
7use crate::index_discovery::find_nearest_index;
8use crate::output::OutputStreams;
9use anyhow::{Context, Result, anyhow};
10use serde::Serialize;
11use sqry_core::graph::unified::concurrent::GraphSnapshot;
12use sqry_core::graph::unified::storage::{FileRegistry, NodeEntry};
13use sqry_core::graph::unified::{FileScope, ResolutionMode, SymbolQuery, SymbolResolutionOutcome};
14
15#[derive(Debug, Serialize)]
17struct ExplainOutput {
18 name: String,
20 qualified_name: String,
22 kind: String,
24 file: String,
26 line: u32,
28 language: String,
30 visibility: Option<String>,
32 documentation: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 context: Option<SymbolContext>,
37}
38
39#[derive(Debug, Serialize)]
40struct SymbolContext {
41 code: String,
43 start_line: u32,
45 end_line: u32,
47}
48
49fn resolve_symbol_by_file_and_name(
51 snapshot: &GraphSnapshot,
52 file_path: &std::path::Path,
53 symbol_name: &str,
54) -> Result<sqry_core::graph::unified::NodeId> {
55 let query = SymbolQuery {
56 symbol: symbol_name,
57 file_scope: FileScope::Path(file_path),
58 mode: ResolutionMode::Strict,
59 };
60
61 let witness = snapshot.resolve_symbol_with_witness(&query);
62 match witness.outcome {
63 SymbolResolutionOutcome::Resolved(node_id) => Ok(node_id),
64 SymbolResolutionOutcome::NotFound => Err(anyhow!(
65 "Symbol '{symbol_name}' not found in '{}'",
66 file_path.display()
67 )),
68 SymbolResolutionOutcome::FileNotIndexed => {
69 Err(anyhow!("File '{}' is not indexed", file_path.display()))
70 }
71 SymbolResolutionOutcome::Ambiguous(_) => Err(anyhow!(
72 "Symbol '{symbol_name}' is ambiguous in '{}'",
73 file_path.display()
74 )),
75 }
76}
77
78fn build_symbol_context(
80 index_root: &std::path::Path,
81 files_registry: &FileRegistry,
82 entry: &NodeEntry,
83) -> Option<SymbolContext> {
84 let file_to_read = files_registry
85 .resolve(entry.file)
86 .map(|p| index_root.join(p.as_ref()));
87
88 let file_to_read = file_to_read?;
89 let content = std::fs::read_to_string(&file_to_read).ok()?;
90 let lines: Vec<&str> = content.lines().collect();
91 let start = entry.start_line.saturating_sub(1) as usize;
92 let end = (entry.end_line as usize).min(lines.len());
93
94 if start < lines.len() {
95 let code_lines: Vec<&str> = lines[start..end].to_vec();
96 Some(SymbolContext {
97 code: code_lines.join("\n"),
98 start_line: entry.start_line,
99 end_line: entry.end_line,
100 })
101 } else {
102 None
103 }
104}
105
106pub fn run_explain(
111 cli: &Cli,
112 file_path: &str,
113 symbol_name: &str,
114 path: Option<&str>,
115 include_context: bool,
116 _include_relations: bool,
117) -> Result<()> {
118 let mut streams = OutputStreams::new();
119
120 let search_path = path.map_or_else(
122 || std::env::current_dir().unwrap_or_default(),
123 std::path::PathBuf::from,
124 );
125
126 let index_location = find_nearest_index(&search_path);
127 let Some(ref loc) = index_location else {
128 streams
129 .write_diagnostic("No .sqry-index found. Run 'sqry index' first to build the index.")?;
130 return Ok(());
131 };
132
133 let config = GraphLoadConfig::default();
135 let graph = load_unified_graph_for_cli(&loc.index_root, &config, cli)
136 .context("Failed to load graph. Run 'sqry index' to build the graph.")?;
137
138 let snapshot = graph.snapshot();
139 let strings = snapshot.strings();
140 let files_registry = snapshot.files();
141 let requested_file_path = if std::path::Path::new(file_path).is_absolute() {
142 std::path::PathBuf::from(file_path)
143 } else {
144 loc.index_root.join(file_path)
145 };
146
147 let node_id = resolve_symbol_by_file_and_name(&snapshot, &requested_file_path, symbol_name)?;
148 let symbol_entry = snapshot
149 .get_node(node_id)
150 .ok_or_else(|| anyhow!("Symbol node not found in graph"))?;
151
152 let file_path_resolved = files_registry
153 .resolve(symbol_entry.file)
154 .map(|p| p.display().to_string())
155 .unwrap_or_default();
156
157 let language = files_registry
158 .language_for_file(symbol_entry.file)
159 .map_or_else(|| "Unknown".to_string(), |l| l.to_string());
160
161 let name = strings
162 .resolve(symbol_entry.name)
163 .map(|s| s.to_string())
164 .unwrap_or_default();
165
166 let qualified_name = symbol_entry
167 .qualified_name
168 .and_then(|id| strings.resolve(id))
169 .map_or_else(|| name.clone(), |s| s.to_string());
170
171 let visibility = symbol_entry
172 .visibility
173 .and_then(|id| strings.resolve(id))
174 .map(|s| s.to_string());
175
176 let documentation = symbol_entry
177 .doc
178 .and_then(|id| strings.resolve(id))
179 .map(|s| s.to_string());
180
181 let context = if include_context {
183 build_symbol_context(&loc.index_root, files_registry, symbol_entry)
184 } else {
185 None
186 };
187
188 let output = ExplainOutput {
189 name,
190 qualified_name,
191 kind: format!("{:?}", symbol_entry.kind),
192 file: file_path_resolved,
193 line: symbol_entry.start_line,
194 language,
195 visibility,
196 documentation,
197 context,
198 };
199
200 if cli.json {
202 let json = serde_json::to_string_pretty(&output).context("Failed to serialize to JSON")?;
203 streams.write_result(&json)?;
204 } else {
205 let text = format_explain_text(&output);
206 streams.write_result(&text)?;
207 }
208
209 Ok(())
210}
211
212fn format_explain_text(output: &ExplainOutput) -> String {
213 let mut lines = Vec::new();
214
215 lines.push(format!("Symbol: {}", output.qualified_name));
216 lines.push(format!(" Kind: {}", output.kind));
217 lines.push(format!(" File: {}:{}", output.file, output.line));
218 lines.push(format!(" Language: {}", output.language));
219
220 if let Some(ref vis) = output.visibility {
221 lines.push(format!(" Visibility: {vis}"));
222 }
223
224 if let Some(ref doc) = output.documentation {
225 lines.push(format!(" Documentation: {doc}"));
226 }
227
228 if let Some(ref ctx) = output.context {
229 lines.push(String::new());
230 lines.push(format!("Code (lines {}-{}):", ctx.start_line, ctx.end_line));
231 for (i, line) in ctx.code.lines().enumerate() {
232 lines.push(format!("{:4} | {}", ctx.start_line as usize + i, line));
233 }
234 }
235
236 lines.join("\n")
237}