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}