Skip to main content

todo_tree/
scanner.rs

1use crate::parser::TodoParser;
2use anyhow::{Context, Result};
3use ignore::WalkBuilder;
4use ignore::overrides::OverrideBuilder;
5use std::path::Path;
6use todo_tree_core::{ScanResult, TodoItem};
7
8#[derive(Debug, Clone)]
9pub struct ScanOptions {
10    pub include: Vec<String>,
11    pub exclude: Vec<String>,
12    pub max_depth: usize,
13    pub follow_links: bool,
14    pub hidden: bool,
15    pub threads: usize,
16    pub respect_gitignore: bool,
17}
18
19impl Default for ScanOptions {
20    fn default() -> Self {
21        Self {
22            include: Vec::new(),
23            exclude: Vec::new(),
24            max_depth: 0,
25            follow_links: false,
26            hidden: false,
27            threads: 0,
28            respect_gitignore: true,
29        }
30    }
31}
32
33pub struct Scanner {
34    parser: TodoParser,
35    options: ScanOptions,
36}
37
38impl Scanner {
39    pub fn new(parser: TodoParser, options: ScanOptions) -> Self {
40        Self { parser, options }
41    }
42
43    pub fn scan(&self, root: &Path) -> Result<ScanResult> {
44        let root = root
45            .canonicalize()
46            .with_context(|| format!("Failed to resolve path: {}", root.display()))?;
47
48        let mut result = ScanResult::new(root.clone());
49        let mut builder = WalkBuilder::new(&root);
50
51        builder
52            .hidden(!self.options.hidden)
53            .follow_links(self.options.follow_links)
54            .git_ignore(self.options.respect_gitignore)
55            .git_global(self.options.respect_gitignore)
56            .git_exclude(self.options.respect_gitignore);
57
58        if self.options.max_depth > 0 {
59            builder.max_depth(Some(self.options.max_depth));
60        }
61
62        if self.options.threads > 0 {
63            builder.threads(self.options.threads);
64        }
65
66        if !self.options.include.is_empty() || !self.options.exclude.is_empty() {
67            let mut override_builder = OverrideBuilder::new(&root);
68            for pattern in &self.options.include {
69                override_builder
70                    .add(pattern)
71                    .with_context(|| format!("Invalid include pattern: {}", pattern))?;
72            }
73
74            for pattern in &self.options.exclude {
75                let exclude_pattern = format!("!{}", pattern);
76                override_builder
77                    .add(&exclude_pattern)
78                    .with_context(|| format!("Invalid exclude pattern: {}", pattern))?;
79            }
80
81            let overrides = override_builder.build()?;
82            builder.overrides(overrides);
83        }
84
85        for entry in builder.build() {
86            match entry {
87                Ok(entry) => {
88                    let path = entry.path();
89
90                    if path.is_dir() {
91                        continue;
92                    }
93
94                    if let Some(file_type) = entry.file_type()
95                        && !file_type.is_file()
96                    {
97                        continue;
98                    }
99
100                    match self.parse_file(path) {
101                        Ok(items) => {
102                            result.add_file(path.to_path_buf(), items);
103                        }
104                        Err(_) => {
105                            result.summary.files_scanned += 1;
106                        }
107                    }
108                }
109                Err(_) => {
110                    continue;
111                }
112            }
113        }
114
115        Ok(result)
116    }
117
118    fn parse_file(&self, path: &Path) -> Result<Vec<TodoItem>> {
119        self.parser
120            .parse_file(path)
121            .with_context(|| format!("Failed to parse file: {}", path.display()))
122    }
123}