use std::path::{Path, PathBuf};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Mode {
Recursive,
Flat,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Anchor {
pub path: PathBuf,
pub mode: Mode,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PathKind {
Directory,
File,
Missing,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct WatchPlan {
pub anchors: Vec<Anchor>,
pub pending: Vec<PathBuf>,
}
impl WatchPlan {
pub(crate) fn build(
paths: impl IntoIterator<Item = (PathBuf, PathKind)>,
nearest_existing: impl Fn(&Path) -> Option<PathBuf>,
) -> Self {
let mut anchors: Vec<Anchor> = vec![];
let mut pending = vec![];
for (path, kind) in paths {
match kind {
PathKind::Directory => anchors.push(Anchor {
path,
mode: Mode::Recursive,
}),
PathKind::File => {
if let Some(parent) = path.parent() {
anchors.push(Anchor {
path: parent.to_path_buf(),
mode: Mode::Flat,
});
}
}
PathKind::Missing => {
if let Some(ancestor) = nearest_existing(&path) {
anchors.push(Anchor {
path: ancestor,
mode: Mode::Flat,
});
}
pending.push(path);
}
}
}
anchors.sort_by(|a, b| a.path.cmp(&b.path).then_with(|| a.mode.cmp(&b.mode)));
let mut merged: Vec<Anchor> = vec![];
for anchor in anchors {
if merged.iter().any(|kept| {
kept.mode == Mode::Recursive
&& (anchor.path == kept.path || anchor.path.starts_with(&kept.path))
}) {
continue;
}
if let Some(existing) = merged.iter_mut().find(|kept| kept.path == anchor.path) {
if anchor.mode == Mode::Recursive {
existing.mode = Mode::Recursive;
let path = existing.path.clone();
merged.retain(|kept| kept.path == path || !kept.path.starts_with(&path));
}
continue;
}
merged.push(anchor);
}
pending.sort();
pending.dedup();
Self {
anchors: merged,
pending,
}
}
}
impl PartialOrd for Mode {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Mode {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let rank = |mode: &Mode| match mode {
Mode::Recursive => 0,
Mode::Flat => 1,
};
rank(self).cmp(&rank(other))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(s: &str) -> PathBuf {
PathBuf::from(s)
}
#[test]
fn directories_are_recursive_and_files_watch_their_parent() {
let plan = WatchPlan::build(
[
(p("/home/u/.config/hypr"), PathKind::Directory),
(p("/home/u/.zshrc"), PathKind::File),
(p("/home/u/.gitconfig"), PathKind::File),
],
|_| None,
);
assert_eq!(
plan.anchors,
vec![
Anchor {
path: p("/home/u"),
mode: Mode::Flat
},
Anchor {
path: p("/home/u/.config/hypr"),
mode: Mode::Recursive
},
]
);
assert!(plan.pending.is_empty());
}
#[test]
fn a_recursive_anchor_absorbs_anchors_below_it() {
let plan = WatchPlan::build(
[
(p("/home/u/.config/hypr/bindings.lua"), PathKind::File),
(p("/home/u/.config"), PathKind::Directory),
(p("/home/u/.config/mise"), PathKind::Directory),
],
|_| None,
);
assert_eq!(
plan.anchors,
vec![Anchor {
path: p("/home/u/.config"),
mode: Mode::Recursive
}]
);
}
#[test]
fn missing_paths_are_pending_and_watched_through_an_ancestor() {
let plan = WatchPlan::build(
[(p("/home/u/.config/later/file"), PathKind::Missing)],
|path| {
assert_eq!(path, Path::new("/home/u/.config/later/file"));
Some(p("/home/u/.config"))
},
);
assert_eq!(
plan.anchors,
vec![Anchor {
path: p("/home/u/.config"),
mode: Mode::Flat
}]
);
assert_eq!(plan.pending, vec![p("/home/u/.config/later/file")]);
}
#[test]
fn recursive_wins_over_flat_on_the_same_path() {
let plan = WatchPlan::build(
[
(p("/home/u/.config/a"), PathKind::File),
(p("/home/u/.config"), PathKind::Directory),
],
|_| None,
);
assert_eq!(plan.anchors.len(), 1);
assert_eq!(plan.anchors[0].mode, Mode::Recursive);
}
}