mod roots;
pub use roots::RootKind;
use std::collections::{HashMap, hash_map::Entry};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, TryRecvError};
use notify::{
Event, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher,
event::{EventKind, ModifyKind, RenameMode},
};
use roots::is_watched;
#[derive(Debug, thiserror::Error)]
#[error("filesystem watcher: {0}")]
pub struct WatchError(#[from] notify::Error);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeKind {
Touched,
Removed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Change {
pub path: PathBuf,
pub kind: ChangeKind,
}
pub struct Watcher {
repo_root: PathBuf,
kind: RootKind,
_inner: RecommendedWatcher,
rx: Receiver<notify::Result<Event>>,
}
impl Watcher {
pub fn new(repo_root: &Path) -> Result<Self, WatchError> {
Self::with_kind(repo_root, RootKind::Workspace)
}
pub fn with_kind(repo_root: &Path, kind: RootKind) -> Result<Self, WatchError> {
let repo_root = repo_root.canonicalize().map_err(notify::Error::from)?;
let (tx, rx) = mpsc::channel();
let mut inner: RecommendedWatcher = notify::recommended_watcher(move |res| {
let _ = tx.send(res);
})?;
inner.watch(&repo_root, RecursiveMode::Recursive)?;
Ok(Self {
repo_root,
kind,
_inner: inner,
rx,
})
}
pub fn tick(&self) -> Vec<Change> {
let mut raw: Vec<(PathBuf, EventKind)> = Vec::new();
loop {
match self.rx.try_recv() {
Ok(Ok(event)) => ingest(event, &mut raw),
Ok(Err(_)) => {}
Err(TryRecvError::Empty | TryRecvError::Disconnected) => break,
}
}
coalesce(&self.repo_root, self.kind, raw)
}
}
fn ingest(event: Event, raw: &mut Vec<(PathBuf, EventKind)>) {
let paths = event.paths;
let kind = event.kind;
if let EventKind::Modify(ModifyKind::Name(RenameMode::Both)) = kind
&& let [from, to] = paths.as_slice()
{
raw.push((
from.clone(),
EventKind::Modify(ModifyKind::Name(RenameMode::From)),
));
raw.push((
to.clone(),
EventKind::Modify(ModifyKind::Name(RenameMode::To)),
));
return;
}
for path in paths {
raw.push((path, kind));
}
}
fn coalesce(repo_root: &Path, root_kind: RootKind, raw: Vec<(PathBuf, EventKind)>) -> Vec<Change> {
let mut renamed: HashMap<PathBuf, bool> = HashMap::new();
let mut order: Vec<PathBuf> = Vec::new();
for (path, kind) in raw {
if !is_watched(root_kind, repo_root, &path) {
continue;
}
let is_rename = matches!(kind, EventKind::Modify(ModifyKind::Name(_)));
match renamed.entry(path.clone()) {
Entry::Occupied(mut e) => *e.get_mut() |= is_rename,
Entry::Vacant(e) => {
order.push(path);
e.insert(is_rename);
}
}
}
order
.into_iter()
.filter_map(|p| {
let change_kind = classify(renamed.get(&p).copied().unwrap_or(false), &p)?;
Some(Change {
path: p,
kind: change_kind,
})
})
.collect()
}
fn classify(renamed: bool, path: &Path) -> Option<ChangeKind> {
if path.exists() {
Some(ChangeKind::Touched)
} else if renamed {
None
} else {
Some(ChangeKind::Removed)
}
}
#[cfg(test)]
mod tests;