1use std::path::{Path, PathBuf};
2use std::sync::mpsc;
3use std::time::Duration;
4
5use notify::{Config, PollWatcher, RecursiveMode, Watcher};
6
7use crate::debounce::{ChangeKind, Debouncer, RawChange};
8use crate::filter::PathFilter;
9
10#[derive(Debug, thiserror::Error)]
11pub enum WatchError {
12 #[error("notify error: {0}")]
13 Notify(#[from] notify::Error),
14 #[error("watch root does not exist: {0}")]
15 RootNotFound(String),
16}
17
18pub struct FileWatcher {
21 _watcher: PollWatcher,
22 receiver: mpsc::Receiver<notify::Result<notify::Event>>,
23 root: PathBuf,
24 filter: PathFilter,
25 debouncer: Debouncer,
26}
27
28impl FileWatcher {
29 pub fn new(root: &Path, filter: PathFilter) -> Result<Self, WatchError> {
30 Self::with_poll_interval(root, filter, Duration::from_millis(200))
31 }
32
33 pub fn with_poll_interval(root: &Path, filter: PathFilter, interval: Duration) -> Result<Self, WatchError> {
34 if !root.exists() {
35 return Err(WatchError::RootNotFound(root.display().to_string()));
36 }
37
38 let (tx, rx) = mpsc::channel();
39 let config = Config::default()
40 .with_poll_interval(interval)
41 .with_compare_contents(true);
42 let mut watcher = PollWatcher::new(tx, config)?;
43 watcher.watch(root, RecursiveMode::Recursive)?;
44
45 Ok(FileWatcher {
46 _watcher: watcher,
47 receiver: rx,
48 root: root.to_path_buf(),
49 filter,
50 debouncer: Debouncer::new(),
51 })
52 }
53
54 pub fn poll_changes(&mut self) -> Vec<RawChange> {
57 while let Ok(event_result) = self.receiver.try_recv() {
59 if let Ok(event) = event_result {
60 let kind = match event.kind {
61 notify::EventKind::Create(_) => ChangeKind::Created,
62 notify::EventKind::Modify(_) => ChangeKind::Modified,
63 notify::EventKind::Remove(_) => ChangeKind::Deleted,
64 _ => continue,
65 };
66
67 for path in event.paths {
68 if let Ok(relative) = path.strip_prefix(&self.root) {
70 let rel_str = relative.to_string_lossy().to_string();
71 if self.filter.is_included(&rel_str) {
72 self.debouncer.push(RawChange {
73 path: relative.to_path_buf(),
74 kind: kind.clone(),
75 });
76 }
77 }
78 }
79 }
80 }
81
82 self.debouncer.drain()
83 }
84
85 pub fn root(&self) -> &Path {
87 &self.root
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use tempfile::TempDir;
95 use std::fs;
96
97 fn test_watcher(root: &Path, filter: PathFilter) -> FileWatcher {
98 FileWatcher::with_poll_interval(root, filter, Duration::from_millis(100)).unwrap()
99 }
100
101 fn poll_with_retry(watcher: &mut FileWatcher, retries: u32) -> Vec<RawChange> {
103 for _ in 0..retries {
104 std::thread::sleep(Duration::from_millis(300));
105 let changes = watcher.poll_changes();
106 if !changes.is_empty() {
107 return changes;
108 }
109 }
110 vec![]
111 }
112
113 #[test]
114 fn detect_file_change() {
115 let dir = TempDir::new().unwrap();
116 let file_path = dir.path().join("test.rs");
117 fs::write(&file_path, b"original").unwrap();
118
119 let filter = PathFilter::new(vec![], vec![]);
120 let mut watcher = test_watcher(dir.path(), filter);
121
122 std::thread::sleep(Duration::from_millis(500));
124
125 fs::write(&file_path, b"modified").unwrap();
127
128 let changes = poll_with_retry(&mut watcher, 10);
129 assert!(!changes.is_empty(), "should detect the file change");
130 }
131
132 #[test]
133 fn filtered_files_ignored() {
134 let dir = TempDir::new().unwrap();
135 let log_path = dir.path().join("debug.log");
136 let rs_path = dir.path().join("main.rs");
137 fs::write(&log_path, b"log").unwrap();
138 fs::write(&rs_path, b"code").unwrap();
139
140 let filter = PathFilter::new(vec![], vec!["*.log".into()]);
141 let mut watcher = test_watcher(dir.path(), filter);
142
143 std::thread::sleep(Duration::from_millis(500));
145
146 fs::write(&log_path, b"more log").unwrap();
147 fs::write(&rs_path, b"more code").unwrap();
148
149 let changes = poll_with_retry(&mut watcher, 10);
150
151 let paths: Vec<_> = changes.iter().map(|c| c.path.clone()).collect();
153 assert!(!paths.iter().any(|p| p.to_string_lossy().contains(".log")),
154 "log files should be filtered out");
155 }
156
157 #[test]
158 fn detect_new_file() {
159 let dir = TempDir::new().unwrap();
160 let filter = PathFilter::new(vec![], vec![]);
161 let mut watcher = test_watcher(dir.path(), filter);
162
163 std::thread::sleep(Duration::from_millis(500));
165
166 let new_file = dir.path().join("new.rs");
168 fs::write(&new_file, b"new content").unwrap();
169
170 let changes = poll_with_retry(&mut watcher, 10);
171 assert!(!changes.is_empty(), "should detect new file creation");
172 }
173}