1use anyhow::{Context as _, Result};
11use notify::{EventKind, RecommendedWatcher, RecursiveMode};
12use notify_debouncer_full::{
13 new_debouncer, DebounceEventResult, DebouncedEvent, Debouncer, FileIdMap,
14};
15use std::path::{Path, PathBuf};
16use std::sync::mpsc::{channel, Receiver, Sender};
17use std::sync::Arc;
18use std::time::Duration;
19use tokio::sync::{broadcast, RwLock};
20
21#[derive(Debug, Clone)]
23pub struct FileChange {
24 pub path: PathBuf,
26
27 pub kind: ChangeKind,
29
30 pub source: ChangeSource,
32
33 pub timestamp: std::time::SystemTime,
35
36 pub content: Option<String>,
38
39 pub patterns: Option<Vec<crate::patterns::PatternMatch>>,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ChangeKind {
46 Created,
47 Modified,
48 Deleted,
49 Renamed,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ChangeSource {
55 Lsp,
56 FileSystem,
57}
58
59#[derive(Debug, Clone)]
61pub struct LspEvent {
62 pub uri: String,
63 pub version: i32,
64 pub content: String,
65}
66
67pub struct LspWatcher {
69 #[allow(dead_code)]
70 lsp_rx: Receiver<LspEvent>,
71 change_tx: broadcast::Sender<FileChange>,
72 running: Arc<RwLock<bool>>,
73}
74
75impl LspWatcher {
76 pub fn new() -> (Self, broadcast::Receiver<FileChange>) {
78 let (_lsp_tx, lsp_rx) = channel();
79 let (change_tx, change_rx) = broadcast::channel(1000);
80
81 (
82 Self {
83 lsp_rx,
84 change_tx,
85 running: Arc::new(RwLock::new(false)),
86 },
87 change_rx,
88 )
89 }
90
91 pub async fn start(&self) -> Result<()> {
93 *self.running.write().await = true;
94
95 println!("📡 LSP Watcher started (mock mode - needs LSP server integration)");
102
103 Ok(())
104 }
105
106 pub async fn stop(&self) -> Result<()> {
108 *self.running.write().await = false;
109 println!("📡 LSP Watcher stopped");
110 Ok(())
111 }
112
113 #[allow(dead_code)]
115 fn process_lsp_event(&self, event: LspEvent) -> Result<()> {
116 let path = PathBuf::from(event.uri.trim_start_matches("file://"));
117
118 let patterns = if let Ok(detector) = crate::patterns::PatternDetector::new() {
120 detector.detect_in_file(&path, &event.content).ok()
121 } else {
122 None
123 };
124
125 let change = FileChange {
126 path,
127 kind: ChangeKind::Modified,
128 source: ChangeSource::Lsp,
129 timestamp: std::time::SystemTime::now(),
130 content: Some(event.content),
131 patterns,
132 };
133
134 let _ = self.change_tx.send(change);
135 Ok(())
136 }
137}
138
139pub struct FileWatcher {
141 debouncer: Option<Debouncer<RecommendedWatcher, FileIdMap>>,
142 _event_tx: Sender<DebounceEventResult>,
143}
144
145impl FileWatcher {
146 pub fn new() -> Result<(Self, broadcast::Receiver<FileChange>)> {
148 let (event_tx, _event_rx) = channel();
149 let (change_tx, change_rx) = broadcast::channel(1000);
150
151 let tx_clone = change_tx.clone();
152
153 let debouncer = new_debouncer(
155 Duration::from_millis(100),
156 None,
157 move |result: DebounceEventResult| {
158 if let Ok(events) = result {
159 for debounced_event in events {
160 if let Some(change) = Self::debounced_event_to_change(debounced_event) {
161 let _ = tx_clone.send(change);
162 }
163 }
164 }
165 },
166 )?;
167
168 Ok((
169 Self {
170 debouncer: Some(debouncer),
171 _event_tx: event_tx,
172 },
173 change_rx,
174 ))
175 }
176
177 pub fn watch(&mut self, path: impl AsRef<Path>) -> Result<()> {
179 if let Some(debouncer) = &mut self.debouncer {
180 debouncer
181 .watch(path.as_ref(), RecursiveMode::Recursive)
182 .with_context(|| format!("Failed to watch: {}", path.as_ref().display()))?;
183
184 println!("👁️ File Watcher started: {}", path.as_ref().display());
185 }
186 Ok(())
187 }
188
189 pub fn stop(&mut self) -> Result<()> {
191 self.debouncer = None;
192 println!("👁️ File Watcher stopped");
193 Ok(())
194 }
195
196 fn debounced_event_to_change(debounced_event: DebouncedEvent) -> Option<FileChange> {
198 let event = &debounced_event.event;
199 let kind = match event.kind {
200 EventKind::Create(_) => ChangeKind::Created,
201 EventKind::Modify(_) => ChangeKind::Modified,
202 EventKind::Remove(_) => ChangeKind::Deleted,
203 _ => return None,
204 };
205
206 let path = event.paths.first()?.clone();
208
209 if !Self::should_process_path(&path) {
211 return None;
212 }
213
214 Some(FileChange {
215 path,
216 kind,
217 source: ChangeSource::FileSystem,
218 timestamp: std::time::SystemTime::now(),
219 content: None,
220 patterns: None,
221 })
222 }
223
224 fn should_process_path(path: &Path) -> bool {
226 if let Some(name) = path.file_name() {
228 let name_str = name.to_string_lossy();
229
230 if name_str.starts_with('.') {
232 return false;
233 }
234
235 if name_str.contains('~') || name_str.ends_with(".tmp") || name_str.ends_with(".swp") {
237 return false;
238 }
239
240 if name_str.ends_with(".lock") {
242 return false;
243 }
244 }
245
246 if let Some(path_str) = path.to_str() {
248 if path_str.contains("/target/")
249 || path_str.contains("\\target\\")
250 || path_str.contains("/node_modules/")
251 || path_str.contains("\\node_modules\\")
252 || path_str.contains("/.dx/")
253 || path_str.contains("\\.dx\\")
254 {
255 return false;
256 }
257 }
258
259 true
260 }
261}
262
263pub struct DualWatcher {
265 lsp_watcher: Arc<LspWatcher>,
266 file_watcher: Arc<RwLock<FileWatcher>>,
267 change_rx: broadcast::Receiver<FileChange>,
268}
269
270impl DualWatcher {
271 pub fn new() -> Result<Self> {
273 let (lsp_watcher, lsp_rx) = LspWatcher::new();
274 let (file_watcher, fs_rx) = FileWatcher::new()?;
275
276 let (change_tx, change_rx) = broadcast::channel(1000);
278
279 let tx1 = change_tx.clone();
281 tokio::spawn(async move {
282 let mut lsp_rx = lsp_rx;
283 while let Ok(change) = lsp_rx.recv().await {
284 let _ = tx1.send(change);
285 }
286 });
287
288 let tx2 = change_tx.clone();
289 tokio::spawn(async move {
290 let mut fs_rx = fs_rx;
291 while let Ok(change) = fs_rx.recv().await {
292 let _ = tx2.send(change);
293 }
294 });
295
296 Ok(Self {
297 lsp_watcher: Arc::new(lsp_watcher),
298 file_watcher: Arc::new(RwLock::new(file_watcher)),
299 change_rx,
300 })
301 }
302
303 pub async fn start(&mut self, path: impl AsRef<Path>) -> Result<()> {
305 self.lsp_watcher.start().await?;
307
308 self.file_watcher.write().await.watch(path)?;
310
311 println!("🔄 Dual Watcher active: LSP + FileSystem");
312 Ok(())
313 }
314
315 pub async fn stop(&mut self) -> Result<()> {
317 self.lsp_watcher.stop().await?;
318 self.file_watcher.write().await.stop()?;
319 println!("🔄 Dual Watcher stopped");
320 Ok(())
321 }
322
323 pub fn receiver(&self) -> broadcast::Receiver<FileChange> {
325 self.change_rx.resubscribe()
326 }
327
328 pub async fn next_change(&mut self) -> Result<FileChange> {
330 self.change_rx
331 .recv()
332 .await
333 .map_err(|e| anyhow::anyhow!("Failed to receive change: {}", e))
334 }
335
336 pub async fn analyze_patterns(&self, mut change: FileChange) -> Result<FileChange> {
338 if change.patterns.is_none() {
340 if let Some(content) = &change.content {
341 let detector = crate::patterns::PatternDetector::new()?;
342 change.patterns = detector.detect_in_file(&change.path, content).ok();
343 } else if change.path.exists() {
344 if let Ok(content) = tokio::fs::read_to_string(&change.path).await {
346 let detector = crate::patterns::PatternDetector::new()?;
347 change.patterns = detector.detect_in_file(&change.path, &content).ok();
348 }
349 }
350 }
351
352 Ok(change)
353 }
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use tempfile::TempDir;
360 use tokio::fs;
361
362 #[tokio::test]
363 async fn test_file_watcher_detects_changes() {
364 let temp_dir = TempDir::new().unwrap();
365 let test_file = temp_dir.path().join("test.txt");
366
367 let (mut watcher, mut rx) = FileWatcher::new().unwrap();
368 watcher.watch(temp_dir.path()).unwrap();
369
370 tokio::time::sleep(Duration::from_millis(100)).await;
372
373 fs::write(&test_file, "test content").await.unwrap();
375
376 tokio::time::sleep(Duration::from_millis(200)).await;
378
379 if let Ok(change) = rx.try_recv() {
381 assert_eq!(change.source, ChangeSource::FileSystem);
382 assert!(matches!(
383 change.kind,
384 ChangeKind::Created | ChangeKind::Modified
385 ));
386 }
387
388 watcher.stop().unwrap();
389 }
390
391 #[tokio::test]
392 async fn test_dual_watcher_creation() {
393 let watcher = DualWatcher::new();
394 assert!(watcher.is_ok());
395 }
396}