1use crate::trace::{CallNode, CallTree, TraceDirection};
2use crate::tree::{NodeType, ReferenceTree, TreeNode};
3use crate::SearchResult;
4use colored::*;
5
6pub struct TreeFormatter {
8 max_width: usize,
9}
10
11impl TreeFormatter {
12 pub fn new() -> Self {
14 colored::control::set_override(true);
16 Self { max_width: 80 }
17 }
18
19 pub fn with_width(max_width: usize) -> Self {
21 Self { max_width }
22 }
23
24 pub fn format_result(&self, result: &SearchResult) -> String {
26 let mut output = String::new();
27
28 if !result.translation_entries.is_empty() {
30 output.push_str(&format!("{}\n", "=== Translation Files ===".bold()));
31 for entry in &result.translation_entries {
32 output.push_str(&format!(
33 "{}:{}:{}: {}\n",
34 entry.file.display(),
35 entry.line,
36 entry.key.yellow().bold(),
37 format!("\"{}\"", entry.value).green().bold()
38 ));
39 }
40 output.push('\n');
41 }
42
43 if !result.code_references.is_empty() {
45 output.push_str(&format!("{}\n", "=== Code References ===".bold()));
46 for code_ref in &result.code_references {
47 let highlighted_context = self.highlight_key_in_context(&code_ref.context, &code_ref.key_path);
49 output.push_str(&format!(
50 "{}:{}:{}\n",
51 code_ref.file.display(),
52 code_ref.line,
53 highlighted_context
54 ));
55 }
56 }
57
58 output
59 }
60
61 fn highlight_key_in_context(&self, context: &str, key: &str) -> String {
63 context.replace(key, &key.bold().to_string())
65 }
66
67 pub fn format(&self, tree: &ReferenceTree) -> String {
69 let mut output = String::new();
70 self.format_node(&tree.root, &mut output, "", true, true);
71 output
72 }
73
74 pub fn format_trace_tree(&self, tree: &CallTree, direction: TraceDirection) -> String {
75 match direction {
76 TraceDirection::Forward => self.format_forward_tree(tree),
77 TraceDirection::Backward => self.format_backward_tree(tree),
78 }
79 }
80
81 fn format_forward_tree(&self, tree: &CallTree) -> String {
82 let mut output = String::new();
83 self.format_call_node(&tree.root, &mut output, "", true, true);
84 output
85 }
86
87 fn format_backward_tree(&self, tree: &CallTree) -> String {
88 let mut output = String::new();
89 let mut paths = Vec::new();
100 self.collect_backward_paths(&tree.root, vec![], &mut paths);
101
102 for path in paths {
103 let mut display_path = path.clone();
115 display_path.reverse();
116
117 let mut chain = display_path.iter()
118 .map(|node| format!("{} ({}:{})", node.def.name.bold(), node.def.file.display(), node.def.line))
119 .collect::<Vec<_>>()
120 .join(" -> ");
121
122 if let Some(first) = display_path.first() {
124 if first.truncated {
125 chain = format!("{} -> {}", "[depth limit reached]".red(), chain);
126 }
127 }
128
129 output.push_str(&chain);
130 output.push('\n');
131 }
132
133 if output.is_empty() {
134 output.push_str(&format!("{} (No incoming calls found)\n", tree.root.def.name));
136 }
137
138 output
139 }
140
141 fn collect_backward_paths<'a>(
142 &'a self,
143 node: &'a CallNode,
144 mut current_path: Vec<&'a CallNode>,
145 paths: &mut Vec<Vec<&'a CallNode>>,
146 ) {
147 current_path.push(node);
148
149 if node.children.is_empty() {
150 if node.truncated {
154 }
158 paths.push(current_path);
159 } else {
160 for child in &node.children {
161 self.collect_backward_paths(child, current_path.clone(), paths);
162 }
163 }
164 }
165
166 fn format_call_node(
167 &self,
168 node: &CallNode,
169 output: &mut String,
170 prefix: &str,
171 is_last: bool,
172 is_root: bool,
173 ) {
174 if !is_root {
175 output.push_str(prefix);
176 output.push_str(if is_last { "└─> " } else { "├─> " });
177 }
178
179 let content = format!(
180 "{} ({}:{})",
181 node.def.name.bold(),
182 node.def.file.display(),
183 node.def.line
184 );
185 output.push_str(&content);
186
187 if node.truncated {
188 output.push_str(&" [depth limit reached]".red().to_string());
189 }
190
191 output.push('\n');
192
193 let child_count = node.children.len();
194 for (i, child) in node.children.iter().enumerate() {
195 let is_last_child = i == child_count - 1;
196 let child_prefix = if is_root {
197 String::new()
198 } else {
199 format!("{}{} ", prefix, if is_last { " " } else { "│" })
200 };
201 self.format_call_node(child, output, &child_prefix, is_last_child, false);
202 }
203 }
204
205
206 fn format_node(
208 &self,
209 node: &TreeNode,
210 output: &mut String,
211 prefix: &str,
212 is_last: bool,
213 is_root: bool,
214 ) {
215 if !is_root {
217 output.push_str(prefix);
218 output.push_str(if is_last { "└─> " } else { "├─> " });
219 }
220
221 let content = self.format_content(node);
223 output.push_str(&content);
224
225 if let Some(location) = &node.location {
227 let location_str = format!(
228 " ({}:{})",
229 location.file.display(),
230 location.line
231 );
232 output.push_str(&location_str);
233 }
234
235 output.push('\n');
236
237 let child_count = node.children.len();
239 for (i, child) in node.children.iter().enumerate() {
240 let is_last_child = i == child_count - 1;
241 let child_prefix = if is_root {
242 String::new()
243 } else {
244 format!("{}{} ", prefix, if is_last { " " } else { "│" })
245 };
246
247 self.format_node(child, output, &child_prefix, is_last_child, false);
248 }
249 }
250
251 fn format_content(&self, node: &TreeNode) -> String {
253 match node.node_type {
254 NodeType::Root => {
255 format!("'{}' (search query)", node.content)
256 }
257 NodeType::Translation => {
258 self.truncate(&node.content, self.max_width - 20)
260 }
261 NodeType::KeyPath => {
262 format!("Key: {}", node.content)
263 }
264 NodeType::CodeRef => {
265 let truncated = self.truncate(&node.content.trim(), self.max_width - 30);
267 format!("Code: {}", truncated)
268 }
269 }
270 }
271
272 fn truncate(&self, s: &str, max_len: usize) -> String {
274 if s.len() <= max_len {
275 s.to_string()
276 } else {
277 format!("{}...", &s[..max_len.saturating_sub(3)])
278 }
279 }
280}
281
282impl Default for TreeFormatter {
283 fn default() -> Self {
284 Self::new()
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use crate::tree::{Location, TreeNode};
292 use std::path::PathBuf;
293
294 #[test]
295 fn test_formatter_creation() {
296 let formatter = TreeFormatter::new();
297 assert_eq!(formatter.max_width, 80);
298 }
299
300 #[test]
301 fn test_formatter_with_custom_width() {
302 let formatter = TreeFormatter::with_width(120);
303 assert_eq!(formatter.max_width, 120);
304 }
305
306 #[test]
307 fn test_format_empty_tree() {
308 let tree = ReferenceTree::with_search_text("test".to_string());
309 let formatter = TreeFormatter::new();
310 let output = formatter.format(&tree);
311
312 assert!(output.contains("'test'"));
313 assert!(output.contains("search query"));
314 }
315
316 #[test]
317 fn test_format_tree_with_translation() {
318 let mut root = TreeNode::new(NodeType::Root, "add new".to_string());
319 let translation = TreeNode::with_location(
320 NodeType::Translation,
321 "invoice.labels.add_new: 'add new'".to_string(),
322 Location::new(PathBuf::from("en.yml"), 4),
323 );
324 root.add_child(translation);
325
326 let tree = ReferenceTree::new(root);
327 let formatter = TreeFormatter::new();
328 let output = formatter.format(&tree);
329
330 assert!(output.contains("'add new'"));
331 assert!(output.contains("invoice.labels.add_new"));
332 assert!(output.contains("en.yml:4"));
333 assert!(output.contains("└─>") || output.contains("├─>"));
334 }
335
336 #[test]
337 fn test_format_complete_tree() {
338 let mut root = TreeNode::new(NodeType::Root, "add new".to_string());
339
340 let mut translation = TreeNode::with_location(
341 NodeType::Translation,
342 "invoice.labels.add_new: 'add new'".to_string(),
343 Location::new(PathBuf::from("en.yml"), 4),
344 );
345
346 let mut key_path = TreeNode::new(
347 NodeType::KeyPath,
348 "invoice.labels.add_new".to_string(),
349 );
350
351 let code_ref = TreeNode::with_location(
352 NodeType::CodeRef,
353 "I18n.t('invoice.labels.add_new')".to_string(),
354 Location::new(PathBuf::from("invoices.ts"), 14),
355 );
356
357 key_path.add_child(code_ref);
358 translation.add_child(key_path);
359 root.add_child(translation);
360
361 let tree = ReferenceTree::new(root);
362 let formatter = TreeFormatter::new();
363 let output = formatter.format(&tree);
364
365 assert!(output.contains("'add new'"));
367 assert!(output.contains("invoice.labels.add_new"));
368 assert!(output.contains("Key:"));
369 assert!(output.contains("Code:"));
370 assert!(output.contains("I18n.t"));
371 assert!(output.contains("en.yml:4"));
372 assert!(output.contains("invoices.ts:14"));
373 }
374
375 #[test]
376 fn test_format_multiple_children() {
377 let mut root = TreeNode::new(NodeType::Root, "test".to_string());
378
379 let child1 = TreeNode::with_location(
380 NodeType::Translation,
381 "key1: 'value1'".to_string(),
382 Location::new(PathBuf::from("file1.yml"), 1),
383 );
384
385 let child2 = TreeNode::with_location(
386 NodeType::Translation,
387 "key2: 'value2'".to_string(),
388 Location::new(PathBuf::from("file2.yml"), 2),
389 );
390
391 root.add_child(child1);
392 root.add_child(child2);
393
394 let tree = ReferenceTree::new(root);
395 let formatter = TreeFormatter::new();
396 let output = formatter.format(&tree);
397
398 assert!(output.contains("key1"));
400 assert!(output.contains("key2"));
401 assert!(output.contains("file1.yml:1"));
402 assert!(output.contains("file2.yml:2"));
403
404 assert!(output.contains("├─>"));
406 assert!(output.contains("└─>"));
407 }
408
409 #[test]
410 fn test_truncate_long_content() {
411 let formatter = TreeFormatter::with_width(50);
412 let long_string = "a".repeat(100);
413 let truncated = formatter.truncate(&long_string, 20);
414
415 assert!(truncated.len() <= 20);
416 assert!(truncated.ends_with("..."));
417 }
418
419 #[test]
420 fn test_truncate_short_content() {
421 let formatter = TreeFormatter::new();
422 let short_string = "short";
423 let result = formatter.truncate(short_string, 20);
424
425 assert_eq!(result, "short");
426 }
427
428 #[test]
429 fn test_format_content_root() {
430 let formatter = TreeFormatter::new();
431 let node = TreeNode::new(NodeType::Root, "test query".to_string());
432 let content = formatter.format_content(&node);
433
434 assert!(content.contains("test query"));
435 assert!(content.contains("search query"));
436 }
437
438 #[test]
439 fn test_format_content_key_path() {
440 let formatter = TreeFormatter::new();
441 let node = TreeNode::new(NodeType::KeyPath, "invoice.labels.add_new".to_string());
442 let content = formatter.format_content(&node);
443
444 assert!(content.contains("Key:"));
445 assert!(content.contains("invoice.labels.add_new"));
446 }
447
448 #[test]
449 fn test_format_content_code_ref() {
450 let formatter = TreeFormatter::new();
451 let node = TreeNode::new(
452 NodeType::CodeRef,
453 " I18n.t('invoice.labels.add_new') ".to_string(),
454 );
455 let content = formatter.format_content(&node);
456
457 assert!(content.contains("Code:"));
458 assert!(content.contains("I18n.t"));
459 assert!(!content.starts_with(" "));
461 }
462
463 #[test]
464 fn test_format_deep_nesting() {
465 let mut root = TreeNode::new(NodeType::Root, "test".to_string());
466 let mut level1 = TreeNode::new(NodeType::Translation, "level1".to_string());
467 let mut level2 = TreeNode::new(NodeType::KeyPath, "level2".to_string());
468 let level3 = TreeNode::new(NodeType::CodeRef, "level3".to_string());
469
470 level2.add_child(level3);
471 level1.add_child(level2);
472 root.add_child(level1);
473
474 let tree = ReferenceTree::new(root);
475 let formatter = TreeFormatter::new();
476 let output = formatter.format(&tree);
477
478 let lines: Vec<&str> = output.lines().collect();
480 assert!(lines.len() >= 4);
481
482 assert!(lines[2].starts_with(' ') || lines[2].starts_with('│'));
484 }
485}