ggen_core/codegen/
watch_mode.rs

1use crate::codegen::{SyncExecutor, SyncOptions};
2use crate::manifest::ManifestParser;
3use ggen_utils::error::{Error, Result};
4use std::path::{Path, PathBuf};
5use std::sync::Arc;
6use std::time::Duration;
7use tokio::sync::RwLock;
8use tokio::time::sleep;
9
10pub struct WatchConfig {
11    pub debounce_ms: u64,
12    pub check_interval_ms: u64,
13    pub max_retries: usize,
14}
15
16impl Default for WatchConfig {
17    fn default() -> Self {
18        Self {
19            debounce_ms: 500,
20            check_interval_ms: 1000,
21            max_retries: 3,
22        }
23    }
24}
25
26pub struct WatchMode {
27    options: SyncOptions,
28    config: WatchConfig,
29    watched_paths: Arc<RwLock<Vec<PathBuf>>>,
30}
31
32impl WatchMode {
33    pub fn new(options: SyncOptions, config: WatchConfig) -> Self {
34        let watched_paths = Arc::new(RwLock::new(vec![options.manifest_path.clone()]));
35
36        Self {
37            options,
38            config,
39            watched_paths,
40        }
41    }
42
43    pub async fn start(&mut self) -> Result<()> {
44        let base_path = self
45            .options
46            .manifest_path
47            .parent()
48            .unwrap_or(Path::new("."));
49
50        // Load initial manifest
51        let mut manifest_data = ManifestParser::parse(&self.options.manifest_path)
52            .map_err(|e| Error::new(&format!("Failed to parse manifest: {}", e)))?;
53
54        // Add ontology to watched paths
55        let ontology_path = base_path.join(&manifest_data.ontology.source);
56        self.watched_paths.write().await.push(ontology_path);
57
58        eprintln!("Watch mode started. Press Ctrl+C to exit.");
59        eprintln!(
60            "Watching {} files for changes...",
61            self.watched_paths.read().await.len()
62        );
63
64        // Store initial file hashes
65        let mut file_hashes = self.compute_file_hashes().await?;
66
67        loop {
68            sleep(Duration::from_millis(self.config.check_interval_ms)).await;
69
70            // Compute current hashes
71            let current_hashes = match self.compute_file_hashes().await {
72                Ok(h) => h,
73                Err(_) => continue,
74            };
75
76            // Check for changes
77            let mut changed = false;
78            for (path, new_hash) in &current_hashes {
79                if let Some(old_hash) = file_hashes.get(path) {
80                    if old_hash != new_hash {
81                        eprintln!("Changed: {}", path.display());
82                        changed = true;
83                    }
84                }
85            }
86
87            // Check for new files
88            if current_hashes.len() != file_hashes.len() {
89                changed = true;
90            }
91
92            if changed {
93                eprintln!("Debouncing changes for {}ms...", self.config.debounce_ms);
94                sleep(Duration::from_millis(self.config.debounce_ms)).await;
95
96                // Reload manifest to detect new files
97                if let Ok(new_manifest) = ManifestParser::parse(&self.options.manifest_path) {
98                    manifest_data = new_manifest;
99
100                    // Update watched paths
101                    self.watched_paths.write().await.clear();
102                    self.watched_paths
103                        .write()
104                        .await
105                        .push(self.options.manifest_path.clone());
106
107                    let ontology_path = base_path.join(&manifest_data.ontology.source);
108                    self.watched_paths.write().await.push(ontology_path);
109                }
110
111                // Trigger sync
112                eprintln!("Triggering sync...");
113                let mut retry_count = 0;
114                loop {
115                    let executor = SyncExecutor::new(self.options.clone());
116                    match executor.execute() {
117                        Ok(result) => {
118                            eprintln!(
119                                "✓ Sync complete: {} files in {}ms",
120                                result.files_synced, result.duration_ms
121                            );
122                            break;
123                        }
124                        Err(e) => {
125                            retry_count += 1;
126                            if retry_count >= self.config.max_retries {
127                                eprintln!(
128                                    "✗ Sync failed after {} retries: {}",
129                                    self.config.max_retries, e
130                                );
131                                break;
132                            }
133                            eprintln!(
134                                "⚠ Sync failed (retry {}/{}): {}",
135                                retry_count, self.config.max_retries, e
136                            );
137                            sleep(Duration::from_millis(self.config.debounce_ms)).await;
138                        }
139                    }
140                }
141
142                file_hashes = self.compute_file_hashes().await?;
143                eprintln!("Watching for more changes...");
144            }
145        }
146    }
147
148    async fn compute_file_hashes(&self) -> Result<std::collections::HashMap<PathBuf, String>> {
149        let mut hashes = std::collections::HashMap::new();
150
151        for path in self.watched_paths.read().await.iter() {
152            if path.exists() {
153                match std::fs::read_to_string(path) {
154                    Ok(content) => {
155                        let hash = Self::hash_file(&content);
156                        hashes.insert(path.clone(), hash);
157                    }
158                    Err(_) => {
159                        // Skip files that can't be read
160                    }
161                }
162            }
163        }
164
165        Ok(hashes)
166    }
167
168    fn hash_file(content: &str) -> String {
169        use sha2::{Digest, Sha256};
170        let mut hasher = Sha256::new();
171        hasher.update(content.as_bytes());
172        format!("{:x}", hasher.finalize())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[tokio::test]
181    async fn test_watch_config_defaults() {
182        let config = WatchConfig::default();
183        assert_eq!(config.debounce_ms, 500);
184        assert_eq!(config.check_interval_ms, 1000);
185        assert_eq!(config.max_retries, 3);
186    }
187
188    #[tokio::test]
189    async fn test_watch_mode_creation() {
190        let options = SyncOptions::default();
191        let config = WatchConfig::default();
192        let watch = WatchMode::new(options.clone(), config);
193
194        let paths = watch.watched_paths.read().await;
195        assert!(paths.contains(&options.manifest_path));
196    }
197
198    #[test]
199    fn test_hash_file_consistency() {
200        let content1 = "test content";
201        let content2 = "test content";
202        let content3 = "different content";
203
204        assert_eq!(
205            WatchMode::hash_file(content1),
206            WatchMode::hash_file(content2)
207        );
208        assert_ne!(
209            WatchMode::hash_file(content1),
210            WatchMode::hash_file(content3)
211        );
212    }
213}