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 tempfile::TempDir;
104
105 #[tokio::test]
106 async fn test_watcher_creation() {
107 let temp = TempDir::new().unwrap();
108 let notebook = temp.path().join("test.rs");
109 fs::write(¬ebook, "// test").unwrap();
110
111 let watcher = FileWatcher::new(¬ebook);
112 assert!(watcher.is_ok());
113 }
114}