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 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
228pub struct DualWatcher {
230 lsp_watcher: Arc<LspWatcher>,
231 file_watcher: Arc<RwLock<FileWatcher>>,
232 change_rx: broadcast::Receiver<FileChange>,
233}
234
235impl DualWatcher {
236 pub fn new() -> Result<Self> {
238 let (lsp_watcher, lsp_rx) = LspWatcher::new();
239 let (file_watcher, fs_rx) = FileWatcher::new()?;
240
241 let (change_tx, change_rx) = broadcast::channel(1000);
243
244 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 pub async fn start(&mut self, path: impl AsRef<Path>) -> Result<()> {
270 self.lsp_watcher.start().await?;
272
273 self.file_watcher.write().await.watch(path)?;
275
276 println!("🔄 Dual Watcher active: LSP + FileSystem");
277 Ok(())
278 }
279
280 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 pub fn receiver(&self) -> broadcast::Receiver<FileChange> {
290 self.change_rx.resubscribe()
291 }
292
293 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 pub async fn analyze_patterns(&self, mut change: FileChange) -> Result<FileChange> {
303 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 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 tokio::time::sleep(Duration::from_millis(100)).await;
337
338 fs::write(&test_file, "test content").await.unwrap();
340
341 tokio::time::sleep(Duration::from_millis(200)).await;
343
344 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}