use clap::Args;
use octocode::config::Config;
use octocode::indexer;
use octocode::lock::IndexLock;
use octocode::state;
use octocode::store::Store;
use octocode::watcher_config::{
IgnorePatterns, DEFAULT_ADDITIONAL_DELAY_MS, MAX_ADDITIONAL_DELAY_MS,
WATCH_DEFAULT_DEBOUNCE_SECS, WATCH_MAX_DEBOUNCE_SECS, WATCH_MIN_DEBOUNCE_SECS,
};
use super::index::IndexArgs;
#[derive(Args, Debug)]
pub struct WatchArgs {
#[arg(long, short)]
pub quiet: bool,
#[arg(long, short)]
pub debounce: Option<u64>,
#[arg(long)]
pub additional_delay: Option<u64>,
#[arg(long)]
pub no_git: bool,
}
pub async fn execute(
store: &Store,
config: &Config,
args: &WatchArgs,
) -> Result<(), anyhow::Error> {
let current_dir = std::env::current_dir()?;
let debounce_secs = args
.debounce
.unwrap_or(WATCH_DEFAULT_DEBOUNCE_SECS)
.clamp(WATCH_MIN_DEBOUNCE_SECS, WATCH_MAX_DEBOUNCE_SECS);
let additional_delay_ms = args
.additional_delay
.unwrap_or(DEFAULT_ADDITIONAL_DELAY_MS)
.clamp(0, MAX_ADDITIONAL_DELAY_MS);
if !args.quiet {
println!(
"Starting watch mode for current directory: {}",
current_dir.display()
);
println!(
"Configuration: debounce={}s, additional_delay={}ms",
debounce_secs, additional_delay_ms
);
println!("Initial indexing...");
}
if !args.quiet {
super::index::execute(
store,
config,
&IndexArgs {
no_git: args.no_git,
list_files: false,
show_file: None,
graphrag: None,
},
)
.await?
} else {
let state = state::create_shared_state();
state.write().current_directory = current_dir.clone();
let git_repo_root = if !args.no_git {
indexer::git::find_git_root(¤t_dir)
} else {
None
};
let mut lock = IndexLock::new(¤t_dir)?;
lock.acquire()?;
tracing::debug!("Watch mode: acquired indexing lock for initial scan");
indexer::index_files(store, state.clone(), config, git_repo_root.as_deref()).await?;
lock.release()?;
tracing::debug!("Watch mode: released indexing lock after initial scan");
}
if !args.quiet {
println!("Loaded ignore patterns from .gitignore and .noindex files");
println!("Watching for changes (press Ctrl+C to stop)...");
}
use notify_debouncer_mini::notify::RecursiveMode;
use notify_debouncer_mini::{new_debouncer, DebouncedEvent};
use std::sync::mpsc::channel;
use std::time::Duration;
let (tx, rx) = channel();
let quiet_mode = args.quiet;
let ignore_patterns = IgnorePatterns::new(current_dir.clone());
let mut debouncer = new_debouncer(
Duration::from_secs(debounce_secs),
move |res: Result<Vec<DebouncedEvent>, notify_debouncer_mini::notify::Error>| {
match res {
Ok(events) => {
let relevant_events = events
.iter()
.filter(|event| !ignore_patterns.should_ignore_path(&event.path))
.count();
if relevant_events > 0 {
let _ = tx.send(());
}
}
Err(e) => {
if !quiet_mode {
eprintln!("Error in file watcher: {:?}", e);
}
}
}
},
)?;
debouncer
.watcher()
.watch(¤t_dir, RecursiveMode::Recursive)?;
let state = state::create_shared_state();
state.write().current_directory = current_dir;
let config = config.clone();
loop {
match rx.recv() {
Ok(()) => {
if !args.quiet {
println!("\nDetected file changes, reindexing...");
}
{
let mut state_guard = state.write();
state_guard.indexed_files = 0;
state_guard.indexing_complete = false;
}
if additional_delay_ms > 0 {
tokio::time::sleep(tokio::time::Duration::from_millis(additional_delay_ms))
.await;
}
if !args.quiet {
super::index::execute(
store,
&config,
&IndexArgs {
no_git: args.no_git,
list_files: false,
show_file: None,
graphrag: None,
},
)
.await?
} else {
let current_dir = state.read().current_directory.clone();
let git_repo_root = if !args.no_git {
indexer::git::find_git_root(¤t_dir)
} else {
None
};
let mut lock = IndexLock::new(¤t_dir)?;
lock.acquire()?;
tracing::debug!("Watch mode: acquired indexing lock for re-indexing");
indexer::index_files(store, state.clone(), &config, git_repo_root.as_deref())
.await?;
lock.release()?;
tracing::debug!("Watch mode: released indexing lock after re-indexing");
}
}
Err(e) => {
if !args.quiet {
eprintln!("Watch error: {:?}", e);
}
break;
}
}
}
Ok(())
}