Skip to main content

project_map_cli_rust/core/
query_engine.rs

1use crate::core::graph::{ProjectGraph, NodeData, NodeType};
2use crate::error::Result;
3use petgraph::visit::Dfs;
4use std::path::Path;
5
6pub struct QueryEngine {
7    graph: ProjectGraph,
8}
9
10impl QueryEngine {
11    pub fn load(path: &Path) -> Result<Self> {
12        let graph = ProjectGraph::load(path)?;
13        Ok(Self { graph })
14    }
15
16    pub fn find_symbols(&self, query: &str) -> Vec<NodeData> {
17        let query_lower = query.to_lowercase();
18        self.graph.graph.node_weights()
19            .filter(|n| n.node_type == NodeType::Symbol && n.name.to_lowercase().contains(&query_lower))
20            .cloned()
21            .collect()
22    }
23
24    pub fn get_file_outline(&self, path: &str) -> Vec<NodeData> {
25        let file_node = self.graph.graph.node_indices()
26            .find(|i| self.graph.graph[*i].node_type == NodeType::File && self.graph.graph[*i].path == path);
27
28        if let Some(idx) = file_node {
29            self.graph.graph.neighbors_directed(idx, petgraph::Direction::Outgoing)
30                .map(|n| self.graph.graph[n].clone())
31                .collect()
32        } else {
33            Vec::new()
34        }
35    }
36
37    pub fn analyze_impact(&self, name: &str) -> Vec<NodeData> {
38        let node_idx = self.graph.graph.node_indices()
39            .find(|i| self.graph.graph[*i].name == name);
40        
41        if let Some(start_node) = node_idx {
42            // Impact: who do I depend on? (Outgoing edges)
43            let mut dfs = Dfs::new(&self.graph.graph, start_node);
44            let mut results = Vec::new();
45            while let Some(nx) = dfs.next(&self.graph.graph) {
46                if nx != start_node {
47                    results.push(self.graph.graph[nx].clone());
48                }
49            }
50            results
51        } else {
52            Vec::new()
53        }
54    }
55
56    pub fn check_blast_radius(&self, path: &str, symbol: &str) -> Vec<NodeData> {
57        let node_idx = self.graph.graph.node_indices()
58            .find(|i| {
59                let node = &self.graph.graph[*i];
60                node.path == path && node.name == symbol && node.node_type == NodeType::Symbol
61            });
62        
63        let start_node = if let Some(idx) = node_idx {
64            idx
65        } else {
66            // Try matching just by file path if symbol not found
67            let file_node = self.graph.graph.node_indices()
68                .find(|i| {
69                    let node = &self.graph.graph[*i];
70                    node.path == path && node.node_type == NodeType::File
71                });
72            if let Some(idx) = file_node { idx } else { return Vec::new(); }
73        };
74
75        // Blast Radius: who depends on me? (Incoming edges)
76        // We need to use a graph traversal that follows edges backwards.
77        // petgraph's Dfs follows outgoing edges. To follow incoming, we can use a custom traversal or reverse the graph.
78        // Alternatively, we can use neighbors_directed with Incoming in a loop.
79        
80        let mut results = Vec::new();
81        let mut stack = vec![start_node];
82        let mut visited = std::collections::HashSet::new();
83        visited.insert(start_node);
84
85        while let Some(current) = stack.pop() {
86            for neighbor in self.graph.graph.neighbors_directed(current, petgraph::Direction::Incoming) {
87                if visited.insert(neighbor) {
88                    results.push(self.graph.graph[neighbor].clone());
89                    stack.push(neighbor);
90                }
91            }
92        }
93        
94        results
95    }
96
97    pub fn find_symbol_in_path(&self, path: &str, name: &str) -> Option<NodeData> {
98        self.graph.graph.node_weights()
99            .find(|n| n.node_type == NodeType::Symbol && n.path == path && n.name == name)
100            .cloned()
101    }
102}