1use super::{BuildEngine, Result, BuildError};
4use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
5use std::collections::HashSet;
6use std::path::PathBuf;
7use std::sync::Arc;
8use std::time::{Duration, Instant};
9use tokio::sync::RwLock;
10
11pub struct FileWatcher {
13 engine: Arc<RwLock<BuildEngine>>,
14 watch_paths: Vec<PathBuf>,
15 ignore_patterns: Vec<String>,
16 debounce_duration: Duration,
17 last_build_time: Arc<RwLock<Option<Instant>>>,
18}
19
20impl FileWatcher {
21 pub fn new(engine: Arc<RwLock<BuildEngine>>) -> Self {
23 Self {
24 engine,
25 watch_paths: vec![],
26 ignore_patterns: vec![
27 ".git".to_string(),
28 "node_modules".to_string(),
29 "target".to_string(),
30 "dist".to_string(),
31 ".DS_Store".to_string(),
32 "*.log".to_string(),
33 "*.tmp".to_string(),
34 ],
35 debounce_duration: Duration::from_millis(500),
36 last_build_time: Arc::new(RwLock::new(None)),
37 }
38 }
39
40 pub fn add_watch_path(&mut self, path: PathBuf) {
42 self.watch_paths.push(path);
43 }
44
45 pub fn set_watch_paths(&mut self, paths: Vec<PathBuf>) {
47 self.watch_paths = paths;
48 }
49
50 pub fn add_ignore_pattern(&mut self, pattern: String) {
52 self.ignore_patterns.push(pattern);
53 }
54
55 pub fn set_debounce_duration(&mut self, duration: Duration) {
57 self.debounce_duration = duration;
58 }
59
60 pub async fn start(&self) -> Result<()> {
62 println!("👀 Starting file watcher...");
63 println!("📁 Watching paths: {:?}", self.watch_paths);
64 println!("🚫 Ignoring patterns: {:?}", self.ignore_patterns);
65 println!("⏱️ Debounce duration: {:?}", self.debounce_duration);
66
67 let (tx, rx) = std::sync::mpsc::channel();
68 let mut watcher = RecommendedWatcher::new(tx, Config::default())
69 .map_err(|e| BuildError::Build(format!("Failed to create watcher: {}", e)))?;
70
71 for path in &self.watch_paths {
73 if path.exists() {
74 watcher.watch(path, RecursiveMode::Recursive)
75 .map_err(|e| BuildError::Build(format!("Failed to watch path {}: {}", path.display(), e)))?;
76 println!("✅ Watching: {}", path.display());
77 } else {
78 println!("⚠️ Path does not exist: {}", path.display());
79 }
80 }
81
82 println!("🏗️ Running initial build...");
84 if let Err(e) = self.run_build().await {
85 println!("❌ Initial build failed: {}", e);
86 }
87
88 println!("🎯 File watcher started successfully!");
89 println!("💡 File changes will trigger automatic rebuilds");
90 println!("🛑 Press Ctrl+C to stop");
91
92 self.watch_file_changes(rx).await?;
94
95 Ok(())
96 }
97
98 async fn watch_file_changes(&self, rx: std::sync::mpsc::Receiver<std::result::Result<Event, notify::Error>>) -> super::Result<()> {
100 loop {
101 match rx.recv() {
102 Ok(Ok(event)) => {
103 if let Err(e) = self.handle_file_event(event).await {
104 println!("❌ Error handling file event: {}", e);
105 }
106 }
107 Ok(Err(e)) => {
108 println!("❌ Watch error: {:?}", e);
109 }
110 Err(e) => {
111 println!("❌ Channel error: {:?}", e);
112 break;
113 }
114 }
115 }
116
117 Ok(())
118 }
119
120 async fn handle_file_event(&self, event: Event) -> Result<()> {
122 let changed_files: Vec<_> = event.paths.into_iter()
124 .filter(|path| !self.should_ignore_path(path))
125 .collect();
126
127 if changed_files.is_empty() {
128 return Ok(());
129 }
130
131 println!("📝 Files changed:");
132 for file in &changed_files {
133 println!(" • {}", file.display());
134 }
135
136 self.debounced_build().await?;
138
139 Ok(())
140 }
141
142 async fn debounced_build(&self) -> Result<()> {
144 let now = Instant::now();
145 let mut last_build_time = self.last_build_time.write().await;
146
147 if let Some(last_time) = *last_build_time {
148 let elapsed = now.duration_since(last_time);
149 if elapsed < self.debounce_duration {
150 return Ok(());
152 }
153 }
154
155 *last_build_time = Some(now);
156
157 drop(last_build_time); self.run_build().await?;
160
161 Ok(())
162 }
163
164 async fn run_build(&self) -> Result<()> {
166 println!("🔄 Rebuilding project...");
167
168 let start_time = Instant::now();
169
170 match self.engine.write().await.build().await {
171 Ok(_) => {
172 let duration = start_time.elapsed();
173 println!("✅ Build completed in {:.2}s", duration.as_secs_f64());
174 }
175 Err(e) => {
176 println!("❌ Build failed: {}", e);
177 }
179 }
180
181 Ok(())
182 }
183
184 fn should_ignore_path(&self, path: &std::path::Path) -> bool {
186 let path_str = path.to_string_lossy();
187
188 for pattern in &self.ignore_patterns {
190 if path_str.contains(pattern) {
191 return true;
192 }
193
194 if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
196 if dir_name == pattern {
197 return true;
198 }
199 }
200 }
201
202 if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
204 if file_name.starts_with('.') || file_name.ends_with('~') {
205 return true;
206 }
207 }
208
209 false
210 }
211
212 fn is_watched_file_type(&self, path: &std::path::Path) -> bool {
214 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
215 match ext {
216 "rs" | "js" | "ts" | "jsx" | "tsx" | "vue" | "svelte" => true,
218 "toml" | "json" | "yaml" | "yml" => true,
220 "html" | "css" | "scss" | "sass" | "less" => true,
222 "md" | "txt" => true,
224 _ => false,
225 }
226 } else {
227 false
228 }
229 }
230}
231
232#[derive(Debug, Clone)]
234pub struct WatchOptions {
235 pub paths: Vec<PathBuf>,
236 pub ignore_patterns: Vec<String>,
237 pub debounce_ms: u64,
238 pub clear_screen: bool,
239 pub verbose: bool,
240}
241
242impl Default for WatchOptions {
243 fn default() -> Self {
244 Self {
245 paths: vec!["src".into(), "kotoba-build.toml".into()],
246 ignore_patterns: vec![
247 ".git".to_string(),
248 "node_modules".to_string(),
249 "target".to_string(),
250 "dist".to_string(),
251 ],
252 debounce_ms: 500,
253 clear_screen: true,
254 verbose: false,
255 }
256 }
257}
258
259#[derive(Debug, Clone)]
261pub struct WatchStats {
262 pub files_watched: usize,
263 pub builds_triggered: usize,
264 pub successful_builds: usize,
265 pub failed_builds: usize,
266 pub total_watch_time: Duration,
267}
268
269impl WatchStats {
270 pub fn new() -> Self {
271 Self {
272 files_watched: 0,
273 builds_triggered: 0,
274 successful_builds: 0,
275 failed_builds: 0,
276 total_watch_time: Duration::default(),
277 }
278 }
279
280 pub fn record_build_success(&mut self) {
281 self.builds_triggered += 1;
282 self.successful_builds += 1;
283 }
284
285 pub fn record_build_failure(&mut self) {
286 self.builds_triggered += 1;
287 self.failed_builds += 1;
288 }
289}
290
291pub struct AdvancedWatcher {
293 watcher: FileWatcher,
294 stats: Arc<RwLock<WatchStats>>,
295 start_time: Instant,
296}
297
298impl AdvancedWatcher {
299 pub fn new(engine: Arc<RwLock<BuildEngine>>, options: WatchOptions) -> Self {
301 let mut watcher = FileWatcher::new(Arc::clone(&engine));
302
303 watcher.set_watch_paths(options.paths);
305 watcher.ignore_patterns = options.ignore_patterns;
306 watcher.set_debounce_duration(Duration::from_millis(options.debounce_ms));
307
308 Self {
309 watcher,
310 stats: Arc::new(RwLock::new(WatchStats::new())),
311 start_time: Instant::now(),
312 }
313 }
314
315 pub async fn start(&self) -> Result<()> {
317 println!("🚀 Starting advanced file watcher...");
318 println!("📊 Statistics tracking enabled");
319
320 {
322 let mut stats = self.stats.write().await;
323 *stats = WatchStats::new();
324 stats.files_watched = self.watcher.watch_paths.len();
325 }
326
327 let stats_clone = Arc::clone(&self.stats);
329 tokio::spawn(async move {
330 let mut interval = tokio::time::interval(Duration::from_secs(30));
331 loop {
332 interval.tick().await;
333 let stats = stats_clone.read().await;
334 println!("📊 Watch Stats: {} builds triggered, {} successful, {} failed",
335 stats.builds_triggered, stats.successful_builds, stats.failed_builds);
336 }
337 });
338
339 self.watcher.start().await?;
340
341 Ok(())
342 }
343
344 pub async fn get_stats(&self) -> WatchStats {
346 let mut stats = self.stats.read().await.clone();
347 stats.total_watch_time = self.start_time.elapsed();
348 stats
349 }
350
351 pub async fn print_stats(&self) {
353 let stats = self.get_stats().await;
354
355 println!("📊 File Watcher Statistics:");
356 println!(" Files watched: {}", stats.files_watched);
357 println!(" Builds triggered: {}", stats.builds_triggered);
358 println!(" Successful builds: {}", stats.successful_builds);
359 println!(" Failed builds: {}", stats.failed_builds);
360 println!(" Total watch time: {:.2}s", stats.total_watch_time.as_secs_f64());
361
362 if stats.builds_triggered > 0 {
363 let success_rate = (stats.successful_builds as f64 / stats.builds_triggered as f64) * 100.0;
364 println!(" Success rate: {:.1}%", success_rate);
365 }
366 }
367}
368
369pub fn filter_file_events(events: &[notify::Event]) -> Vec<notify::Event> {
371 events.iter().filter(|event| {
372 matches!(event.kind, notify::EventKind::Create(_) |
374 notify::EventKind::Modify(_) |
375 notify::EventKind::Remove(_))
376 }).cloned().collect()
377}
378
379pub struct BatchProcessor {
381 events: Vec<notify::Event>,
382 batch_timeout: Duration,
383 last_process_time: Instant,
384}
385
386impl BatchProcessor {
387 pub fn new(batch_timeout: Duration) -> Self {
388 Self {
389 events: vec![],
390 batch_timeout,
391 last_process_time: Instant::now(),
392 }
393 }
394
395 pub fn add_event(&mut self, event: notify::Event) {
396 self.events.push(event);
397 }
398
399 pub fn should_process(&self) -> bool {
400 self.last_process_time.elapsed() >= self.batch_timeout && !self.events.is_empty()
401 }
402
403 pub fn take_events(&mut self) -> Vec<notify::Event> {
404 self.last_process_time = Instant::now();
405 std::mem::take(&mut self.events)
406 }
407
408 pub fn clear(&mut self) {
409 self.events.clear();
410 }
411}