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