pub mod event;
pub mod incremental;
use std::path::Path;
use std::sync::mpsc as std_mpsc;
use std::time::Duration;
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use notify::RecursiveMode;
use notify_debouncer_mini::{DebounceEventResult, new_debouncer};
use event::WatchEvent;
pub struct WatcherHandle {
_debouncer: notify_debouncer_mini::Debouncer<notify::RecommendedWatcher>,
_bridge_thread: std::thread::JoinHandle<()>,
}
const SOURCE_EXTENSIONS: &[&str] = &["ts", "tsx", "js", "jsx", "rs", "py", "go"];
const FULL_REINDEX_FILES: &[&str] = &[
"tsconfig.json",
"package.json",
"pnpm-workspace.yaml",
"Cargo.toml",
"lib.rs",
"main.rs",
"mod.rs",
"__init__.py",
"go.mod",
"go.work",
];
const CRATE_ROOT_FILES: &[&str] = &["Cargo.toml", "lib.rs", "main.rs", "mod.rs"];
fn build_gitignore_matcher(project_root: &Path) -> Gitignore {
let mut builder = GitignoreBuilder::new(project_root);
let gitignore_path = project_root.join(".gitignore");
if gitignore_path.exists() {
let _ = builder.add(&gitignore_path);
}
builder.build().unwrap_or_else(|_| Gitignore::empty())
}
pub fn start_watcher(
watch_root: &Path,
) -> anyhow::Result<(WatcherHandle, std_mpsc::Receiver<WatchEvent>)> {
let (notify_tx, notify_rx) = std::sync::mpsc::channel::<DebounceEventResult>();
let mut debouncer = new_debouncer(Duration::from_millis(75), move |res| {
let _ = notify_tx.send(res);
})?;
debouncer
.watcher()
.watch(watch_root, RecursiveMode::Recursive)?;
let gitignore = build_gitignore_matcher(watch_root);
let (event_tx, event_rx) = std_mpsc::channel::<WatchEvent>();
let root = watch_root.to_path_buf();
let bridge_thread = std::thread::spawn(move || {
let mut consecutive_errors: usize = 0;
while let Ok(result) = notify_rx.recv() {
match result {
Ok(events) => {
consecutive_errors = 0;
for debounced_event in events {
let path = debounced_event.path;
if let Some(watch_event) = classify_event(&path, &root, &gitignore)
&& event_tx.send(watch_event).is_err()
{
return; }
}
}
Err(err) => {
consecutive_errors += 1;
eprintln!("[watcher] error: {:?}", err);
if consecutive_errors >= 5 {
eprintln!(
"[watcher] warning: {} consecutive errors — watcher may be degraded",
consecutive_errors
);
}
}
}
}
});
Ok((
WatcherHandle {
_debouncer: debouncer,
_bridge_thread: bridge_thread,
},
event_rx,
))
}
fn classify_event(path: &Path, _project_root: &Path, gitignore: &Gitignore) -> Option<WatchEvent> {
if path.components().any(|c| c.as_os_str() == "node_modules") {
return None;
}
if path.components().any(|c| c.as_os_str() == ".code-graph") {
return None;
}
let is_dir = path.is_dir();
if gitignore.matched(path, is_dir).is_ignore() {
return None;
}
if let Some(file_name) = path.file_name().and_then(|n| n.to_str())
&& FULL_REINDEX_FILES.contains(&file_name)
{
if CRATE_ROOT_FILES.contains(&file_name) {
return Some(WatchEvent::CrateRootChanged(path.to_path_buf()));
} else {
return Some(WatchEvent::ConfigChanged);
}
}
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
if !SOURCE_EXTENSIONS.contains(&ext) {
return None;
}
if path.exists() {
Some(WatchEvent::Modified(path.to_path_buf()))
} else {
Some(WatchEvent::Deleted(path.to_path_buf()))
}
}