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 kind: String,
16 pub file_path: Option<String>,
17 pub source_code: Option<String>,
18 pub node: GraphNode,
19 pub unit_index: usize,
20 pub start_line: usize,
21 pub end_line: usize,
22}
23
24impl GraphBlockInfo {
25 pub fn format_for_llm(&self) -> String {
26 let mut output = String::new();
27
28 let location = if let Some(path) = &self.file_path {
30 use std::path::Path;
31 let abs_path = Path::new(path);
32 if abs_path.is_absolute() {
33 path.clone()
34 } else {
35 match std::env::current_dir() {
36 Ok(cwd) => cwd.join(path).display().to_string(),
37 Err(_) => path.clone(),
38 }
39 }
40 } else {
41 format!("<file_unit_{}>", self.unit_index)
42 };
43
44 output.push_str(&format!(
45 "┌─ {} [{}] at {}\n",
46 self.name, self.kind, location
47 ));
48
49 if let Some(source) = &self.source_code {
51 let lines: Vec<&str> = source.lines().collect();
52 let max_line_num = self.end_line;
53 let line_num_width = max_line_num.to_string().len();
54
55 for (idx, line) in lines.iter().enumerate() {
56 let line_num = self.start_line + idx;
57 output.push_str(&format!(
58 "│ [{:width$}] {}\n",
59 line_num,
60 line,
61 width = line_num_width
62 ));
63 }
64 }
65
66 output.push_str("└─\n");
67 output
68 }
69}
70
71#[derive(Debug, Default)]
73pub struct QueryResult {
74 pub primary: Vec<GraphBlockInfo>,
75 pub depends: Vec<GraphBlockInfo>,
76 pub depended: Vec<GraphBlockInfo>,
77}
78
79impl QueryResult {
80 pub fn format_for_llm(&self) -> String {
81 let mut output = String::new();
82
83 if !self.primary.is_empty() {
84 output.push_str(" ------------- PRIMARY RESULTS ------------------- \n");
85 for block in &self.primary {
86 output.push_str(&block.format_for_llm());
87 output.push('\n');
88 }
89 }
90
91 if !self.depends.is_empty() {
92 output.push_str(" -------------- DEPENDS ON (Dependencies) ----------------- \n");
93 for block in &self.depends {
94 output.push_str(&block.format_for_llm());
95 output.push('\n');
96 }
97 }
98
99 if !self.depended.is_empty() {
100 output.push_str(" -------------- DEPENDED BY (Dependents) ----------------- \n");
101 for block in &self.depended {
102 output.push_str(&block.format_for_llm());
103 output.push('\n');
104 }
105 }
106
107 output
108 }
109}
110
111pub struct ProjectQuery<'tcx> {
113 graph: &'tcx ProjectGraph<'tcx>,
114}
115
116impl<'tcx> ProjectQuery<'tcx> {
117 pub fn new(graph: &'tcx ProjectGraph<'tcx>) -> Self {
118 Self { graph }
119 }
120
121 pub fn find_by_name(&self, name: &str) -> QueryResult {
123 let mut result = QueryResult::default();
124
125 let blocks = self.graph.blocks_by_name(name);
126 for node in blocks {
127 if let Some(block_info) = self.node_to_block_info(node) {
128 result.primary.push(block_info);
129 }
130 }
131
132 result
133 }
134
135 pub fn find_all_functions(&self) -> QueryResult {
137 self.find_by_kind(BlockKind::Func)
138 }
139
140 pub fn find_all_structs(&self) -> QueryResult {
142 self.find_by_kind(BlockKind::Class)
143 }
144
145 pub fn find_by_kind(&self, kind: BlockKind) -> QueryResult {
147 let mut result = QueryResult::default();
148
149 let blocks = self.graph.blocks_by_kind(kind);
150 for node in blocks {
151 if let Some(block_info) = self.node_to_block_info(node) {
152 result.primary.push(block_info.clone());
153 }
154 }
155
156 result
157 }
158
159 pub fn file_structure(&self, unit_index: usize) -> QueryResult {
161 let mut result = QueryResult::default();
162
163 let blocks = self.graph.blocks_in(unit_index);
164 for node in blocks {
165 if let Some(block_info) = self.node_to_block_info(node) {
166 result.primary.push(block_info.clone());
167 }
168 }
169
170 result
171 }
172
173 pub fn find_depends(&self, name: &str) -> QueryResult {
175 let mut result = QueryResult::default();
176
177 if let Some(primary_node) = self.graph.block_by_name(name) {
179 if let Some(block_info) = self.node_to_block_info(primary_node) {
180 result.primary.push(block_info);
181
182 let depends_blocks = self
184 .graph
185 .find_related_blocks(primary_node, vec![BlockRelation::DependsOn]);
186 for depends_node in depends_blocks {
187 if let Some(depends_info) = self.node_to_block_info(depends_node) {
188 result.depends.push(depends_info);
189 }
190 }
191 }
192 }
193
194 result
195 }
196
197 pub fn find_depended(&self, name: &str) -> QueryResult {
199 let mut result = QueryResult::default();
200
201 if let Some(primary_node) = self.graph.block_by_name(name) {
203 if let Some(block_info) = self.node_to_block_info(primary_node) {
204 result.primary.push(block_info);
205
206 let depended_blocks = self
208 .graph
209 .find_related_blocks(primary_node, vec![BlockRelation::DependedBy]);
210 for depended_node in depended_blocks {
211 if let Some(depended_info) = self.node_to_block_info(depended_node) {
212 result.depended.push(depended_info);
213 }
214 }
215 }
216 }
217
218 result
219 }
220
221 pub fn find_depends_recursive(&self, name: &str) -> QueryResult {
223 let mut result = QueryResult::default();
224
225 if let Some(primary_node) = self.graph.block_by_name(name) {
227 if let Some(block_info) = self.node_to_block_info(primary_node) {
228 result.primary.push(block_info);
229
230 let all_related = self.graph.find_dpends_blocks_recursive(primary_node);
232 for related_node in all_related {
233 if let Some(related_info) = self.node_to_block_info(related_node) {
234 result.depends.push(related_info);
235 }
236 }
237 }
238 }
239
240 result
241 }
242
243 pub fn traverse_bfs(&self, start_name: &str) -> Vec<GraphBlockInfo> {
245 let mut results = Vec::new();
246
247 if let Some(start_node) = self.graph.block_by_name(start_name) {
248 self.graph.traverse_bfs(start_node, |node| {
249 if let Some(block_info) = self.node_to_block_info(node) {
250 results.push(block_info);
251 }
252 });
253 }
254
255 results
256 }
257
258 pub fn traverse_dfs(&self, start_name: &str) -> Vec<GraphBlockInfo> {
260 let mut results = Vec::new();
261
262 if let Some(start_node) = self.graph.block_by_name(start_name) {
263 self.graph.traverse_dfs(start_node, |node| {
264 if let Some(block_info) = self.node_to_block_info(node) {
265 results.push(block_info);
266 }
267 });
268 }
269
270 results
271 }
272
273 pub fn find_by_kind_in_unit(&self, kind: BlockKind, unit_index: usize) -> QueryResult {
275 let mut result = QueryResult::default();
276
277 let blocks = self.graph.blocks_by_kind_in(kind, unit_index);
278 for node in blocks {
279 if let Some(block_info) = self.node_to_block_info(node) {
280 result.primary.push(block_info.clone());
281 }
282 }
283
284 result
285 }
286
287 fn node_to_block_info(&self, node: GraphNode) -> Option<GraphBlockInfo> {
289 let (unit_index, name, kind) = self.graph.block_info(node.block_id)?;
290
291 let display_name =
293 if let Some(symbol) = self.graph.cc.find_symbol_by_block_id(node.block_id) {
294 let fqn = symbol.fqn_name.borrow();
295 if !fqn.is_empty() && *fqn != symbol.name {
296 fqn.clone()
297 } else {
298 name.unwrap_or_else(|| format!("_unnamed_{}", node.block_id.0))
299 }
300 } else {
301 name.unwrap_or_else(|| format!("_unnamed_{}", node.block_id.0))
302 };
303
304 let file_path = self
306 .graph
307 .cc
308 .files
309 .get(unit_index)
310 .and_then(|file| file.path().map(|s| s.to_string()));
311
312 let source_code = self.get_block_source_code(node, unit_index);
314
315 let (start_line, end_line) = self.get_line_numbers(node, unit_index);
317
318 Some(GraphBlockInfo {
319 name: display_name,
320 kind: format!("{:?}", kind),
321 file_path,
322 source_code,
323 node,
324 unit_index,
325 start_line,
326 end_line,
327 })
328 }
329
330 fn get_line_numbers(&self, node: GraphNode, unit_index: usize) -> (usize, usize) {
332 let file = match self.graph.cc.files.get(unit_index) {
333 Some(f) => f,
334 None => return (0, 0),
335 };
336
337 let unit = self.graph.cc.compile_unit(unit_index);
338
339 let bb = match unit.opt_bb(node.block_id) {
341 Some(b) => b,
342 None => return (0, 0),
343 };
344
345 let base = match bb.base() {
347 Some(b) => b,
348 None => return (0, 0),
349 };
350
351 let hir_node = base.node;
352 let start_byte = hir_node.start_byte();
353 let end_byte = hir_node.end_byte();
354
355 if let Some(content) = file.file.content.as_ref() {
357 let start_line = content[..start_byte.min(content.len())]
358 .iter()
359 .filter(|&&b| b == b'\n')
360 .count()
361 + 1;
362 let end_line = content[..end_byte.min(content.len())]
363 .iter()
364 .filter(|&&b| b == b'\n')
365 .count()
366 + 1;
367 (start_line, end_line)
368 } else {
369 (0, 0)
370 }
371 }
372
373 fn get_block_source_code(&self, node: GraphNode, unit_index: usize) -> Option<String> {
375 let file = self.graph.cc.files.get(unit_index)?;
376 let unit = self.graph.cc.compile_unit(unit_index);
377
378 let bb = unit.opt_bb(node.block_id)?;
380
381 let base = bb.base()?;
383 let hir_node = base.node;
384
385 let start_byte = hir_node.start_byte();
387 let end_byte = hir_node.end_byte();
388
389 if let crate::block::BasicBlock::Class(class_block) = bb {
391 return self.extract_class_definition(class_block, unit, file, start_byte, end_byte);
392 }
393
394 file.opt_get_text(start_byte, end_byte)
395 }
396
397 fn extract_class_definition(
400 &self,
401 class_block: &crate::block::BlockClass,
402 unit: crate::context::CompileUnit,
403 file: &crate::file::File,
404 class_start_byte: usize,
405 class_end_byte: usize,
406 ) -> Option<String> {
407 let full_text = file.opt_get_text(class_start_byte, class_end_byte)?;
409
410 let mut method_start_bytes = Vec::new();
412
413 for child_id in &class_block.base.children {
414 let child_bb = unit.opt_bb(*child_id)?;
415 let child_kind = child_bb.kind();
416
417 if child_kind == BlockKind::Func {
419 if let Some(child_base) = child_bb.base() {
420 let child_node = child_base.node;
421 let child_start = child_node.start_byte();
422 if child_start > class_start_byte {
423 method_start_bytes.push(child_start);
424 }
425 }
426 }
427 }
428
429 if method_start_bytes.is_empty() {
431 return Some(full_text);
432 }
433
434 method_start_bytes.sort();
436 let first_method_start = method_start_bytes[0];
437
438 let offset = first_method_start - class_start_byte;
440 if offset >= full_text.len() {
441 return Some(full_text);
442 }
443
444 let class_def = full_text[..offset].to_string();
446
447 let trimmed = class_def.trim_end();
449
450 if let Some(last_newline) = trimmed.rfind('\n') {
452 Some(trimmed[..=last_newline].to_string())
453 } else {
454 Some(trimmed.to_string())
455 }
456 }
457}