kotoba_build/
watcher.rs

1//! ファイル監視モジュール
2
3use 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
11/// ファイル監視エンジン
12pub 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    /// 新しいファイル監視エンジンを作成
22    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    /// 監視するパスを追加
41    pub fn add_watch_path(&mut self, path: PathBuf) {
42        self.watch_paths.push(path);
43    }
44
45    /// 監視するパスを設定
46    pub fn set_watch_paths(&mut self, paths: Vec<PathBuf>) {
47        self.watch_paths = paths;
48    }
49
50    /// 無視パターンを追加
51    pub fn add_ignore_pattern(&mut self, pattern: String) {
52        self.ignore_patterns.push(pattern);
53    }
54
55    /// デバウンス時間を設定
56    pub fn set_debounce_duration(&mut self, duration: Duration) {
57        self.debounce_duration = duration;
58    }
59
60    /// ファイル監視を開始
61    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        // パスを監視対象に追加
72        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        // 初期ビルドを実行
83        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        // ファイル変更を監視
93        self.watch_file_changes(rx).await?;
94
95        Ok(())
96    }
97
98    /// ファイル変更を監視
99    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    /// ファイルイベントを処理
121    async fn handle_file_event(&self, event: Event) -> Result<()> {
122        // 変更されたファイルをフィルタリング
123        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        // デバウンス処理
137        self.debounced_build().await?;
138
139        Ok(())
140    }
141
142    /// デバウンス付きビルドを実行
143    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                // デバウンス期間内なのでスキップ
151                return Ok(());
152            }
153        }
154
155        *last_build_time = Some(now);
156
157        // ビルドを実行
158        drop(last_build_time); // ロックを解放
159        self.run_build().await?;
160
161        Ok(())
162    }
163
164    /// ビルドを実行
165    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                // エラーが発生しても監視は継続
178            }
179        }
180
181        Ok(())
182    }
183
184    /// パスを無視すべきかどうかを判定
185    fn should_ignore_path(&self, path: &std::path::Path) -> bool {
186        let path_str = path.to_string_lossy();
187
188        // 無視パターンにマッチするかチェック
189        for pattern in &self.ignore_patterns {
190            if path_str.contains(pattern) {
191                return true;
192            }
193
194            // ディレクトリ名でのマッチ
195            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        // 隠しファイルや一時ファイルを無視
203        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    /// 監視対象のファイルタイプを判定
213    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                // ソースコードファイル
217                "rs" | "js" | "ts" | "jsx" | "tsx" | "vue" | "svelte" => true,
218                // 設定ファイル
219                "toml" | "json" | "yaml" | "yml" => true,
220                // テンプレートファイル
221                "html" | "css" | "scss" | "sass" | "less" => true,
222                // マークアップファイル
223                "md" | "txt" => true,
224                _ => false,
225            }
226        } else {
227            false
228        }
229    }
230}
231
232/// ウォッチオプション
233#[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/// ウォッチ統計情報
260#[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
291/// 高度なファイル監視機能
292pub struct AdvancedWatcher {
293    watcher: FileWatcher,
294    stats: Arc<RwLock<WatchStats>>,
295    start_time: Instant,
296}
297
298impl AdvancedWatcher {
299    /// 新しい高度な監視エンジンを作成
300    pub fn new(engine: Arc<RwLock<BuildEngine>>, options: WatchOptions) -> Self {
301        let mut watcher = FileWatcher::new(Arc::clone(&engine));
302
303        // オプションを適用
304        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    /// 監視を開始
316    pub async fn start(&self) -> Result<()> {
317        println!("🚀 Starting advanced file watcher...");
318        println!("📊 Statistics tracking enabled");
319
320        // 統計情報を初期化
321        {
322            let mut stats = self.stats.write().await;
323            *stats = WatchStats::new();
324            stats.files_watched = self.watcher.watch_paths.len();
325        }
326
327        // 定期的な統計表示を開始
328        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    /// 統計情報を取得
345    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    /// 統計情報を表示
352    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
369/// ファイル変更イベントのフィルタリング
370pub fn filter_file_events(events: &[notify::Event]) -> Vec<notify::Event> {
371    events.iter().filter(|event| {
372        // 作成、変更、削除イベントのみを対象
373        matches!(event.kind, notify::EventKind::Create(_) |
374                          notify::EventKind::Modify(_) |
375                          notify::EventKind::Remove(_))
376    }).cloned().collect()
377}
378
379/// ファイル変更のバッチ処理
380pub 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}