todoit 0.1.0

CLI Tool for showing all TODO's in a project
use std::{fs, path::PathBuf, fmt::Display};

use clap::Parser;
use ignore::WalkBuilder;
use regex::Regex;


#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Directory to list all TODO's from
    dir: Option<PathBuf>,

    #[arg(short, long)]
    /// Exclude file
    exclude: Vec<String>,

    #[arg(long)]
    /// Include hidden files
    hidden: bool,

    #[arg(long)]
    /// Sort todo's ascending by urgency instead of descending
    ascending: bool,

    // TODO: Add option to print urgency as a number
    // TODOOO: Add option for including ignored files
}

#[derive(Debug)]
struct Todo {
    urgency: usize,
    comment: String,
    path: PathBuf,
    line: usize
}

impl Display for Todo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let mut todo = "TOD".to_string();
        for _ in 0..self.urgency {
            todo = todo + "O";
        }
        write!(f, "{}:{} {}: {}", self.path.display(), self.line, todo, self.comment)
    }
}


fn main() {
    let mut args = Args::parse();

    args.exclude.append(&mut vec!["LICENSE".to_string(), "README.md".to_string()]);

    let dir = args.dir.unwrap_or(".".into());

    let walker = WalkBuilder::new(dir)
        .hidden(!args.hidden)
        .follow_links(false)
        .filter_entry(move |file| {
            let file_name = file.file_name().to_str().unwrap_or("");

            let not_lock = !file_name.ends_with(".lock");

            let not_excluded = !args.exclude.contains(&file_name.to_string());

            not_lock && not_excluded
        })
        .build();

    let comment_regex = Regex::new(r"(.*)(/?)// TOD(O*): (.*)").unwrap();

    let mut todos: Vec<Todo> = vec![];
    for ele in walker {
        match ele {
            Ok(a) => {
                if a.file_type().unwrap().is_dir() {
                    continue;
                } else {
                    let contents = match fs::read_to_string(a.path()) {
                        Ok(a) => a,
                        Err(e) => {
                            eprintln!("Error reading file: {e}");
                            continue;
                        },
                    };
                    let lines = contents.split("\n");

                    let mut i = 0;
                    for line in lines {
                       i += 1;
                        for (_, [_, _, os, todo]) in comment_regex.captures_iter(line).map(|c| c.extract()) {
                            let todo = Todo {
                                urgency: os.len(),
                                comment: todo.to_string(),
                                line: i,
                                // TODOO: Remove the ./ at the beginning
                                path: a.path().to_path_buf()
                            };
                            todos.push(todo);
                        };
                    }
                    
                }
            },
            Err(e) => {
                eprintln!("Error: {e}");
            },
        }
    }

    if args.ascending {
        // Sort by urgency, ascending
        todos.sort_by(|a, b| a.urgency.partial_cmp(&b.urgency).unwrap() );
    } else {
        // Sort by urgency, descending
        todos.sort_by(|a, b| b.urgency.partial_cmp(&a.urgency).unwrap() );
    }

    // TODOO: Add colors
    if todos.is_empty() {
        println!("No TODO's found. Well done :)");
    } else {
        for todo in todos {
            println!("{}", todo);
        }
    }
}