1use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::time::Duration;
8
9use notify_debouncer_mini::{DebounceEventResult, new_debouncer, notify::RecursiveMode};
10use tokio::sync::mpsc;
11
12use crate::error::{ServerError, ServerResult};
13
14#[derive(Debug, Clone)]
16pub enum FileEvent {
17 Modified(PathBuf),
19 Created(PathBuf),
21 Removed(PathBuf),
23}
24
25pub struct FileWatcher {
27 _debouncer: notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>,
29 rx: mpsc::UnboundedReceiver<FileEvent>,
31}
32
33impl FileWatcher {
34 pub fn new(path: impl AsRef<Path>) -> ServerResult<Self> {
36 let path = path.as_ref().to_path_buf();
37 let watch_path = if path.is_file() {
38 path.parent().unwrap_or(Path::new(".")).to_path_buf()
39 } else {
40 path.clone()
41 };
42
43 let (tx, rx) = mpsc::unbounded_channel();
44 let target_file = if path.is_file() {
45 Some(Arc::new(path))
46 } else {
47 None
48 };
49
50 let mut debouncer = new_debouncer(
51 Duration::from_millis(200),
52 move |result: DebounceEventResult| {
53 if let Ok(events) = result {
54 for event in events {
55 let event_path = &event.path;
56
57 if event_path.extension().is_none_or(|ext| ext != "rs") {
59 continue;
60 }
61
62 if let Some(ref target) = target_file
64 && event_path != target.as_ref()
65 {
66 continue;
67 }
68
69 let file_event = if event_path.exists() {
70 FileEvent::Modified(event_path.clone())
71 } else {
72 FileEvent::Removed(event_path.clone())
73 };
74
75 let _ = tx.send(file_event);
76 }
77 }
78 },
79 )
80 .map_err(|e| ServerError::Watch(e.to_string()))?;
81
82 debouncer
83 .watcher()
84 .watch(&watch_path, RecursiveMode::NonRecursive)
85 .map_err(|e| ServerError::Watch(e.to_string()))?;
86
87 Ok(Self {
88 _debouncer: debouncer,
89 rx,
90 })
91 }
92
93 pub async fn recv(&mut self) -> Option<FileEvent> {
95 self.rx.recv().await
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use std::fs;
103 use std::time::Duration;
104 use tempfile::TempDir;
105 use tokio::time::sleep;
106
107 #[tokio::test]
108 async fn test_watcher_creation() {
109 let temp = TempDir::new().unwrap();
110 let notebook = temp.path().join("test.rs");
111 fs::write(¬ebook, "// test").unwrap();
112
113 let watcher = FileWatcher::new(¬ebook);
114 assert!(watcher.is_ok());
115 }
116
117 #[tokio::test]
118 async fn test_watcher_detects_modification() {
119 let temp = TempDir::new().unwrap();
120 let notebook = temp.path().join("test.rs");
121 fs::write(¬ebook, "// initial content").unwrap();
122
123 let mut watcher = FileWatcher::new(¬ebook).unwrap();
124
125 sleep(Duration::from_millis(100)).await;
127
128 fs::write(¬ebook, "// modified content").unwrap();
130
131 let timeout = tokio::time::timeout(Duration::from_secs(2), watcher.recv()).await;
133
134 assert!(timeout.is_ok(), "Watcher did not detect modification");
135 let event = timeout.unwrap();
136
137 match event {
138 Some(FileEvent::Modified(path)) => {
139 assert_eq!(path, notebook);
140 }
141 Some(other) => panic!("Expected Modified event, got {:?}", other),
142 None => panic!("Received None from watcher"),
143 }
144 }
145
146 #[tokio::test]
147 async fn test_watcher_ignores_non_rust_files() {
148 let temp = TempDir::new().unwrap();
149 let notebook = temp.path().join("test.rs");
150 fs::write(¬ebook, "// test").unwrap();
151
152 let mut watcher = FileWatcher::new(¬ebook).unwrap();
153
154 sleep(Duration::from_millis(100)).await;
156
157 let other_file = temp.path().join("test.txt");
159 fs::write(&other_file, "text content").unwrap();
160
161 let timeout =
163 tokio::time::timeout(Duration::from_millis(500), watcher.recv()).await;
164
165 assert!(timeout.is_err(), "Watcher should ignore non-.rs files");
167 }
168
169 #[tokio::test]
170 async fn test_watcher_directory_mode() {
171 let temp = TempDir::new().unwrap();
172 let notebook = temp.path().join("test.rs");
173 fs::write(¬ebook, "// test").unwrap();
174
175 let mut watcher = FileWatcher::new(temp.path()).unwrap();
177
178 sleep(Duration::from_millis(100)).await;
180
181 fs::write(¬ebook, "// modified").unwrap();
183
184 let timeout = tokio::time::timeout(Duration::from_secs(2), watcher.recv()).await;
186
187 assert!(
188 timeout.is_ok(),
189 "Directory watcher did not detect file modification"
190 );
191 }
192
193 #[tokio::test]
194 async fn test_file_event_types() {
195 let event = FileEvent::Modified(PathBuf::from("/test.rs"));
197 assert!(matches!(event, FileEvent::Modified(_)));
198
199 let event = FileEvent::Created(PathBuf::from("/test.rs"));
200 assert!(matches!(event, FileEvent::Created(_)));
201
202 let event = FileEvent::Removed(PathBuf::from("/test.rs"));
203 assert!(matches!(event, FileEvent::Removed(_)));
204 }
205}