1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
use anyhow::{Context, Result};
use notify::RecursiveMode;
use notify_debouncer_full::{DebounceEventResult, Debouncer, RecommendedCache, new_debouncer};
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
use tracing::debug;
pub struct FileWatcher {
debouncer: Option<Debouncer<notify::RecommendedWatcher, RecommendedCache>>,
}
impl FileWatcher {
/// Create a new file watcher.
pub fn new() -> Result<Self> {
Ok(Self { debouncer: None })
}
/// Watch a directory for changes, returning a receiver for changed file paths.
/// Events are debounced (500ms).
pub fn watch(&mut self, root: &Path) -> Result<mpsc::Receiver<Vec<PathBuf>>> {
let (tx, rx) = mpsc::channel();
let mut debouncer = new_debouncer(
Duration::from_millis(500),
None,
move |result: DebounceEventResult| {
let events = match result {
Ok(events) => events,
Err(errs) => {
for err in errs {
debug!("File watcher error: {:?}", err);
}
return;
}
};
let mut paths = Vec::new();
for event in &events {
use notify::EventKind;
match event.kind {
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
for path in &event.paths {
// Only include files, not directories
if path.is_file() || !path.exists() {
paths.push(path.clone());
}
}
}
_ => {}
}
}
paths.sort();
paths.dedup();
if !paths.is_empty() {
let _ = tx.send(paths);
}
},
)
.context("Failed to create file watcher")?;
debouncer
.watch(root, RecursiveMode::Recursive)
.with_context(|| format!("Failed to watch directory: {}", root.display()))?;
self.debouncer = Some(debouncer);
Ok(rx)
}
/// Add another path to the SAME watch session (must be called after
/// [`watch`](Self::watch) — reuses its debouncer/channel rather than
/// creating a second one). Events from `path` are funneled into the
/// same receiver `watch` already returned. `recursive` selects
/// [`RecursiveMode::Recursive`] vs [`RecursiveMode::NonRecursive`] (a
/// `bool` rather than the `notify` type itself, so callers outside this
/// crate don't need a direct `notify` dependency just to call this).
///
/// Used by `semantex watch` to additionally watch the directory holding
/// the git `HEAD` file (which, for a linked worktree, lives OUTSIDE the
/// project root and so wouldn't be seen by the recursive project-root
/// watch at all) so a branch switch is detected even when it changes no
/// tracked file (e.g. `git switch -c` from an identical tree). The
/// directory rather than `HEAD` itself: git swaps `HEAD` in by rename,
/// and an inotify watch on the file's inode fires once and then dies
/// with the replaced inode — a non-recursive directory watch keeps
/// reporting every subsequent switch.
pub fn watch_additional(&mut self, path: &Path, recursive: bool) -> Result<()> {
let mode = if recursive {
RecursiveMode::Recursive
} else {
RecursiveMode::NonRecursive
};
let debouncer = self
.debouncer
.as_mut()
.context("watch_additional called before watch")?;
debouncer
.watch(path, mode)
.with_context(|| format!("Failed to watch path: {}", path.display()))
}
/// Stop watching.
pub fn stop(&mut self) {
self.debouncer.take();
}
}
impl Drop for FileWatcher {
fn drop(&mut self) {
self.stop();
}
}