Skip to main content

todo_tree/printer/
flat.rs

1use super::options::PrintOptions;
2use super::utils::{colorize_tag, format_path, make_clickable_link};
3use crate::core::{ScanResult, TodoItem};
4use colored::Colorize;
5use std::io::{self, Write};
6use std::path::Path;
7
8/// Renders `result` as a flat, one-line-per-item list sorted by file then
9/// line number.
10pub fn print_flat<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    let mut all_items = result.all_items();
21    all_items.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.line.cmp(&b.1.line)));
22
23    for (path, item) in all_items {
24        print_flat_item(writer, &path, &item, options)?;
25    }
26
27    Ok(())
28}
29
30fn print_flat_item<W: Write>(
31    writer: &mut W,
32    path: &Path,
33    item: &TodoItem,
34    options: &PrintOptions,
35) -> io::Result<()> {
36    let display_path = format_path(path, options);
37    let link = make_clickable_link(path, item.line, options);
38
39    let path_str = link.unwrap_or_else(|| {
40        if options.colored {
41            display_path.bold().to_string()
42        } else {
43            display_path.to_string()
44        }
45    });
46
47    let line_col = format!(":{}:{}", item.line, item.column);
48    let line_col_display = if options.colored {
49        line_col.cyan().to_string()
50    } else {
51        line_col
52    };
53
54    let tag = colorize_tag(&item.tag, options);
55
56    writeln!(
57        writer,
58        "{}{} [{}] {}",
59        path_str, line_col_display, tag, item.message
60    )?;
61    Ok(())
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use std::path::PathBuf;
68
69    fn item(tag: &str, line: usize, message: &str) -> TodoItem {
70        TodoItem {
71            tag: tag.to_string(),
72            message: message.to_string(),
73            line,
74            column: 1,
75            line_content: None,
76            author: None,
77            priority: crate::core::TodoPriority::from_tag(tag),
78        }
79    }
80
81    fn options() -> PrintOptions {
82        PrintOptions {
83            clickable_links: false,
84            colored: false,
85            ..PrintOptions::default()
86        }
87    }
88
89    #[test]
90    fn print_flat_reports_empty_result() {
91        let result = ScanResult::new(PathBuf::from("."));
92        let mut buf = Vec::new();
93
94        print_flat(&mut buf, &result, &options()).unwrap();
95
96        assert_eq!(
97            String::from_utf8(buf).unwrap().trim(),
98            "No TODO items found."
99        );
100    }
101
102    #[test]
103    fn print_flat_sorts_by_file_then_line() {
104        let mut result = ScanResult::new(PathBuf::from("."));
105        result.add_file(PathBuf::from("b.rs"), vec![item("TODO", 5, "second file")]);
106        result.add_file(
107            PathBuf::from("a.rs"),
108            vec![
109                item("FIXME", 2, "later line"),
110                item("TODO", 1, "first line"),
111            ],
112        );
113        let mut buf = Vec::new();
114
115        print_flat(&mut buf, &result, &options()).unwrap();
116        let output = String::from_utf8(buf).unwrap();
117        let lines: Vec<&str> = output.lines().collect();
118
119        assert_eq!(lines.len(), 3);
120        assert!(lines[0].contains("a.rs") && lines[0].contains("first line"));
121        assert!(lines[1].contains("a.rs") && lines[1].contains("later line"));
122        assert!(lines[2].contains("b.rs") && lines[2].contains("second file"));
123    }
124
125    #[test]
126    fn print_flat_colored_variant_includes_tag_and_message() {
127        let mut result = ScanResult::new(PathBuf::from("."));
128        result.add_file(PathBuf::from("a.rs"), vec![item("TODO", 1, "hello")]);
129        let opts = PrintOptions {
130            colored: true,
131            clickable_links: false,
132            ..PrintOptions::default()
133        };
134        let mut buf = Vec::new();
135
136        print_flat(&mut buf, &result, &opts).unwrap();
137        let output = String::from_utf8(buf).unwrap();
138
139        assert!(output.contains("TODO"));
140        assert!(output.contains("hello"));
141    }
142}