Skip to main content

project_map_cli_rust/core/
toon.rs

1use crate::core::graph::NodeData;
2use std::collections::{HashMap, HashSet};
3
4pub struct ToonFormatter;
5
6impl ToonFormatter {
7    pub fn format_symbols(query: &str, matches: &[NodeData]) -> String {
8        let mut output = format!("Resource: Symbols | Query: {}\n", query);
9        output.push_str(&format!("Matches Found: {}\n", matches.len()));
10        
11        for m in matches.iter().take(10) {
12            output.push_str(&format!("- {} ({}) [line {}]\n", m.path, m.name, m.line));
13        }
14        
15        if matches.len() > 10 {
16            output.push_str(&format!("... and {} more.\n", matches.len() - 10));
17        }
18
19        if let Some(first) = matches.first() {
20            output.push_str(&format!("\nNext Step: `project-map impact --fqn {}`", first.name));
21        }
22
23        output
24    }
25
26    pub fn format_file_context(path: &str, symbols: &[NodeData]) -> String {
27        let mut output = format!("Resource: FileContext | Path: {}\n", path);
28        output.push_str("\n--- File Outline ---\n");
29        
30        if symbols.is_empty() {
31            output.push_str("- (No symbols detected or file not indexed)\n");
32        } else {
33            for s in symbols {
34                output.push_str(&format!("- {} {} (line: {})\n", s.kind, s.name, s.line));
35            }
36        }
37
38        output.push_str(&format!("\nNext Step: `project-map fetch --path {} --symbol <symbol_name>`", path));
39        output
40    }
41
42    pub fn format_impact_analysis(fqn: &str, impact: &[NodeData]) -> String {
43        let mut output = format!("Resource: Impact Analysis | Target: {}\n", fqn);
44        output.push_str(&format!("Nodes Impacted: {}\n", impact.len()));
45        
46        for node in impact.iter().take(10) {
47            output.push_str(&format!("- {:?}: {} ({})\n", node.node_type, node.name, node.path));
48        }
49
50        if impact.len() > 10 {
51            output.push_str(&format!("... and {} more.\n", impact.len() - 10));
52        }
53
54        if let Some(first) = impact.first() {
55            output.push_str(&format!("\nNext Step: `project-map blast --path {} --symbol {}` to see what this impacts.", first.path, first.name));
56        }
57
58        output
59    }
60
61    pub fn format_blast_radius(path: &str, symbol: &str, results: &[NodeData]) -> String {
62        let mut output = format!("Resource: Blast Radius | Symbol: {} in {}\n", symbol, path);
63        
64        if results.is_empty() {
65            output.push_str("No dependent components found.\n");
66        } else {
67            let mut dir_counts: HashMap<String, usize> = HashMap::new();
68            let mut unique_files: HashSet<String> = HashSet::new();
69
70            for r in results {
71                unique_files.insert(r.path.clone());
72                let dir = std::path::Path::new(&r.path)
73                    .parent()
74                    .and_then(|p| p.to_str())
75                    .unwrap_or("root")
76                    .to_string();
77                *dir_counts.entry(dir).or_insert(0) += 1;
78            }
79
80            output.push_str("Summary:\n");
81            output.push_str(&format!("- Total Impacted Nodes: {}\n", results.len()));
82            output.push_str(&format!("- Unique Files Affected: {}\n", unique_files.len()));
83            output.push_str("- Affected Modules/Packages:\n");
84
85            let mut sorted_dirs: Vec<_> = dir_counts.into_iter().collect();
86            sorted_dirs.sort_by(|a, b| b.1.cmp(&a.1));
87
88            for (dir, count) in sorted_dirs.iter().take(5) {
89                output.push_str(&format!("  * {}: {} nodes\n", dir, count));
90            }
91
92            if sorted_dirs.len() > 5 {
93                output.push_str(&format!("  * ... and {} more directories.\n", sorted_dirs.len() - 5));
94            }
95
96            output.push_str("\nTop Direct Dependents:\n");
97            for r in results.iter().take(10) {
98                output.push_str(&format!("- {} (ln: {}) -> {}\n", r.path, r.line, r.name));
99            }
100
101            if results.len() > 10 {
102                output.push_str(&format!("... and {} more nodes omitted for brevity.\n", results.len() - 10));
103            }
104
105            if let Some(first) = results.first() {
106                output.push_str(&format!("\nNext Step: `project-map fetch --path {} --symbol {}` to view source.", first.path, first.name));
107            }
108        }
109
110        output
111    }
112
113    pub fn format_status(is_ready: bool, index_path: Option<&str>, pulse_tree: Option<&str>, active_features: &[String]) -> String {
114        let mut output = "Project Map CLI - Status\n".to_string();
115        if is_ready {
116            output.push_str("Phase: Ready\n");
117            if let Some(path) = index_path {
118                output.push_str(&format!("Index: Found ({})\n", path));
119            }
120
121            if let Some(tree) = pulse_tree {
122                output.push_str("\n--- Project Pulse ---\n");
123                output.push_str(tree);
124            }
125
126            if !active_features.is_empty() {
127                output.push_str("\n--- Active Features ---\n");
128                for feature in active_features {
129                    output.push_str(&format!("- projects/active/{}\n", feature));
130                }
131            }
132        } else {
133            output.push_str("Phase: Discovery (No index found)\n");
134            output.push_str("Next Step: Run `project-map build` to generate the index.\n");
135        }
136        output
137    }
138
139    pub fn format_file_matches(query: &str, matches: &[NodeData]) -> String {
140        let mut output = format!("Resource: Files | Query: {}\n", query);
141        output.push_str(&format!("Matches Found: {}\n", matches.len()));
142        
143        for m in matches.iter().take(10) {
144            output.push_str(&format!("- {}\n", m.path));
145        }
146        
147        if matches.len() > 10 {
148            output.push_str(&format!("... and {} more.\n", matches.len() - 10));
149        }
150
151        if let Some(first) = matches.first() {
152            output.push_str(&format!("\nNext Step: `project-map context --path {}` to see the file outline.", first.path));
153        }
154
155        output
156    }
157
158    pub fn format_fetch_result(path: &str, symbol: &str, content: Option<&str>) -> String {
159        if let Some(c) = content {
160            format!("Resource: Fetch | Path: {} | Symbol: {}\n---\n{}\n---", path, symbol, c)
161        } else {
162            format!("Resource: Fetch | Status: Symbol not found: {} in {}", symbol, path)
163        }
164    }
165}