typ_buffer/watch.rs
1//! Notice when a file changes on disk.
2//!
3//! A rebase, a formatter, or another editor writes the file while it is open.
4//! Without this the editor neither reloads nor warns, and the next save
5//! silently overwrites whatever the other writer did.
6
7use std::path::{Path, PathBuf};
8
9use anyhow::{Context, Result};
10use notify::{RecommendedWatcher, RecursiveMode, Watcher};
11
12/// A live watch. Dropping it stops the watching, which is how opening another
13/// file replaces the old watch rather than accumulating them.
14pub struct FileWatch {
15 _watcher: RecommendedWatcher,
16}
17
18/// Report changes to `path` by calling `on_change` from the watcher's thread.
19///
20/// **Watches the parent directory, not the file.** Editors and formatters write
21/// by rename-over, which destroys the inode a file watch is pinned to and
22/// leaves that watch pointed at nothing — the file keeps changing and the
23/// watcher keeps saying nothing. Watching the directory and filtering by name
24/// survives it, and also sees the file being deleted and recreated.
25///
26/// `on_change` is handed the path as it was given here, not the path the OS
27/// reported, so a caller can compare it against what it has open without
28/// worrying about how each platform spells it.
29pub fn watch_file(path: &Path, on_change: impl Fn(PathBuf) + Send + 'static) -> Result<FileWatch> {
30 let path = path.to_path_buf();
31 let dir = path
32 .parent()
33 .filter(|p| !p.as_os_str().is_empty())
34 .map(Path::to_path_buf)
35 .unwrap_or_else(|| PathBuf::from("."));
36 let name = path
37 .file_name()
38 .context("watching a path with no file name")?
39 .to_os_string();
40
41 let reported = path.clone();
42 let mut watcher = notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
43 let Ok(event) = event else { return };
44 // Access events fire on every read, including our own. Only creation,
45 // modification and removal change what is on disk.
46 if !(event.kind.is_create() || event.kind.is_modify() || event.kind.is_remove()) {
47 return;
48 }
49 if event.paths.iter().any(|p| p.file_name() == Some(&name)) {
50 on_change(reported.clone());
51 }
52 })
53 .context("creating a file watcher")?;
54
55 // ponytail: no debouncing. One save produces several events on every
56 // platform, and the handler on the other end is idempotent — it compares
57 // the file against the buffer and does nothing when they agree. A
58 // debouncer earns its place when an event costs more than that comparison.
59 watcher
60 .watch(&dir, RecursiveMode::NonRecursive)
61 .with_context(|| format!("watching {}", dir.display()))?;
62
63 Ok(FileWatch { _watcher: watcher })
64}