Skip to main content

lspkit_live/
watcher.rs

1//! Filesystem watcher that emits coarse change events.
2//!
3//! Wraps [`notify`] and forwards events to a tokio channel so consumers can
4//! await them with the usual async machinery. Exclusion rules let callers
5//! ignore noisy directories (`target/`, `.git/`, vendor caches, etc.).
6
7use std::path::{Path, PathBuf};
8use std::sync::mpsc as std_mpsc;
9
10use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
11use tokio::sync::mpsc;
12
13/// A coarse change event suitable for triggering re-analysis.
14#[non_exhaustive]
15#[derive(Debug, Clone)]
16pub enum ChangeEvent {
17    /// One or more paths under the watched root were created or modified.
18    Touched(Vec<PathBuf>),
19    /// One or more paths were removed.
20    Removed(Vec<PathBuf>),
21}
22
23/// Errors from setting up or running the watcher.
24#[non_exhaustive]
25#[derive(Debug, thiserror::Error)]
26pub enum WatcherError {
27    /// The filesystem watcher could not be created.
28    #[error("watcher setup failed: {0}")]
29    Setup(String),
30    /// A path could not be watched.
31    #[error("watch failed: {0}")]
32    Watch(String),
33}
34
35/// A filesystem watcher that streams [`ChangeEvent`] values.
36pub struct FileWatcher {
37    _inner: RecommendedWatcher,
38    rx: mpsc::UnboundedReceiver<ChangeEvent>,
39}
40
41impl FileWatcher {
42    /// Start watching `root` recursively. Returns a watcher that streams events.
43    ///
44    /// # Errors
45    /// Returns [`WatcherError`] if the watcher cannot be created or the path
46    /// cannot be watched.
47    pub fn new(root: &Path) -> Result<Self, WatcherError> {
48        let (event_tx, event_rx) = std_mpsc::channel::<notify::Result<Event>>();
49        let mut watcher = notify::recommended_watcher(event_tx)
50            .map_err(|e| WatcherError::Setup(e.to_string()))?;
51        watcher
52            .watch(root, RecursiveMode::Recursive)
53            .map_err(|e| WatcherError::Watch(e.to_string()))?;
54
55        let (out_tx, out_rx) = mpsc::unbounded_channel();
56        std::thread::spawn(move || {
57            for result in event_rx {
58                let Ok(event) = result else { continue };
59                let mapped = map_event(event);
60                if let Some(change) = mapped {
61                    if out_tx.send(change).is_err() {
62                        break;
63                    }
64                }
65            }
66        });
67
68        Ok(Self {
69            _inner: watcher,
70            rx: out_rx,
71        })
72    }
73
74    /// Await the next change event. Returns `None` when the watcher is dropped.
75    pub async fn next(&mut self) -> Option<ChangeEvent> {
76        self.rx.recv().await
77    }
78}
79
80fn map_event(event: Event) -> Option<ChangeEvent> {
81    match event.kind {
82        EventKind::Create(_) | EventKind::Modify(_) => Some(ChangeEvent::Touched(event.paths)),
83        EventKind::Remove(_) => Some(ChangeEvent::Removed(event.paths)),
84        _ => None,
85    }
86}