use anyhow::{Context as _, Result};
use notify::{EventKind, RecommendedWatcher, RecursiveMode};
use notify_debouncer_full::{
new_debouncer, DebounceEventResult, DebouncedEvent, Debouncer, FileIdMap,
};
use std::path::{Path, PathBuf};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{broadcast, RwLock};
#[derive(Debug, Clone)]
pub struct FileChange {
pub path: PathBuf,
pub kind: ChangeKind,
pub source: ChangeSource,
pub timestamp: std::time::SystemTime,
pub content: Option<String>,
pub patterns: Option<Vec<crate::patterns::PatternMatch>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeKind {
Created,
Modified,
Deleted,
Renamed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChangeSource {
Lsp,
FileSystem,
}
#[derive(Debug, Clone)]
pub struct LspEvent {
pub uri: String,
pub version: i32,
pub content: String,
}
pub struct LspWatcher {
#[allow(dead_code)]
lsp_rx: Receiver<LspEvent>,
change_tx: broadcast::Sender<FileChange>,
running: Arc<RwLock<bool>>,
}
impl LspWatcher {
pub fn new() -> (Self, broadcast::Receiver<FileChange>) {
let (_lsp_tx, lsp_rx) = channel();
let (change_tx, change_rx) = broadcast::channel(1000);
(
Self {
lsp_rx,
change_tx,
running: Arc::new(RwLock::new(false)),
},
change_rx,
)
}
pub async fn start(&self) -> Result<()> {
*self.running.write().await = true;
println!("📡 LSP Watcher started (mock mode - needs LSP server integration)");
Ok(())
}
pub async fn stop(&self) -> Result<()> {
*self.running.write().await = false;
println!("📡 LSP Watcher stopped");
Ok(())
}
#[allow(dead_code)]
fn process_lsp_event(&self, event: LspEvent) -> Result<()> {
let path = PathBuf::from(event.uri.trim_start_matches("file://"));
let patterns = if let Ok(detector) = crate::patterns::PatternDetector::new() {
detector.detect_in_file(&path, &event.content).ok()
} else {
None
};
let change = FileChange {
path,
kind: ChangeKind::Modified,
source: ChangeSource::Lsp,
timestamp: std::time::SystemTime::now(),
content: Some(event.content),
patterns,
};
let _ = self.change_tx.send(change);
Ok(())
}
}
pub struct FileWatcher {
debouncer: Option<Debouncer<RecommendedWatcher, FileIdMap>>,
_event_tx: Sender<DebounceEventResult>,
}
impl FileWatcher {
pub fn new() -> Result<(Self, broadcast::Receiver<FileChange>)> {
let (event_tx, _event_rx) = channel();
let (change_tx, change_rx) = broadcast::channel(1000);
let tx_clone = change_tx.clone();
let debouncer = new_debouncer(
Duration::from_millis(100),
None,
move |result: DebounceEventResult| {
if let Ok(events) = result {
for debounced_event in events {
if let Some(change) = Self::debounced_event_to_change(debounced_event) {
let _ = tx_clone.send(change);
}
}
}
},
)?;
Ok((
Self {
debouncer: Some(debouncer),
_event_tx: event_tx,
},
change_rx,
))
}
pub fn watch(&mut self, path: impl AsRef<Path>) -> Result<()> {
if let Some(debouncer) = &mut self.debouncer {
debouncer
.watch(path.as_ref(), RecursiveMode::Recursive)
.with_context(|| format!("Failed to watch: {}", path.as_ref().display()))?;
println!("👁️ File Watcher started: {}", path.as_ref().display());
}
Ok(())
}
pub fn stop(&mut self) -> Result<()> {
self.debouncer = None;
println!("👁️ File Watcher stopped");
Ok(())
}
fn debounced_event_to_change(debounced_event: DebouncedEvent) -> Option<FileChange> {
let event = &debounced_event.event;
let kind = match event.kind {
EventKind::Create(_) => ChangeKind::Created,
EventKind::Modify(_) => ChangeKind::Modified,
EventKind::Remove(_) => ChangeKind::Deleted,
_ => return None,
};
let path = event.paths.first()?.clone();
if !Self::should_process_path(&path) {
return None;
}
Some(FileChange {
path,
kind,
source: ChangeSource::FileSystem,
timestamp: std::time::SystemTime::now(),
content: None,
patterns: None,
})
}
fn should_process_path(path: &Path) -> bool {
if let Some(name) = path.file_name() {
let name_str = name.to_string_lossy();
if name_str.starts_with('.') {
return false;
}
if name_str.contains('~') || name_str.ends_with(".tmp") || name_str.ends_with(".swp") {
return false;
}
if name_str.ends_with(".lock") {
return false;
}
}
if let Some(path_str) = path.to_str() {
if path_str.contains("/target/")
|| path_str.contains("\\target\\")
|| path_str.contains("/node_modules/")
|| path_str.contains("\\node_modules\\")
|| path_str.contains("/.dx/")
|| path_str.contains("\\.dx\\")
{
return false;
}
}
true
}
}
pub struct DualWatcher {
lsp_watcher: Arc<LspWatcher>,
file_watcher: Arc<RwLock<FileWatcher>>,
change_rx: broadcast::Receiver<FileChange>,
}
impl DualWatcher {
pub fn new() -> Result<Self> {
let (lsp_watcher, lsp_rx) = LspWatcher::new();
let (file_watcher, fs_rx) = FileWatcher::new()?;
let (change_tx, change_rx) = broadcast::channel(1000);
let tx1 = change_tx.clone();
tokio::spawn(async move {
let mut lsp_rx = lsp_rx;
while let Ok(change) = lsp_rx.recv().await {
let _ = tx1.send(change);
}
});
let tx2 = change_tx.clone();
tokio::spawn(async move {
let mut fs_rx = fs_rx;
while let Ok(change) = fs_rx.recv().await {
let _ = tx2.send(change);
}
});
Ok(Self {
lsp_watcher: Arc::new(lsp_watcher),
file_watcher: Arc::new(RwLock::new(file_watcher)),
change_rx,
})
}
pub async fn start(&mut self, path: impl AsRef<Path>) -> Result<()> {
self.lsp_watcher.start().await?;
self.file_watcher.write().await.watch(path)?;
println!("🔄 Dual Watcher active: LSP + FileSystem");
Ok(())
}
pub async fn stop(&mut self) -> Result<()> {
self.lsp_watcher.stop().await?;
self.file_watcher.write().await.stop()?;
println!("🔄 Dual Watcher stopped");
Ok(())
}
pub fn receiver(&self) -> broadcast::Receiver<FileChange> {
self.change_rx.resubscribe()
}
pub async fn next_change(&mut self) -> Result<FileChange> {
self.change_rx
.recv()
.await
.map_err(|e| anyhow::anyhow!("Failed to receive change: {}", e))
}
pub async fn analyze_patterns(&self, mut change: FileChange) -> Result<FileChange> {
if change.patterns.is_none() {
if let Some(content) = &change.content {
let detector = crate::patterns::PatternDetector::new()?;
change.patterns = detector.detect_in_file(&change.path, content).ok();
} else if change.path.exists() {
if let Ok(content) = tokio::fs::read_to_string(&change.path).await {
let detector = crate::patterns::PatternDetector::new()?;
change.patterns = detector.detect_in_file(&change.path, &content).ok();
}
}
}
Ok(change)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
use tokio::fs;
#[tokio::test]
async fn test_file_watcher_detects_changes() {
let temp_dir = TempDir::new().unwrap();
let test_file = temp_dir.path().join("test.txt");
let (mut watcher, mut rx) = FileWatcher::new().unwrap();
watcher.watch(temp_dir.path()).unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
fs::write(&test_file, "test content").await.unwrap();
tokio::time::sleep(Duration::from_millis(200)).await;
if let Ok(change) = rx.try_recv() {
assert_eq!(change.source, ChangeSource::FileSystem);
assert!(matches!(
change.kind,
ChangeKind::Created | ChangeKind::Modified
));
}
watcher.stop().unwrap();
}
#[tokio::test]
async fn test_dual_watcher_creation() {
let watcher = DualWatcher::new();
assert!(watcher.is_ok());
}
}