oo_ide/editor/
file_watcher.rs1use 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 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 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 changed.push(event.path.clone());
85 }
86 }
87 }
88 }
89 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 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 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}