1use crate::cache::{CacheEntry, CacheStats, ScanCache};
7use crate::class_extractor::{ClassContext, ClassExtractor, ExtractedClass};
8use crate::content_config::{ContentConfig, FilePattern, ScanConfig};
9use crate::error::{Result, ScannerError};
10use crate::parallel_processor::{ParallelProcessor, ProcessingStats};
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15use tokio::sync::RwLock;
16
17#[derive(Debug)]
19pub struct ContentScanner {
20 config: ScanConfig,
21 file_scanner: FileScanner,
22 class_extractor: ClassExtractor,
23 parallel_processor: ParallelProcessor,
24 cache: Arc<RwLock<ScanCache>>,
25}
26
27#[derive(Debug)]
29pub struct FileScanner {
30 config: ContentConfig,
31 file_types: HashMap<String, FileType>,
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct FileInfo {
37 pub path: PathBuf,
38 pub file_type: FileType,
39 pub size: u64,
40 pub modified: std::time::SystemTime,
41 pub content: Option<String>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
46pub enum FileType {
47 Rust,
48 JavaScript,
49 TypeScript,
50 Html,
51 Vue,
52 Svelte,
53 Css,
54 Scss,
55 Less,
56 Json,
57 Yaml,
58 Toml,
59 Markdown,
60 Other(String),
61}
62
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct ClassSet {
66 pub classes: HashSet<String>,
67 pub file_classes: HashMap<PathBuf, Vec<ExtractedClass>>,
68 pub total_files: usize,
69 pub total_classes: usize,
70 pub unique_classes: usize,
71 pub processing_stats: ProcessingStats,
72}
73
74impl ClassSet {
75 pub fn new() -> Self {
77 Self {
78 classes: HashSet::new(),
79 file_classes: HashMap::new(),
80 total_files: 0,
81 total_classes: 0,
82 unique_classes: 0,
83 processing_stats: ProcessingStats::default(),
84 }
85 }
86
87 pub fn add_file_classes(&mut self, file_path: PathBuf, classes: Vec<ExtractedClass>) {
89 let unique_classes: HashSet<String> =
90 classes.iter().map(|c| c.class_name.clone()).collect();
91
92 self.classes.extend(unique_classes);
93 self.file_classes.insert(file_path, classes);
94 self.total_files += 1;
95 self.unique_classes = self.classes.len();
96 }
97
98 pub fn total_classes(&self) -> usize {
100 self.total_classes
101 }
102
103 pub fn unique_classes(&self) -> usize {
105 self.unique_classes
106 }
107
108 pub fn get_file_classes(&self, file_path: &Path) -> Option<&Vec<ExtractedClass>> {
110 self.file_classes.get(file_path)
111 }
112
113 pub fn has_class(&self, class: &str) -> bool {
115 self.classes.contains(class)
116 }
117
118 pub fn get_all_classes(&self) -> Vec<String> {
120 let mut classes: Vec<String> = self.classes.iter().cloned().collect();
121 classes.sort();
122 classes
123 }
124}
125
126impl Default for ClassSet {
127 fn default() -> Self {
128 Self::new()
129 }
130}
131
132impl ContentScanner {
133 pub fn new(config: ScanConfig) -> Result<Self> {
135 let file_scanner = FileScanner::new(config.content_config.clone())?;
136 let class_extractor = ClassExtractor::new();
137 let parallel_processor = ParallelProcessor::new(config.parallel_processing);
138 let cache = Arc::new(RwLock::new(ScanCache::new()));
139
140 Ok(Self {
141 config,
142 file_scanner,
143 class_extractor,
144 parallel_processor,
145 cache,
146 })
147 }
148
149 pub async fn scan_paths(&self, paths: &[String]) -> Result<ClassSet> {
151 let start_time = std::time::Instant::now();
152
153 let files = self.file_scanner.discover_files(paths).await?;
155
156 let results = self
158 .parallel_processor
159 .process_files(&files, &self.class_extractor, &self.config)
160 .await?;
161
162 let mut class_set = ClassSet::new();
164 for (file_path, classes) in results {
165 class_set.add_file_classes(file_path, classes);
166 }
167
168 class_set.processing_stats.total_time = start_time.elapsed();
169 class_set.total_classes = class_set.classes.len();
170
171 {
173 let mut cache = self.cache.write().await;
174 for (file_path, classes) in &class_set.file_classes {
175 cache.update_file(file_path.clone(), classes.clone());
176 }
177 }
178
179 Ok(class_set)
180 }
181
182 pub async fn scan_file(&self, file_path: &Path) -> Result<Vec<ExtractedClass>> {
184 let file_info = self.file_scanner.read_file(file_path).await?;
185 self.class_extractor.extract_classes(&file_info).await
186 }
187
188 pub async fn get_cache_stats(&self) -> CacheStats {
190 let cache = self.cache.read().await;
191 cache.get_stats()
192 }
193
194 pub async fn clear_cache(&self) {
196 let mut cache = self.cache.write().await;
197 cache.clear();
198 }
199
200 pub fn get_config(&self) -> &ScanConfig {
202 &self.config
203 }
204
205 pub fn update_config(&mut self, config: ScanConfig) -> Result<()> {
207 self.config = config;
208 self.file_scanner = FileScanner::new(self.config.content_config.clone())?;
209 self.parallel_processor = ParallelProcessor::new(self.config.parallel_processing);
210 Ok(())
211 }
212}
213
214impl FileScanner {
215 pub fn new(config: ContentConfig) -> Result<Self> {
217 let mut file_types = HashMap::new();
218
219 file_types.insert("rs".to_string(), FileType::Rust);
221 file_types.insert("js".to_string(), FileType::JavaScript);
222 file_types.insert("jsx".to_string(), FileType::JavaScript);
223 file_types.insert("ts".to_string(), FileType::TypeScript);
224 file_types.insert("tsx".to_string(), FileType::TypeScript);
225 file_types.insert("html".to_string(), FileType::Html);
226 file_types.insert("htm".to_string(), FileType::Html);
227 file_types.insert("vue".to_string(), FileType::Vue);
228 file_types.insert("svelte".to_string(), FileType::Svelte);
229 file_types.insert("css".to_string(), FileType::Css);
230 file_types.insert("scss".to_string(), FileType::Scss);
231 file_types.insert("sass".to_string(), FileType::Scss);
232 file_types.insert("less".to_string(), FileType::Less);
233 file_types.insert("json".to_string(), FileType::Json);
234 file_types.insert("yaml".to_string(), FileType::Yaml);
235 file_types.insert("yml".to_string(), FileType::Yaml);
236 file_types.insert("toml".to_string(), FileType::Toml);
237 file_types.insert("md".to_string(), FileType::Markdown);
238 file_types.insert("markdown".to_string(), FileType::Markdown);
239
240 Ok(Self { config, file_types })
241 }
242
243 pub async fn discover_files(&self, paths: &[String]) -> Result<Vec<FileInfo>> {
245 let mut files = Vec::new();
246
247 for path in paths {
248 let path = Path::new(path);
249 if path.is_file() {
250 if let Ok(file_info) = self.read_file(path).await {
252 files.push(file_info);
253 }
254 } else if path.is_dir() {
255 let dir_files = self.scan_directory(path).await?;
257 files.extend(dir_files);
258 }
259 }
260
261 Ok(files)
262 }
263
264 async fn scan_directory(&self, dir_path: &Path) -> Result<Vec<FileInfo>> {
266 let mut files = Vec::new();
267
268 for entry in walkdir::WalkDir::new(dir_path)
269 .follow_links(false)
270 .into_iter()
271 .filter_map(|e| e.ok())
272 {
273 if entry.file_type().is_file() {
274 if let Ok(file_info) = self.read_file(entry.path()).await {
275 if self.matches_patterns(&file_info) {
277 files.push(file_info);
278 }
279 }
280 }
281 }
282
283 Ok(files)
284 }
285
286 pub async fn read_file(&self, file_path: &Path) -> Result<FileInfo> {
288 let metadata = std::fs::metadata(file_path)?;
289 let file_type = self.detect_file_type(file_path);
290 let content = if self.should_read_content(&file_type) {
291 Some(std::fs::read_to_string(file_path)?)
292 } else {
293 None
294 };
295
296 Ok(FileInfo {
297 path: file_path.to_path_buf(),
298 file_type,
299 size: metadata.len(),
300 modified: metadata.modified()?,
301 content,
302 })
303 }
304
305 fn detect_file_type(&self, file_path: &Path) -> FileType {
307 if let Some(extension) = file_path.extension() {
308 if let Some(extension_str) = extension.to_str() {
309 if let Some(file_type) = self.file_types.get(extension_str) {
310 return file_type.clone();
311 }
312 }
313 }
314 FileType::Other("unknown".to_string())
315 }
316
317 fn matches_patterns(&self, file_info: &FileInfo) -> bool {
319 if self.config.patterns.is_empty() {
320 return true; }
322
323 for pattern in &self.config.patterns {
324 if pattern.matches_file(&file_info.path) {
325 return true;
326 }
327 }
328
329 false
330 }
331
332 fn should_read_content(&self, file_type: &FileType) -> bool {
334 match file_type {
335 FileType::Rust
336 | FileType::JavaScript
337 | FileType::TypeScript
338 | FileType::Html
339 | FileType::Vue
340 | FileType::Svelte
341 | FileType::Css
342 | FileType::Scss
343 | FileType::Less
344 | FileType::Markdown => true,
345 _ => false,
346 }
347 }
348}
349
350impl Default for ProcessingStats {
351 fn default() -> Self {
352 Self {
353 total_files: 0,
354 processed_files: 0,
355 total_classes: 0,
356 unique_classes: 0,
357 total_time: std::time::Duration::from_millis(0),
358 average_file_time: std::time::Duration::from_millis(0),
359 memory_usage: 0,
360 }
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use std::path::PathBuf;
368
369 #[test]
370 fn test_class_set_creation() {
371 let class_set = ClassSet::new();
372 assert_eq!(class_set.total_classes(), 0);
373 assert_eq!(class_set.unique_classes(), 0);
374 }
375
376 #[test]
377 fn test_class_set_operations() {
378 let mut class_set = ClassSet::new();
379
380 let classes = vec![
381 ExtractedClass {
382 class_name: "p-4".to_string(),
383 context: ClassContext::new(),
384 line: 1,
385 column: 1,
386 },
387 ExtractedClass {
388 class_name: "bg-blue-500".to_string(),
389 context: ClassContext::new(),
390 line: 2,
391 column: 1,
392 },
393 ];
394
395 class_set.add_file_classes(PathBuf::from("test.rs"), classes);
396
397 assert_eq!(class_set.total_files, 1);
398 assert_eq!(class_set.unique_classes(), 2);
399 assert!(class_set.has_class("p-4"));
400 assert!(class_set.has_class("bg-blue-500"));
401 assert!(!class_set.has_class("text-red-500"));
402 }
403
404 #[test]
405 fn test_file_type_detection() {
406 let scanner = FileScanner::new(ContentConfig::default()).unwrap();
407
408 let rust_file = Path::new("test.rs");
409 let js_file = Path::new("test.js");
410 let html_file = Path::new("test.html");
411
412 assert_eq!(scanner.detect_file_type(rust_file), FileType::Rust);
413 assert_eq!(scanner.detect_file_type(js_file), FileType::JavaScript);
414 assert_eq!(scanner.detect_file_type(html_file), FileType::Html);
415 }
416
417 #[tokio::test]
418 async fn test_content_scanner_creation() {
419 let config = ScanConfig::default();
420 let scanner = ContentScanner::new(config);
421 assert!(scanner.is_ok());
422 }
423}