oo-ide 0.0.3

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
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 {
    // Lazily-initialised Debouncer so creating many AppState instances in tests
    // doesn't exhaust OS file descriptors. The underlying watcher is created on
    // first call to `watch()`.
    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(());
        }

        // Lazily create the underlying watcher to avoid using up file descriptors
        // for ephemeral AppState instances created by tests.
        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 {
                        // Treat any debounced event as a potential modification
                        // to a watched file. Clone the path and collect it.
                        changed.push(event.path.clone());
                    }
                }
            }
        }
        // Deduplicate to avoid repetitive checks upstream.
        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() {
        // Create a temporary file and open it in a Buffer, then watch it.
        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");

        // Modify file externally and wait for the debouncer interval.
        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");
    }
}