use std::path::{Path, PathBuf};
use std::time::Instant;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
pub fn spawn_watcher(project_path: PathBuf, tx: mpsc::UnboundedSender<PathBuf>) -> JoinHandle<()> {
tokio::task::spawn_blocking(move || {
use notify::{RecursiveMode, Watcher};
let tx_clone = tx;
let mut last_sent = Instant::now();
let mut watcher =
match notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
if let Ok(event) = res {
match event.kind {
notify::EventKind::Create(_) | notify::EventKind::Modify(_) => {}
_ => return,
}
for path in event.paths {
if !is_relevant(&path) {
continue;
}
let now = Instant::now();
if now.duration_since(last_sent).as_millis() < 500 {
continue;
}
last_sent = now;
let _ = tx_clone.send(path);
}
}
}) {
Ok(w) => w,
Err(e) => {
tracing::error!("Failed to create watcher: {e}");
return;
}
};
if let Err(e) = watcher.watch(&project_path, RecursiveMode::Recursive) {
tracing::error!("Failed to watch {}: {e}", project_path.display());
return;
}
tracing::info!("Watching {} for changes", project_path.display());
loop {
std::thread::sleep(std::time::Duration::from_secs(3600));
}
})
}
pub fn is_relevant(path: &Path) -> bool {
let skip_dirs = ["node_modules", "target", "dist", "build", "__pycache__"];
for component in path.components() {
if let std::path::Component::Normal(name) = component {
let name = name.to_string_lossy();
if name.starts_with('.') {
return false;
}
if skip_dirs.iter().any(|d| *d == &*name) {
return false;
}
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_relevant_filter() {
assert!(is_relevant(Path::new("src/main.rs")));
assert!(is_relevant(Path::new("README.md")));
assert!(is_relevant(Path::new("app/config/plans.js")));
assert!(!is_relevant(Path::new(".git/config")));
assert!(!is_relevant(Path::new(".env")));
assert!(!is_relevant(Path::new("src/.hidden/file.rs")));
assert!(!is_relevant(Path::new("node_modules/foo/bar.js")));
assert!(!is_relevant(Path::new("target/debug/main")));
}
#[test]
fn test_debounce_skips_fast_events() {
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let mut last_sent = Instant::now() - std::time::Duration::from_secs(10);
let debounce_ms: u128 = 500;
let process_event = |last: &mut Instant, counter: &AtomicUsize| {
let now = Instant::now();
if now.duration_since(*last).as_millis() >= debounce_ms {
*last = now;
counter.fetch_add(1, Ordering::SeqCst);
}
};
process_event(&mut last_sent, &counter_clone);
assert_eq!(counter.load(Ordering::SeqCst), 1);
process_event(&mut last_sent, &counter_clone);
assert_eq!(counter.load(Ordering::SeqCst), 1);
std::thread::sleep(std::time::Duration::from_millis(550));
process_event(&mut last_sent, &counter_clone);
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
}