Skip to main content

oo_ide/editor/
file_watcher.rs

1use std::collections::HashMap;
2use std::path::PathBuf;
3use std::sync::mpsc::{channel, Receiver};
4use std::time::Duration;
5
6use notify::{RecommendedWatcher, RecursiveMode};
7use notify_debouncer_mini::{new_debouncer, Debouncer};
8
9use crate::prelude::*;
10
11#[derive(Debug)]
12pub struct FileWatcher {
13    // Lazily-initialised Debouncer so creating many AppState instances in tests
14    // doesn't exhaust OS file descriptors. The underlying watcher is created on
15    // first call to `watch()`.
16    watcher: Option<Debouncer<RecommendedWatcher>>,
17    events_rx: Option<Receiver<Result<Vec<notify_debouncer_mini::DebouncedEvent>, notify::Error>>>,
18    watched_paths: HashMap<PathBuf, PathBuf>,
19}
20
21impl FileWatcher {
22    pub fn new() -> Self {
23        Self {
24            watcher: None,
25            events_rx: None,
26            watched_paths: HashMap::new(),
27        }
28    }
29
30    fn ensure_watcher(&mut self) -> Result<()> {
31        if self.watcher.is_some() {
32            return Ok(());
33        }
34        let (tx, rx) = channel();
35        let watcher = new_debouncer(Duration::from_millis(500), tx)
36            .map_err(|e| anyhow!("Failed to create file debouncer: {}", e))?;
37        self.events_rx = Some(rx);
38        self.watcher = Some(watcher);
39        Ok(())
40    }
41
42    pub fn watch(&mut self, path: PathBuf) -> Result<()> {
43        if self.watched_paths.contains_key(&path) {
44            return Ok(());
45        }
46
47        let dir = path.parent().unwrap_or(&path);
48        if !dir.exists() {
49            return Ok(());
50        }
51
52        // Lazily create the underlying watcher to avoid using up file descriptors
53        // for ephemeral AppState instances created by tests.
54        self.ensure_watcher()?;
55
56        if let Some(ref mut deb) = self.watcher {
57            deb.watcher()
58                .watch(dir, RecursiveMode::NonRecursive)
59                .map_err(|e| anyhow!("Failed to watch {:?}: {}", dir, e))?;
60            self.watched_paths.insert(path.clone(), dir.to_path_buf());
61        }
62
63        Ok(())
64    }
65
66    pub fn unwatch(&mut self, path: &PathBuf) -> Result<()> {
67        if let Some(dir) = self.watched_paths.remove(path)
68            && let Some(ref mut deb) = self.watcher {
69                deb.watcher()
70                    .unwatch(&dir)
71                    .map_err(|e| anyhow!("Failed to unwatch {:?}: {}", dir, e))?;
72            }
73        Ok(())
74    }
75
76    pub fn check_events(&self) -> Vec<PathBuf> {
77        let mut changed = Vec::new();
78        if let Some(ref rx) = self.events_rx {
79            while let Ok(events) = rx.try_recv() {
80                if let Ok(events) = events {
81                    for event in events {
82                        // Treat any debounced event as a potential modification
83                        // to a watched file. Clone the path and collect it.
84                        changed.push(event.path.clone());
85                    }
86                }
87            }
88        }
89        // Deduplicate to avoid repetitive checks upstream.
90        changed.sort();
91        changed.dedup();
92        changed
93    }
94
95    pub fn is_watching(&self, path: &PathBuf) -> bool {
96        self.watched_paths.contains_key(path)
97    }
98}
99
100impl Default for FileWatcher {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::editor::buffer::Buffer;
110    use tempfile::tempdir;
111    use std::fs;
112    use std::time::Duration;
113
114    #[test]
115    fn file_watcher_detects_external_modification() {
116        // Create a temporary file and open it in a Buffer, then watch it.
117        let dir = tempdir().expect("tempdir");
118        let file_path = dir.path().join("test_file.rs");
119        fs::write(&file_path, "fn main() {}\n").expect("write initial");
120
121        let mut buf = Buffer::open(&file_path).expect("open buffer");
122        buf.compute_and_store_file_hash();
123
124        let mut fw = FileWatcher::new();
125        fw.watch(file_path.clone()).expect("watch");
126
127        // Modify file externally and wait for the debouncer interval.
128        fs::write(&file_path, "fn main() { println!(\"hi\"); }\n").expect("write modified");
129        std::thread::sleep(Duration::from_millis(700));
130
131        let changed = fw.check_events();
132        assert!(changed.contains(&file_path), "changed paths: {:?}", changed);
133
134        assert!(buf.check_external_modification(), "buffer failed to detect external modification");
135    }
136}