use crate::{data, model::workspace::Workspace};
use artifact::project::fs;
use bezel::gpui::{Context, Task};
use futures::{StreamExt as _, channel::mpsc};
use notify::{RecommendedWatcher, RecursiveMode, Watcher as _};
use std::{
path::{Path, PathBuf},
time::Duration,
};
pub const BOUNCE: u64 = 150;
pub const BOUNCE_RANGE: (u64, u64) = (20, 2_000);
pub fn bounce(ms: u64) -> Duration {
Duration::from_millis(ms.clamp(BOUNCE_RANGE.0, BOUNCE_RANGE.1))
}
pub struct Watch {
_pump: Task<()>,
}
impl Watch {
pub fn open(root: PathBuf, cx: &mut Context<Workspace>) -> Self {
let pump = cx.spawn(async move |workspace, cx| {
loop {
let Some((_watcher, mut knocks, deep)) = arm(&root) else {
return;
};
loop {
if knocks.next().await.is_none() {
return;
}
let settle = workspace
.read_with(cx, |workspace, _| bounce(workspace.settings.watch_bounce))
.unwrap_or_else(|_| bounce(BOUNCE));
cx.background_executor().timer(settle).await;
while knocks.try_recv().is_ok() {}
let held = workspace
.update(cx, |workspace, cx| workspace.reload_project(&root, cx))
.is_ok();
if !held {
return;
}
if !deep && fs::Project::new(&root).cydonia().exists() {
break;
}
}
}
});
Self { _pump: pump }
}
}
fn arm(root: &Path) -> Option<(RecommendedWatcher, mpsc::UnboundedReceiver<()>, bool)> {
let dir =
fs::Project::new(std::fs::canonicalize(root).unwrap_or_else(|_| root.to_owned())).cydonia();
let (tx, rx) = mpsc::unbounded();
let mut watcher = notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
let Ok(event) = event else {
return;
};
if event.paths.iter().any(|path| ours(&dir, path)) {
let _ = tx.unbounded_send(());
}
})
.ok()?;
match watcher.watch(&fs::Project::new(root).cydonia(), RecursiveMode::Recursive) {
Ok(()) => Some((watcher, rx, true)),
Err(_) => watcher
.watch(root, RecursiveMode::NonRecursive)
.ok()
.map(|()| (watcher, rx, false)),
}
}
fn ours(dir: &Path, path: &Path) -> bool {
let Ok(rest) = path.strip_prefix(dir) else {
return path == dir;
};
let Some(head) = rest.components().next() else {
return true;
};
let head = head.as_os_str().to_string_lossy();
if head == "articles" || head == "boards" {
return true;
}
let Some(tail) = head.strip_prefix(data::FILE) else {
return false;
};
matches!(tail, "" | "-wal" | "-journal")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_what_is_read_back_knocks() {
let dir = Path::new("/p/.cydonia");
for path in [
"/p/.cydonia",
"/p/.cydonia/articles/1/content.md",
"/p/.cydonia/boards/1.toml",
"/p/.cydonia/data.db",
"/p/.cydonia/data.db-wal",
] {
assert!(ours(dir, Path::new(path)), "{path} should knock");
}
for path in [
"/p/.cydonia/data.db-shm",
"/p/.cydonia/sessions/1.json",
"/p/src/main.rs",
"/other/.cydonia/articles/1/content.md",
] {
assert!(!ours(dir, Path::new(path)), "{path} should not knock");
}
}
}