use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::mpsc::{channel, Receiver};
use std::time::Duration;
use notify::{RecommendedWatcher, RecursiveMode};
use notify_debouncer_mini::{new_debouncer, Debouncer};
use crate::prelude::*;
#[derive(Debug)]
pub struct FileWatcher {
watcher: Option<Debouncer<RecommendedWatcher>>,
events_rx: Option<Receiver<Result<Vec<notify_debouncer_mini::DebouncedEvent>, notify::Error>>>,
watched_paths: HashMap<PathBuf, PathBuf>,
}
impl FileWatcher {
pub fn new() -> Self {
Self {
watcher: None,
events_rx: None,
watched_paths: HashMap::new(),
}
}
fn ensure_watcher(&mut self) -> Result<()> {
if self.watcher.is_some() {
return Ok(());
}
let (tx, rx) = channel();
let watcher = new_debouncer(Duration::from_millis(500), tx)
.map_err(|e| anyhow!("Failed to create file debouncer: {}", e))?;
self.events_rx = Some(rx);
self.watcher = Some(watcher);
Ok(())
}
pub fn watch(&mut self, path: PathBuf) -> Result<()> {
if self.watched_paths.contains_key(&path) {
return Ok(());
}
let dir = path.parent().unwrap_or(&path);
if !dir.exists() {
return Ok(());
}
self.ensure_watcher()?;
if let Some(ref mut deb) = self.watcher {
deb.watcher()
.watch(dir, RecursiveMode::NonRecursive)
.map_err(|e| anyhow!("Failed to watch {:?}: {}", dir, e))?;
self.watched_paths.insert(path.clone(), dir.to_path_buf());
}
Ok(())
}
pub fn unwatch(&mut self, path: &PathBuf) -> Result<()> {
if let Some(dir) = self.watched_paths.remove(path)
&& let Some(ref mut deb) = self.watcher {
deb.watcher()
.unwatch(&dir)
.map_err(|e| anyhow!("Failed to unwatch {:?}: {}", dir, e))?;
}
Ok(())
}
pub fn check_events(&self) -> Vec<PathBuf> {
let mut changed = Vec::new();
if let Some(ref rx) = self.events_rx {
while let Ok(events) = rx.try_recv() {
if let Ok(events) = events {
for event in events {
changed.push(event.path.clone());
}
}
}
}
changed.sort();
changed.dedup();
changed
}
pub fn is_watching(&self, path: &PathBuf) -> bool {
self.watched_paths.contains_key(path)
}
}
impl Default for FileWatcher {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::editor::buffer::Buffer;
use tempfile::tempdir;
use std::fs;
use std::time::Duration;
#[test]
fn file_watcher_detects_external_modification() {
let dir = tempdir().expect("tempdir");
let file_path = dir.path().join("test_file.rs");
fs::write(&file_path, "fn main() {}\n").expect("write initial");
let mut buf = Buffer::open(&file_path).expect("open buffer");
buf.compute_and_store_file_hash();
let mut fw = FileWatcher::new();
fw.watch(file_path.clone()).expect("watch");
fs::write(&file_path, "fn main() { println!(\"hi\"); }\n").expect("write modified");
std::thread::sleep(Duration::from_millis(700));
let changed = fw.check_events();
assert!(changed.contains(&file_path), "changed paths: {:?}", changed);
assert!(buf.check_external_modification(), "buffer failed to detect external modification");
}
}