use std::path::{Path, PathBuf};
use std::sync::mpsc::{RecvTimeoutError, channel};
use std::time::{Duration, Instant};
use anyhow::{Context, Result};
use clap::Args;
use notify::{RecursiveMode, Watcher};
use crate::commands::index::{self, IndexArgs};
use crate::config::Config;
#[derive(Debug, Args)]
pub struct WatchArgs {
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(long, default_value_t = 1500)]
pub debounce_ms: u64,
}
pub fn is_relevant_change(path: &Path) -> bool {
const SKIP: &[&str] = &[".git", "target", "node_modules", ".asd", ".asd-state.db"];
for comp in path.components() {
if let std::path::Component::Normal(os) = comp {
if os.to_str().is_some_and(|n| SKIP.contains(&n)) {
return false;
}
}
}
matches!(
path.extension().and_then(|e| e.to_str()),
Some(
"py" | "ts"
| "tsx"
| "mts"
| "cts"
| "rs"
| "go"
| "java"
| "cs"
| "rb"
| "kt"
| "kts"
| "swift"
)
)
}
pub fn run(cfg: &Config, args: WatchArgs) -> Result<()> {
let (tx, rx) = channel();
let mut watcher = notify::recommended_watcher(move |res| {
let _ = tx.send(res);
})
.context("create filesystem watcher")?;
watcher
.watch(&args.path, RecursiveMode::Recursive)
.with_context(|| format!("watch {}", args.path.display()))?;
println!(
"asd watch: watching {} (Ctrl-C to stop)",
args.path.display()
);
reindex(cfg, &args.path);
let debounce = Duration::from_millis(args.debounce_ms);
let mut pending: Option<Instant> = None;
loop {
let timeout = pending
.map(|t| debounce.saturating_sub(t.elapsed()))
.unwrap_or_else(|| Duration::from_secs(3600));
match rx.recv_timeout(timeout) {
Ok(Ok(event)) => {
if event.paths.iter().any(|p| is_relevant_change(p)) {
pending = Some(Instant::now());
}
}
Ok(Err(_)) => {} Err(RecvTimeoutError::Timeout) => {
if pending.take_if(|t| t.elapsed() >= debounce).is_some() {
reindex(cfg, &args.path);
}
}
Err(RecvTimeoutError::Disconnected) => break,
}
}
Ok(())
}
fn reindex(cfg: &Config, path: &Path) {
print!("asd watch: reindexing… ");
let _ = std::io::Write::flush(&mut std::io::stdout());
match index::run(
cfg,
IndexArgs {
path: path.to_path_buf(),
verbose: false,
},
) {
Ok(()) => println!("done"),
Err(e) => println!("failed: {e}"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_files_are_relevant() {
assert!(is_relevant_change(Path::new("src/foo.rs")));
assert!(is_relevant_change(Path::new("app/models/user.py")));
assert!(is_relevant_change(Path::new("a/b/Handler.kt")));
}
#[test]
fn noise_is_ignored() {
assert!(!is_relevant_change(Path::new("target/debug/foo.rs")));
assert!(!is_relevant_change(Path::new(".git/index")));
assert!(!is_relevant_change(Path::new("node_modules/x/y.ts")));
assert!(!is_relevant_change(Path::new(".asd/index.log")));
assert!(!is_relevant_change(Path::new("./.asd-state.db")));
assert!(!is_relevant_change(Path::new("README.md")));
assert!(!is_relevant_change(Path::new("Cargo.toml")));
}
}