dx_forge/
watcher.rs

1//! Dual-Watcher Architecture - LSP + File System monitoring
2//!
3//! Provides two-tier file change detection:
4//! 1. **LSP Watcher** (Primary): Monitors Language Server Protocol events
5//! 2. **File System Watcher** (Fallback): Monitors actual file system changes
6//!
7//! The LSP watcher detects changes before they hit the disk, enabling
8//! faster response times and semantic understanding of code changes.
9
10use 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/// File change event
22#[derive(Debug, Clone)]
23pub struct FileChange {
24    /// Path to the changed file
25    pub path: PathBuf,
26
27    /// Type of change
28    pub kind: ChangeKind,
29
30    /// Source of the event (LSP or FileSystem)
31    pub source: ChangeSource,
32
33    /// Timestamp of the change
34    pub timestamp: std::time::SystemTime,
35
36    /// Optional content if available from LSP
37    pub content: Option<String>,
38
39    /// Detected DX patterns (if analyzed)
40    pub patterns: Option<Vec<crate::patterns::PatternMatch>>,
41}
42
43/// Type of file change
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum ChangeKind {
46    Created,
47    Modified,
48    Deleted,
49    Renamed,
50}
51
52/// Source of the change detection
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ChangeSource {
55    Lsp,
56    FileSystem,
57}
58
59/// LSP event (simplified - full LSP protocol would be more complex)
60#[derive(Debug, Clone)]
61pub struct LspEvent {
62    pub uri: String,
63    pub version: i32,
64    pub content: String,
65}
66
67/// LSP Watcher - monitors Language Server Protocol events
68pub 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    /// Create a new LSP watcher
77    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    /// Start watching for LSP events
92    pub async fn start(&self) -> Result<()> {
93        *self.running.write().await = true;
94
95        // In a real implementation, this would:
96        // 1. Connect to LSP server via stdin/stdout or socket
97        // 2. Subscribe to textDocument/didChange notifications
98        // 3. Parse LSP JSON-RPC messages
99        // 4. Extract file changes and content
100
101        println!("📡 LSP Watcher started (mock mode - needs LSP server integration)");
102
103        Ok(())
104    }
105
106    /// Stop watching
107    pub async fn stop(&self) -> Result<()> {
108        *self.running.write().await = false;
109        println!("📡 LSP Watcher stopped");
110        Ok(())
111    }
112
113    /// Process LSP events (would be called from LSP message loop)
114    #[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        // Detect patterns in content
119        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
139/// File System Watcher - monitors actual file system changes
140pub struct FileWatcher {
141    debouncer: Option<Debouncer<RecommendedWatcher, FileIdMap>>,
142    _event_tx: Sender<DebounceEventResult>,
143}
144
145impl FileWatcher {
146    /// Create a new file system watcher
147    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        // Create debouncer with 100ms delay
154        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    /// Watch a directory recursively
178    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    /// Stop watching
190    pub fn stop(&mut self) -> Result<()> {
191        self.debouncer = None;
192        println!("👁️  File Watcher stopped");
193        Ok(())
194    }
195
196    /// Convert debounced event to FileChange
197    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        // Get first path from event
207        let path = event.paths.first()?.clone();
208
209        // Intelligent filtering for performance
210        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    /// Determine if a path should be processed (performance optimization)
225    fn should_process_path(path: &Path) -> bool {
226        // Skip hidden files and temp files
227        if let Some(name) = path.file_name() {
228            let name_str = name.to_string_lossy();
229            
230            // Skip hidden files
231            if name_str.starts_with('.') {
232                return false;
233            }
234            
235            // Skip temp files
236            if name_str.contains('~') || name_str.ends_with(".tmp") || name_str.ends_with(".swp") {
237                return false;
238            }
239            
240            // Skip lock files
241            if name_str.ends_with(".lock") {
242                return false;
243            }
244        }
245
246        // Skip target directories and node_modules
247        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
263/// Dual Watcher - combines LSP and File System watchers
264pub struct DualWatcher {
265    lsp_watcher: Arc<LspWatcher>,
266    file_watcher: Arc<RwLock<FileWatcher>>,
267    change_rx: broadcast::Receiver<FileChange>,
268}
269
270impl DualWatcher {
271    /// Create a new dual watcher
272    pub fn new() -> Result<Self> {
273        let (lsp_watcher, lsp_rx) = LspWatcher::new();
274        let (file_watcher, fs_rx) = FileWatcher::new()?;
275
276        // Create unified change channel
277        let (change_tx, change_rx) = broadcast::channel(1000);
278
279        // Spawn task to merge LSP and FS events
280        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    /// Start both watchers
304    pub async fn start(&mut self, path: impl AsRef<Path>) -> Result<()> {
305        // Start LSP watcher
306        self.lsp_watcher.start().await?;
307
308        // Start file system watcher
309        self.file_watcher.write().await.watch(path)?;
310
311        println!("🔄 Dual Watcher active: LSP + FileSystem");
312        Ok(())
313    }
314
315    /// Stop both watchers
316    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    /// Get the change receiver
324    pub fn receiver(&self) -> broadcast::Receiver<FileChange> {
325        self.change_rx.resubscribe()
326    }
327
328    /// Wait for next change
329    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    /// Analyze file changes for DX patterns
337    pub async fn analyze_patterns(&self, mut change: FileChange) -> Result<FileChange> {
338        // If content is available and patterns not yet detected
339        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                // Read file if it exists
345                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        // Give watcher time to start
371        tokio::time::sleep(Duration::from_millis(100)).await;
372
373        // Create a file
374        fs::write(&test_file, "test content").await.unwrap();
375
376        // Wait for event
377        tokio::time::sleep(Duration::from_millis(200)).await;
378
379        // Check if we received an event
380        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}