Skip to main content

todo_tree/printer/
tree.rs

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}