use anyhow::Result;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use crate::backend::{Backend, BackendTrait};
use crate::error::LlmError;
use crate::output::{OutputFormat, SearchResponse, SymbolMatch};
use crate::query::SearchOptions;
pub fn run_watch<'a>(
db_path: PathBuf,
options: SearchOptions<'a>,
output_format: OutputFormat,
shutdown: Arc<AtomicBool>,
) -> Result<()> {
let backend = Backend::detect_and_open(&db_path)?;
match backend {
Backend::Sqlite(inner) => {
run_watch_with_filesystem(&inner, db_path, options, output_format, shutdown)
}
#[cfg(feature = "geometric-backend")]
Backend::Geometric(_) => Err(LlmError::SearchFailed {
reason: "Watch command not supported for geometric backend".to_string(),
}
.into()),
}
}
fn run_watch_with_filesystem<'a>(
backend: &dyn BackendTrait,
db_path: PathBuf,
options: SearchOptions<'a>,
output_format: OutputFormat,
shutdown: Arc<AtomicBool>,
) -> Result<()> {
let (response, _partial, _paths_bounded) =
backend
.search_symbols(options.clone())
.map_err(|e| LlmError::SearchFailed {
reason: e.to_string(),
})?;
display_results(&response, &output_format)?;
let mut previous_results = response.results;
let mut last_modified = get_file_modification_time(&db_path)?;
const POLL_INTERVAL_MS: u64 = 1000;
while !shutdown.load(Ordering::Relaxed) {
std::thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
if let Ok(current_modified) = get_file_modification_time(&db_path) {
if current_modified > last_modified {
last_modified = current_modified;
match backend.search_symbols(options.clone()) {
Ok((current_response, _, _)) => {
format_delta(&previous_results, ¤t_response.results, &output_format)?;
previous_results = current_response.results;
}
Err(e) => {
eprintln!("Query failed: {}", e);
}
}
}
}
}
println!("SHUTDOWN");
Ok(())
}
fn get_file_modification_time(path: &Path) -> Result<SystemTime, LlmError> {
path.metadata()
.and_then(|m| m.modified())
.map_err(LlmError::IoError)
}
fn display_results(response: &SearchResponse, output_format: &OutputFormat) -> Result<()> {
match output_format {
OutputFormat::Human => {
println!("Found {} results", response.total_count);
for result in &response.results {
println!(" {}", format_symbol_match(result));
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let json_output = serde_json::to_string_pretty(response)?;
println!("{}", json_output);
}
}
Ok(())
}
fn format_delta(
previous: &[SymbolMatch],
current: &[SymbolMatch],
output_format: &OutputFormat,
) -> Result<()> {
let previous_ids: HashSet<&str> = previous.iter().map(|p| p.match_id.as_str()).collect();
let current_ids: HashSet<&str> = current.iter().map(|c| c.match_id.as_str()).collect();
let added: Vec<&SymbolMatch> = current
.iter()
.filter(|c| !previous_ids.contains(c.match_id.as_str()))
.collect();
let removed: Vec<&SymbolMatch> = previous
.iter()
.filter(|p| !current_ids.contains(p.match_id.as_str()))
.collect();
if added.is_empty() && removed.is_empty() {
return Ok(()); }
match output_format {
OutputFormat::Human => {
println!(
"\n--- Changes: Added: {}, Removed: {} ---",
added.len(),
removed.len()
);
for result in &added {
println!("+ {}", format_symbol_match(result));
}
for result in &removed {
println!("- {}", format_symbol_match(result));
}
}
OutputFormat::Json | OutputFormat::Pretty => {
let notice = format!("Added: {}, Removed: {}", added.len(), removed.len());
println!("\n--- {} ---", notice);
for result in &added {
let json_output = serde_json::to_string_pretty(result)?;
println!("+ {}", json_output);
}
for result in &removed {
let json_output = serde_json::to_string_pretty(result)?;
println!("- {}", json_output);
}
}
}
Ok(())
}
fn format_symbol_match(result: &SymbolMatch) -> String {
let kind = result.kind.as_str();
let name = result.name.as_str();
let path = result.span.file_path.as_str();
format!("[{}:{}] {} ({})", path, result.span.start_line, name, kind)
}