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        // Skip hidden files and temp files
210        if let Some(name) = path.file_name() {
211            let name_str = name.to_string_lossy();
212            if name_str.starts_with('.') || name_str.contains('~') || name_str.ends_with(".tmp") {
213                return None;
214            }
215        }
216
217        Some(FileChange {
218            path,
219            kind,
220            source: ChangeSource::FileSystem,
221            timestamp: std::time::SystemTime::now(),
222            content: None,
223            patterns: None,
224        })
225    }
226}
227
228/// Dual Watcher - combines LSP and File System watchers
229pub struct DualWatcher {
230    lsp_watcher: Arc<LspWatcher>,
231    file_watcher: Arc<RwLock<FileWatcher>>,
232    change_rx: broadcast::Receiver<FileChange>,
233}
234
235impl DualWatcher {
236    /// Create a new dual watcher
237    pub fn new() -> Result<Self> {
238        let (lsp_watcher, lsp_rx) = LspWatcher::new();
239        let (file_watcher, fs_rx) = FileWatcher::new()?;
240
241        // Create unified change channel
242        let (change_tx, change_rx) = broadcast::channel(1000);
243
244        // Spawn task to merge LSP and FS events
245        let tx1 = change_tx.clone();
246        tokio::spawn(async move {
247            let mut lsp_rx = lsp_rx;
248            while let Ok(change) = lsp_rx.recv().await {
249                let _ = tx1.send(change);
250            }
251        });
252
253        let tx2 = change_tx.clone();
254        tokio::spawn(async move {
255            let mut fs_rx = fs_rx;
256            while let Ok(change) = fs_rx.recv().await {
257                let _ = tx2.send(change);
258            }
259        });
260
261        Ok(Self {
262            lsp_watcher: Arc::new(lsp_watcher),
263            file_watcher: Arc::new(RwLock::new(file_watcher)),
264            change_rx,
265        })
266    }
267
268    /// Start both watchers
269    pub async fn start(&mut self, path: impl AsRef<Path>) -> Result<()> {
270        // Start LSP watcher
271        self.lsp_watcher.start().await?;
272
273        // Start file system watcher
274        self.file_watcher.write().await.watch(path)?;
275
276        println!("🔄 Dual Watcher active: LSP + FileSystem");
277        Ok(())
278    }
279
280    /// Stop both watchers
281    pub async fn stop(&mut self) -> Result<()> {
282        self.lsp_watcher.stop().await?;
283        self.file_watcher.write().await.stop()?;
284        println!("🔄 Dual Watcher stopped");
285        Ok(())
286    }
287
288    /// Get the change receiver
289    pub fn receiver(&self) -> broadcast::Receiver<FileChange> {
290        self.change_rx.resubscribe()
291    }
292
293    /// Wait for next change
294    pub async fn next_change(&mut self) -> Result<FileChange> {
295        self.change_rx
296            .recv()
297            .await
298            .map_err(|e| anyhow::anyhow!("Failed to receive change: {}", e))
299    }
300
301    /// Analyze file changes for DX patterns
302    pub async fn analyze_patterns(&self, mut change: FileChange) -> Result<FileChange> {
303        // If content is available and patterns not yet detected
304        if change.patterns.is_none() {
305            if let Some(content) = &change.content {
306                let detector = crate::patterns::PatternDetector::new()?;
307                change.patterns = detector.detect_in_file(&change.path, content).ok();
308            } else if change.path.exists() {
309                // Read file if it exists
310                if let Ok(content) = tokio::fs::read_to_string(&change.path).await {
311                    let detector = crate::patterns::PatternDetector::new()?;
312                    change.patterns = detector.detect_in_file(&change.path, &content).ok();
313                }
314            }
315        }
316
317        Ok(change)
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324    use tempfile::TempDir;
325    use tokio::fs;
326
327    #[tokio::test]
328    async fn test_file_watcher_detects_changes() {
329        let temp_dir = TempDir::new().unwrap();
330        let test_file = temp_dir.path().join("test.txt");
331
332        let (mut watcher, mut rx) = FileWatcher::new().unwrap();
333        watcher.watch(temp_dir.path()).unwrap();
334
335        // Give watcher time to start
336        tokio::time::sleep(Duration::from_millis(100)).await;
337
338        // Create a file
339        fs::write(&test_file, "test content").await.unwrap();
340
341        // Wait for event
342        tokio::time::sleep(Duration::from_millis(200)).await;
343
344        // Check if we received an event
345        if let Ok(change) = rx.try_recv() {
346            assert_eq!(change.source, ChangeSource::FileSystem);
347            assert!(matches!(
348                change.kind,
349                ChangeKind::Created | ChangeKind::Modified
350            ));
351        }
352
353        watcher.stop().unwrap();
354    }
355
356    #[tokio::test]
357    async fn test_dual_watcher_creation() {
358        let watcher = DualWatcher::new();
359        assert!(watcher.is_ok());
360    }
361}