todo_tree/printer/
flat.rs1use 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
8pub 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}