lean_ctx/core/graph_expand/
partial.rs1use std::collections::HashMap;
7
8#[derive(Debug, Clone, Default)]
10pub struct PartialGraph {
11 pub nodes: HashMap<String, NodeInfo>,
13 pub edges: Vec<(String, String, EdgeKind)>,
15 pub center: String,
17 pub max_depth: usize,
19}
20
21#[derive(Debug, Clone)]
23pub struct NodeInfo {
24 pub file: String,
26 pub kind: String,
28 pub depth: usize,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum EdgeKind {
35 Calls,
37 CalledBy,
39 Imports,
41 Implements,
43}
44
45impl PartialGraph {
46 pub fn render(&self) -> String {
48 let Some(center) = self.nodes.get(&self.center) else {
49 return String::new();
50 };
51
52 let mut output = format!("Center: {} ({})", self.center, center.file);
53 for depth in 1..=self.max_depth {
54 let mut nodes: Vec<(&str, &NodeInfo)> = self
55 .nodes
56 .iter()
57 .filter(|(_, info)| info.depth == depth)
58 .map(|(name, info)| (name.as_str(), info))
59 .collect();
60 nodes.sort_unstable_by_key(|(name, _)| *name);
61
62 if nodes.is_empty() {
63 continue;
64 }
65
66 output.push_str(&format!("\nDepth {depth}: "));
67 let rendered = nodes
68 .into_iter()
69 .map(|(name, info)| {
70 let direction = self.direction_for(name, depth);
71 format!("{direction}{name} ({})", info.file)
72 })
73 .collect::<Vec<_>>()
74 .join(", ");
75 output.push_str(&rendered);
76 }
77 output
78 }
79
80 pub fn node_count(&self) -> usize {
82 self.nodes.len()
83 }
84
85 pub fn at_depth(&self, depth: usize) -> Vec<&str> {
87 let mut symbols: Vec<&str> = self
88 .nodes
89 .iter()
90 .filter(|(_, info)| info.depth == depth)
91 .map(|(name, _)| name.as_str())
92 .collect();
93 symbols.sort_unstable();
94 symbols
95 }
96
97 fn direction_for(&self, name: &str, depth: usize) -> String {
98 let relation = self.edges.iter().find_map(|(from, to, relation)| {
99 if to == name {
100 self.nodes
101 .get(from)
102 .filter(|info| info.depth + 1 == depth)
103 .map(|_| *relation)
104 } else {
105 None
106 }
107 });
108 let arrow = match relation {
109 Some(EdgeKind::CalledBy) => "← ",
110 Some(EdgeKind::Calls | EdgeKind::Imports | EdgeKind::Implements) | None => "→ ",
111 };
112 arrow.repeat(depth)
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::{NodeInfo, PartialGraph};
119
120 #[test]
121 fn empty_graph_renders_empty() {
122 assert_eq!(PartialGraph::default().render(), "");
123 }
124
125 #[test]
126 fn single_node_graph() {
127 let mut graph = PartialGraph {
128 center: "root".to_string(),
129 max_depth: 2,
130 ..PartialGraph::default()
131 };
132 graph.nodes.insert(
133 "root".to_string(),
134 NodeInfo {
135 file: "src/root.rs".to_string(),
136 kind: "function".to_string(),
137 depth: 0,
138 },
139 );
140
141 assert_eq!(graph.node_count(), 1);
142 assert_eq!(graph.render(), "Center: root (src/root.rs)");
143 }
144
145 #[test]
146 fn at_depth_filters_correctly() {
147 let mut graph = PartialGraph::default();
148 for (name, depth) in [("root", 0), ("beta", 1), ("alpha", 1), ("leaf", 2)] {
149 graph.nodes.insert(
150 name.to_string(),
151 NodeInfo {
152 file: format!("{name}.rs"),
153 kind: "function".to_string(),
154 depth,
155 },
156 );
157 }
158
159 assert_eq!(graph.at_depth(1), vec!["alpha", "beta"]);
160 assert_eq!(graph.at_depth(2), vec!["leaf"]);
161 assert!(graph.at_depth(3).is_empty());
162 }
163}