1use super::options::PrintOptions;
2use super::utils::{colorize_tag, format_path, make_clickable_link, make_line_link};
3use crate::core::{ScanResult, TodoItem};
4use colored::Colorize;
5use std::collections::HashMap;
6use std::io::{self, Write};
7use std::path::Path;
8use std::path::PathBuf;
9
10pub fn print_tree<W: Write>(
11 writer: &mut W,
12 result: &ScanResult,
13 options: &PrintOptions,
14) -> io::Result<()> {
15 if result.is_empty() {
16 writeln!(writer, "{}", "No TODO items found.".dimmed())?;
17 return Ok(());
18 }
19
20 if options.group_by_tag {
21 print_tree_by_tag(writer, result, options)?;
22 } else {
23 print_tree_by_file(writer, result, options)?;
24 }
25
26 Ok(())
27}
28
29fn print_tree_by_file<W: Write>(
30 writer: &mut W,
31 result: &ScanResult,
32 options: &PrintOptions,
33) -> io::Result<()> {
34 let sorted_files = result.sorted_files();
35 let total_files = sorted_files.len();
36
37 for (idx, (path, items)) in sorted_files.iter().enumerate() {
38 let is_last_file = idx == total_files - 1;
39 print_file_header(writer, path, items.len(), is_last_file, options)?;
40
41 let total_items = items.len();
42 for (item_idx, item) in items.iter().enumerate() {
43 let is_last_item = item_idx == total_items - 1;
44 print_tree_item(writer, item, is_last_file, is_last_item, path, options)?;
45 }
46 }
47
48 Ok(())
49}
50
51fn print_tree_by_tag<W: Write>(
52 writer: &mut W,
53 result: &ScanResult,
54 options: &PrintOptions,
55) -> io::Result<()> {
56 let mut by_tag: HashMap<String, Vec<(PathBuf, TodoItem)>> = HashMap::new();
57
58 for (path, items) in &result.files_map {
59 for item in items {
60 by_tag
61 .entry(item.tag.clone())
62 .or_default()
63 .push((path.clone(), item.clone()));
64 }
65 }
66
67 let mut tags: Vec<_> = by_tag.keys().collect();
68 tags.sort();
69
70 let total_tags = tags.len();
71
72 for (idx, tag) in tags.iter().enumerate() {
73 let is_last_tag = idx == total_tags - 1;
74 let items = by_tag.get(*tag).unwrap();
75
76 let prefix = if is_last_tag {
77 "└──"
78 } else {
79 "├──"
80 };
81 let colored_tag = colorize_tag(tag, options);
82 writeln!(writer, "{} {} ({})", prefix, colored_tag, items.len())?;
83
84 let total_items = items.len();
85 for (item_idx, (path, item)) in items.iter().enumerate() {
86 let is_last_item = item_idx == total_items - 1;
87 let tree_prefix = if is_last_tag { " " } else { "│ " };
88 let item_prefix = if is_last_item {
89 "└──"
90 } else {
91 "├──"
92 };
93
94 let display_path = format_path(path, options);
95 let link = make_clickable_link(path, item.line, options);
96
97 writeln!(
98 writer,
99 "{}{} {}:{} - {}",
100 tree_prefix,
101 item_prefix,
102 link.unwrap_or_else(|| display_path.to_string()),
103 item.line.to_string().cyan(),
104 item.message.dimmed()
105 )?;
106 }
107 }
108
109 Ok(())
110}
111
112fn print_file_header<W: Write>(
113 writer: &mut W,
114 path: &Path,
115 item_count: usize,
116 is_last: bool,
117 options: &PrintOptions,
118) -> io::Result<()> {
119 let prefix = if is_last { "└──" } else { "├──" };
120 let display_path = format_path(path, options);
121 let link = make_clickable_link(path, 1, options);
122
123 let path_str = link.unwrap_or_else(|| {
124 if options.colored {
125 display_path.bold().to_string()
126 } else {
127 display_path.to_string()
128 }
129 });
130
131 let count_str = format!("({})", item_count);
132 let count_display = if options.colored {
133 count_str.dimmed().to_string()
134 } else {
135 count_str
136 };
137
138 writeln!(writer, "{} {} {}", prefix, path_str, count_display)?;
139 Ok(())
140}
141
142fn print_tree_item<W: Write>(
143 writer: &mut W,
144 item: &TodoItem,
145 is_last_file: bool,
146 is_last_item: bool,
147 path: &Path,
148 options: &PrintOptions,
149) -> io::Result<()> {
150 let tree_prefix = if is_last_file { " " } else { "│ " };
151 let item_prefix = if is_last_item {
152 "└──"
153 } else {
154 "├──"
155 };
156
157 let tag = colorize_tag(&item.tag, options);
158 let line_num = if options.colored {
159 format!("L{}", item.line).cyan().to_string()
160 } else {
161 format!("L{}", item.line)
162 };
163
164 let line_display = if options.clickable_links {
165 make_line_link(path, item.line, options).unwrap_or_else(|| line_num.clone())
166 } else {
167 line_num
168 };
169
170 let author_str = item
171 .author
172 .as_ref()
173 .map(|a| format!("({})", a))
174 .unwrap_or_default();
175
176 if author_str.is_empty() {
177 writeln!(
178 writer,
179 "{}{} [{}] {}: {}",
180 tree_prefix, item_prefix, line_display, tag, item.message
181 )?;
182 } else {
183 let author_display = if options.colored {
184 author_str.yellow().to_string()
185 } else {
186 author_str
187 };
188 writeln!(
189 writer,
190 "{}{} [{}] {} {}: {}",
191 tree_prefix, item_prefix, line_display, tag, author_display, item.message
192 )?;
193 }
194
195 Ok(())
196}