1use crate::block::{BlockKind, BlockRelation};
2use crate::graph_builder::{GraphNode, ProjectGraph};
3
4#[derive(Debug, Clone)]
13pub struct GraphBlockInfo {
14 pub name: String,
15 pub qualified_name: Option<String>,
16 pub kind: String,
17 pub file_path: Option<String>,
18 pub source_code: Option<String>,
19 pub node: GraphNode,
20 pub unit_index: usize,
21 pub start_line: usize,
22 pub end_line: usize,
23}
24
25impl GraphBlockInfo {
26 fn resolved_location(&self) -> String {
27 use std::env;
28 use std::path::Path;
29
30 if let Some(path) = &self.file_path {
31 let candidate = Path::new(path);
32 if candidate.is_absolute() {
33 candidate.display().to_string()
34 } else if let Ok(cwd) = env::current_dir() {
35 cwd.join(candidate).display().to_string()
36 } else {
37 path.clone()
38 }
39 } else {
40 format!("<file_unit_{}>", self.unit_index)
41 }
42 }
43
44 pub fn format_for_llm(&self) -> String {
45 let mut output = String::new();
46
47 let location = self.resolved_location();
49
50 output.push_str(&format!(
51 "┌─ {} [{}] at {}\n",
52 self.name, self.kind, location
53 ));
54
55 if let Some(fqn) = &self.qualified_name {
56 if fqn != &self.name {
57 output.push_str(&format!("│ aka {}\n", fqn));
58 }
59 }
60
61 if let Some(source) = &self.source_code {
63 let lines: Vec<&str> = source.lines().collect();
64 let max_line_num = self.end_line;
65 let line_num_width = max_line_num.to_string().len();
66
67 for (idx, line) in lines.iter().enumerate() {
68 let line_num = self.start_line + idx;
69 output.push_str(&format!(
70 "│ [{:width$}] {}\n",
71 line_num,
72 line,
73 width = line_num_width
74 ));
75 }
76 }
77
78 output.push_str("└─\n");
79 output
80 }
81
82 pub fn format_summary(&self) -> String {
83 let display_name = self
84 .qualified_name
85 .as_ref()
86 .filter(|name| !name.is_empty())
87 .cloned()
88 .unwrap_or_else(|| self.name.clone());
89
90 let location = self.resolved_location();
91
92 format!(
93 "{} @ {}:{}-{}",
94 display_name, location, self.start_line, self.end_line
95 )
96 }
97}
98
99#[derive(Debug, Default)]
101pub struct QueryResult {
102 pub primary: Vec<GraphBlockInfo>,
103 pub depends: Vec<GraphBlockInfo>,
104 pub depended: Vec<GraphBlockInfo>,
105}
106
107impl QueryResult {
108 pub fn format_for_llm(&self) -> String {
109 let mut output = String::new();
110
111 if !self.primary.is_empty() {
112 output.push_str(" ------------- ASK SYMBOL ------------------- \n");
113 for block in &self.primary {
114 output.push_str(&block.format_for_llm());
115 output.push('\n');
116 }
117 }
118
119 if !self.depends.is_empty() {
120 output.push_str(" -------------- DEPENDS ON (Dependencies) ----------------- \n");
121 for block in &self.depends {
122 output.push_str(&block.format_for_llm());
123 output.push('\n');
124 }
125 }
126
127 if !self.depended.is_empty() {
128 output.push_str(" -------------- DEPENDED BY (Dependents) ----------------- \n");
129 for block in &self.depended {
130 output.push_str(&block.format_for_llm());
131 output.push('\n');
132 }
133 }
134
135 output
136 }
137
138 pub fn format_summary(&self) -> String {
139 fn push_section(output: &mut String, title: &str, blocks: &[GraphBlockInfo]) {
140 if blocks.is_empty() {
141 return;
142 }
143 if !output.is_empty() {
144 output.push('\n');
145 }
146 output.push_str(title);
147 output.push('\n');
148 for block in blocks {
149 output.push_str(" - ");
150 output.push_str(&block.format_summary());
151 output.push('\n');
152 }
153 }
154
155 let mut output = String::new();
156 push_section(&mut output, "SYMBOL:", &self.primary);
157 push_section(&mut output, "DEPENDS:", &self.depends);
158 push_section(&mut output, "DEPENDENTS:", &self.depended);
159 while output.ends_with('\n') {
160 output.pop();
161 }
162 output
163 }
164}
165
166pub struct ProjectQuery<'tcx> {
168 graph: &'tcx ProjectGraph<'tcx>,
169}
170
171impl<'tcx> ProjectQuery<'tcx> {
172 pub fn new(graph: &'tcx ProjectGraph<'tcx>) -> Self {
173 Self { graph }
174 }
175
176 pub fn find_by_name(&self, name: &str) -> QueryResult {
178 let mut result = QueryResult::default();
179
180 let blocks = self.graph.blocks_by_name(name);
181 for node in blocks {
182 if let Some(block_info) = self.node_to_block_info(node) {
183 result.primary.push(block_info);
184 }
185 }
186
187 result
188 }
189
190 pub fn find_all_functions(&self) -> QueryResult {
192 self.find_by_kind(BlockKind::Func)
193 }
194
195 pub fn find_all_structs(&self) -> QueryResult {
197 self.find_by_kind(BlockKind::Class)
198 }
199
200 pub fn find_by_kind(&self, kind: BlockKind) -> QueryResult {
202 let mut result = QueryResult::default();
203
204 let blocks = self.graph.blocks_by_kind(kind);
205 for node in blocks {
206 if let Some(block_info) = self.node_to_block_info(node) {
207 result.primary.push(block_info.clone());
208 }
209 }
210
211 result
212 }
213
214 pub fn file_structure(&self, unit_index: usize) -> QueryResult {
216 let mut result = QueryResult::default();
217
218 let blocks = self.graph.blocks_in(unit_index);
219 for node in blocks {
220 if let Some(block_info) = self.node_to_block_info(node) {
221 result.primary.push(block_info.clone());
222 }
223 }
224
225 result
226 }
227
228 pub fn find_depends(&self, name: &str) -> QueryResult {
230 let mut result = QueryResult::default();
231
232 if let Some(primary_node) = self.graph.block_by_name(name) {
234 if let Some(block_info) = self.node_to_block_info(primary_node) {
235 result.primary.push(block_info);
236
237 let depends_blocks = self
239 .graph
240 .find_related_blocks(primary_node, vec![BlockRelation::DependsOn]);
241 for depends_node in depends_blocks {
242 if let Some(depends_info) = self.node_to_block_info(depends_node) {
243 result.depends.push(depends_info);
244 }
245 }
246 }
247 }
248
249 result
250 }
251
252 pub fn find_depended(&self, name: &str) -> QueryResult {
254 let mut result = QueryResult::default();
255
256 if let Some(primary_node) = self.graph.block_by_name(name) {
258 if let Some(block_info) = self.node_to_block_info(primary_node) {
259 result.primary.push(block_info);
260
261 let depended_blocks = self
263 .graph
264 .find_related_blocks(primary_node, vec![BlockRelation::DependedBy]);
265 for depended_node in depended_blocks {
266 if let Some(depended_info) = self.node_to_block_info(depended_node) {
267 result.depended.push(depended_info);
268 }
269 }
270 }
271 }
272
273 result
274 }
275
276 pub fn find_depends_recursive(&self, name: &str) -> QueryResult {
278 let mut result = QueryResult::default();
279
280 if let Some(primary_node) = self.graph.block_by_name(name) {
282 if let Some(block_info) = self.node_to_block_info(primary_node) {
283 result.primary.push(block_info);
284
285 let all_related = self.graph.find_dpends_blocks_recursive(primary_node);
287 for related_node in all_related {
288 if let Some(related_info) = self.node_to_block_info(related_node) {
289 result.depends.push(related_info);
290 }
291 }
292 }
293 }
294
295 result
296 }
297
298 pub fn find_depended_recursive(&self, name: &str) -> QueryResult {
300 let mut result = QueryResult::default();
301
302 if let Some(primary_node) = self.graph.block_by_name(name) {
303 if let Some(block_info) = self.node_to_block_info(primary_node) {
304 result.primary.push(block_info);
305
306 let all_related = self.graph.find_depended_blocks_recursive(primary_node);
307 for related_node in all_related {
308 if let Some(related_info) = self.node_to_block_info(related_node) {
309 result.depended.push(related_info);
310 }
311 }
312 }
313 }
314
315 result
316 }
317
318 pub fn traverse_bfs(&self, start_name: &str) -> Vec<GraphBlockInfo> {
320 let mut results = Vec::new();
321
322 if let Some(start_node) = self.graph.block_by_name(start_name) {
323 self.graph.traverse_bfs(start_node, |node| {
324 if let Some(block_info) = self.node_to_block_info(node) {
325 results.push(block_info);
326 }
327 });
328 }
329
330 results
331 }
332
333 pub fn traverse_dfs(&self, start_name: &str) -> Vec<GraphBlockInfo> {
335 let mut results = Vec::new();
336
337 if let Some(start_node) = self.graph.block_by_name(start_name) {
338 self.graph.traverse_dfs(start_node, |node| {
339 if let Some(block_info) = self.node_to_block_info(node) {
340 results.push(block_info);
341 }
342 });
343 }
344
345 results
346 }
347
348 pub fn find_by_kind_in_unit(&self, kind: BlockKind, unit_index: usize) -> QueryResult {
350 let mut result = QueryResult::default();
351
352 let blocks = self.graph.blocks_by_kind_in(kind, unit_index);
353 for node in blocks {
354 if let Some(block_info) = self.node_to_block_info(node) {
355 result.primary.push(block_info.clone());
356 }
357 }
358
359 result
360 }
361
362 fn node_to_block_info(&self, node: GraphNode) -> Option<GraphBlockInfo> {
364 let (unit_index, name, kind) = self.graph.block_info(node.block_id)?;
365
366 let (display_name, qualified_name) =
368 if let Some(symbol) = self.graph.cc.find_symbol_by_block_id(node.block_id) {
369 let fallback = name
370 .clone()
371 .unwrap_or_else(|| format!("_unnamed_{}", node.block_id.0));
372 let base_name = if symbol.name.is_empty() {
373 fallback
374 } else {
375 symbol.name.clone()
376 };
377
378 let fqn = symbol.fqn_name.borrow().clone();
379 let qualified = if !fqn.is_empty() && fqn != base_name {
380 Some(fqn)
381 } else {
382 None
383 };
384
385 (base_name, qualified)
386 } else {
387 (
388 name.unwrap_or_else(|| format!("_unnamed_{}", node.block_id.0)),
389 None,
390 )
391 };
392
393 let file_path = self
395 .graph
396 .cc
397 .files
398 .get(unit_index)
399 .and_then(|file| file.path().map(|s| s.to_string()));
400
401 let source_code = self.get_block_source_code(node, unit_index);
403
404 let (start_line, end_line) = self.get_line_numbers(node, unit_index);
406
407 Some(GraphBlockInfo {
408 name: display_name,
409 qualified_name,
410 kind: format!("{:?}", kind),
411 file_path,
412 source_code,
413 node,
414 unit_index,
415 start_line,
416 end_line,
417 })
418 }
419
420 fn get_line_numbers(&self, node: GraphNode, unit_index: usize) -> (usize, usize) {
422 let file = match self.graph.cc.files.get(unit_index) {
423 Some(f) => f,
424 None => return (0, 0),
425 };
426
427 let unit = self.graph.cc.compile_unit(unit_index);
428
429 let bb = match unit.opt_bb(node.block_id) {
431 Some(b) => b,
432 None => return (0, 0),
433 };
434
435 let base = match bb.base() {
437 Some(b) => b,
438 None => return (0, 0),
439 };
440
441 let hir_node = base.node;
442 let start_byte = hir_node.start_byte();
443 let end_byte = hir_node.end_byte();
444
445 if let Some(content) = file.file.content.as_ref() {
447 let start_line = content[..start_byte.min(content.len())]
448 .iter()
449 .filter(|&&b| b == b'\n')
450 .count()
451 + 1;
452 let end_line = content[..end_byte.min(content.len())]
453 .iter()
454 .filter(|&&b| b == b'\n')
455 .count()
456 + 1;
457 (start_line, end_line)
458 } else {
459 (0, 0)
460 }
461 }
462
463 fn get_block_source_code(&self, node: GraphNode, unit_index: usize) -> Option<String> {
465 let file = self.graph.cc.files.get(unit_index)?;
466 let unit = self.graph.cc.compile_unit(unit_index);
467
468 let bb = unit.opt_bb(node.block_id)?;
470
471 let base = bb.base()?;
473 let hir_node = base.node;
474
475 let start_byte = hir_node.start_byte();
477 let end_byte = hir_node.end_byte();
478
479 if let crate::block::BasicBlock::Class(class_block) = bb {
481 return self.extract_class_definition(class_block, unit, file, start_byte, end_byte);
482 }
483
484 file.opt_get_text(start_byte, end_byte)
485 }
486
487 fn extract_class_definition(
490 &self,
491 class_block: &crate::block::BlockClass,
492 unit: crate::context::CompileUnit,
493 file: &crate::file::File,
494 class_start_byte: usize,
495 class_end_byte: usize,
496 ) -> Option<String> {
497 let full_text = file.opt_get_text(class_start_byte, class_end_byte)?;
499
500 let mut method_start_bytes = Vec::new();
502
503 for child_id in &class_block.base.children {
504 let child_bb = unit.opt_bb(*child_id)?;
505 let child_kind = child_bb.kind();
506
507 if child_kind == BlockKind::Func {
509 if let Some(child_base) = child_bb.base() {
510 let child_node = child_base.node;
511 let child_start = child_node.start_byte();
512 if child_start > class_start_byte {
513 method_start_bytes.push(child_start);
514 }
515 }
516 }
517 }
518
519 if method_start_bytes.is_empty() {
521 return Some(full_text);
522 }
523
524 method_start_bytes.sort();
526 let first_method_start = method_start_bytes[0];
527
528 let offset = first_method_start - class_start_byte;
530 if offset >= full_text.len() {
531 return Some(full_text);
532 }
533
534 let class_def = full_text[..offset].to_string();
536
537 let trimmed = class_def.trim_end();
539
540 if let Some(last_newline) = trimmed.rfind('\n') {
542 Some(trimmed[..=last_newline].to_string())
543 } else {
544 Some(trimmed.to_string())
545 }
546 }
547}