todo-tree 0.5.2

A CLI tool to find and display TODO-style comments in your codebase
Documentation
use super::options::PrintOptions;
use super::utils::{colorize_tag, format_path, make_clickable_link, make_line_link};
use colored::Colorize;
use std::collections::HashMap;
use std::io::{self, Write};
use std::path::Path;
use std::path::PathBuf;
use todo_tree_core::{ScanResult, TodoItem};

pub fn print_tree<W: Write>(
    writer: &mut W,
    result: &ScanResult,
    options: &PrintOptions,
) -> io::Result<()> {
    if result.is_empty() {
        writeln!(writer, "{}", "No TODO items found.".dimmed())?;
        return Ok(());
    }

    if options.group_by_tag {
        print_tree_by_tag(writer, result, options)?;
    } else {
        print_tree_by_file(writer, result, options)?;
    }

    Ok(())
}

fn print_tree_by_file<W: Write>(
    writer: &mut W,
    result: &ScanResult,
    options: &PrintOptions,
) -> io::Result<()> {
    let sorted_files = result.sorted_files();
    let total_files = sorted_files.len();

    for (idx, (path, items)) in sorted_files.iter().enumerate() {
        let is_last_file = idx == total_files - 1;
        print_file_header(writer, path, items.len(), is_last_file, options)?;

        let total_items = items.len();
        for (item_idx, item) in items.iter().enumerate() {
            let is_last_item = item_idx == total_items - 1;
            print_tree_item(writer, item, is_last_file, is_last_item, path, options)?;
        }
    }

    Ok(())
}

fn print_tree_by_tag<W: Write>(
    writer: &mut W,
    result: &ScanResult,
    options: &PrintOptions,
) -> io::Result<()> {
    let mut by_tag: HashMap<String, Vec<(PathBuf, TodoItem)>> = HashMap::new();

    for (path, items) in &result.files_map {
        for item in items {
            by_tag
                .entry(item.tag.clone())
                .or_default()
                .push((path.clone(), item.clone()));
        }
    }

    let mut tags: Vec<_> = by_tag.keys().collect();
    tags.sort();

    let total_tags = tags.len();

    for (idx, tag) in tags.iter().enumerate() {
        let is_last_tag = idx == total_tags - 1;
        let items = by_tag.get(*tag).unwrap();

        let prefix = if is_last_tag {
            "└──"
        } else {
            "├──"
        };
        let colored_tag = colorize_tag(tag, options);
        writeln!(writer, "{} {} ({})", prefix, colored_tag, items.len())?;

        let total_items = items.len();
        for (item_idx, (path, item)) in items.iter().enumerate() {
            let is_last_item = item_idx == total_items - 1;
            let tree_prefix = if is_last_tag { "    " } else { "" };
            let item_prefix = if is_last_item {
                "└──"
            } else {
                "├──"
            };

            let display_path = format_path(path, options);
            let link = make_clickable_link(path, item.line, options);

            writeln!(
                writer,
                "{}{} {}:{} - {}",
                tree_prefix,
                item_prefix,
                link.unwrap_or_else(|| display_path.to_string()),
                item.line.to_string().cyan(),
                item.message.dimmed()
            )?;
        }
    }

    Ok(())
}

fn print_file_header<W: Write>(
    writer: &mut W,
    path: &Path,
    item_count: usize,
    is_last: bool,
    options: &PrintOptions,
) -> io::Result<()> {
    let prefix = if is_last { "└──" } else { "├──" };
    let display_path = format_path(path, options);
    let link = make_clickable_link(path, 1, options);

    let path_str = link.unwrap_or_else(|| {
        if options.colored {
            display_path.bold().to_string()
        } else {
            display_path.to_string()
        }
    });

    let count_str = format!("({})", item_count);
    let count_display = if options.colored {
        count_str.dimmed().to_string()
    } else {
        count_str
    };

    writeln!(writer, "{} {} {}", prefix, path_str, count_display)?;
    Ok(())
}

fn print_tree_item<W: Write>(
    writer: &mut W,
    item: &TodoItem,
    is_last_file: bool,
    is_last_item: bool,
    path: &Path,
    options: &PrintOptions,
) -> io::Result<()> {
    let tree_prefix = if is_last_file { "    " } else { "" };
    let item_prefix = if is_last_item {
        "└──"
    } else {
        "├──"
    };

    let tag = colorize_tag(&item.tag, options);
    let line_num = if options.colored {
        format!("L{}", item.line).cyan().to_string()
    } else {
        format!("L{}", item.line)
    };

    let line_display = if options.clickable_links {
        make_line_link(path, item.line, options).unwrap_or_else(|| line_num.clone())
    } else {
        line_num
    };

    let author_str = item
        .author
        .as_ref()
        .map(|a| format!("({})", a))
        .unwrap_or_default();

    if author_str.is_empty() {
        writeln!(
            writer,
            "{}{} [{}] {}: {}",
            tree_prefix, item_prefix, line_display, tag, item.message
        )?;
    } else {
        let author_display = if options.colored {
            author_str.yellow().to_string()
        } else {
            author_str
        };
        writeln!(
            writer,
            "{}{} [{}] {} {}: {}",
            tree_prefix, item_prefix, line_display, tag, author_display, item.message
        )?;
    }

    Ok(())
}