1use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::path::{Path, PathBuf};
10use std::time::SystemTime;
11
12use crate::{detect_project_context, FileNode, Scanner, ScannerConfig, TreeStats};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ProjectAnalysis {
17 pub project_path: PathBuf,
18 pub project_type: String,
19 pub project_name: String,
20 pub total_files: usize,
21 pub total_directories: usize,
22 pub total_size: u64,
23 pub key_files: Vec<String>,
24 pub recent_files: Vec<String>,
25 pub file_types: std::collections::HashMap<String, usize>,
26 pub insights: Vec<String>,
27}
28
29pub struct ProjectAnalyzer {
31 default_config: ScannerConfig,
32}
33
34impl Default for ProjectAnalyzer {
35 fn default() -> Self {
36 Self::new()
37 }
38}
39
40impl ProjectAnalyzer {
41 pub fn new() -> Self {
43 let config = ScannerConfig {
44 max_depth: 10,
45 show_hidden: false,
46 respect_gitignore: true,
47 ..ScannerConfig::default()
48 };
49
50 Self {
51 default_config: config,
52 }
53 }
54
55 pub fn analyze_project(&self, project_path: &Path) -> Result<ProjectAnalysis> {
57 let scanner = Scanner::new(project_path, self.default_config.clone())?;
58 let (nodes, stats) = scanner.scan()?;
59
60 let project_type =
62 detect_project_context(project_path).unwrap_or_else(|| "Unknown".to_string());
63
64 let project_name = project_path
66 .file_name()
67 .and_then(|n| n.to_str())
68 .unwrap_or("Unknown")
69 .to_string();
70
71 let key_files = Self::extract_key_files(&nodes);
73
74 let recent_files = Self::find_recent_files(&nodes, 1);
76
77 let file_types = Self::analyze_file_types(&nodes);
79
80 let insights = Self::generate_insights(&stats, &project_type, &nodes);
82
83 Ok(ProjectAnalysis {
84 project_path: project_path.to_path_buf(),
85 project_type,
86 project_name,
87 total_files: stats.total_files as usize,
88 total_directories: stats.total_dirs as usize,
89 total_size: stats.total_size,
90 key_files,
91 recent_files,
92 file_types,
93 insights,
94 })
95 }
96
97 pub fn quick_analysis(&self, project_path: &Path) -> Result<ProjectAnalysis> {
99 let mut config = self.default_config.clone();
100 config.max_depth = 2; let scanner = Scanner::new(project_path, config)?;
103 let (_nodes, stats) = scanner.quick_scan()?;
104
105 let project_type =
106 detect_project_context(project_path).unwrap_or_else(|| "Unknown".to_string());
107
108 let project_name = project_path
109 .file_name()
110 .and_then(|n| n.to_str())
111 .unwrap_or("Unknown")
112 .to_string();
113
114 Ok(ProjectAnalysis {
115 project_path: project_path.to_path_buf(),
116 project_type: project_type.clone(),
117 project_name,
118 total_files: stats.total_files as usize,
119 total_directories: stats.total_dirs as usize,
120 total_size: stats.total_size,
121 key_files: vec![], recent_files: vec![], file_types: std::collections::HashMap::new(), insights: vec![format!(
125 "{} project with {} files",
126 project_type, stats.total_files
127 )],
128 })
129 }
130
131 pub fn find_recent_activity(&self, project_path: &Path, hours: u64) -> Result<Vec<FileNode>> {
133 let scanner = Scanner::new(project_path, self.default_config.clone())?;
134 scanner.find_recent_files(hours)
135 }
136
137 pub fn get_key_files(&self, project_path: &Path) -> Result<Vec<FileNode>> {
139 let scanner = Scanner::new(project_path, self.default_config.clone())?;
140 scanner.find_key_files()
141 }
142
143 fn extract_key_files(nodes: &[FileNode]) -> Vec<String> {
145 let important_patterns = [
146 "main.rs",
147 "lib.rs",
148 "mod.rs",
149 "package.json",
150 "Cargo.toml",
151 "requirements.txt",
152 "pyproject.toml",
153 "README.md",
154 "LICENSE",
155 "Makefile",
156 "CMakeLists.txt",
157 "index.js",
158 "app.js",
159 "server.js",
160 "main.js",
161 "main.py",
162 "__init__.py",
163 "setup.py",
164 "go.mod",
165 "main.go",
166 ];
167
168 let mut key_files = Vec::new();
169 for node in nodes {
170 if !node.is_dir {
171 let file_name = node.path.file_name().and_then(|n| n.to_str()).unwrap_or("");
172
173 for pattern in &important_patterns {
174 if file_name == *pattern {
175 key_files.push(node.path.to_string_lossy().to_string());
176 break;
177 }
178 }
179 }
180 }
181
182 key_files.sort();
183 key_files.dedup();
184 key_files
185 }
186
187 fn find_recent_files(nodes: &[FileNode], hours_ago: u64) -> Vec<String> {
188 let cutoff_time = SystemTime::now() - std::time::Duration::from_secs(hours_ago * 3600);
189
190 nodes
191 .iter()
192 .filter(|node| !node.is_dir && node.modified > cutoff_time)
193 .map(|node| node.path.to_string_lossy().to_string())
194 .collect()
195 }
196
197 fn analyze_file_types(nodes: &[FileNode]) -> std::collections::HashMap<String, usize> {
198 let mut types = std::collections::HashMap::new();
199
200 for node in nodes {
201 if !node.is_dir {
202 let category = format!("{:?}", node.category);
203 *types.entry(category).or_insert(0) += 1;
204 }
205 }
206
207 types
208 }
209
210 fn generate_insights(stats: &TreeStats, project_type: &str, nodes: &[FileNode]) -> Vec<String> {
211 let mut insights = Vec::new();
212
213 if stats.total_files > 1000 {
215 insights.push("Large codebase with extensive structure".to_string());
216 } else if stats.total_files > 100 {
217 insights.push("Medium-sized project".to_string());
218 } else {
219 insights.push("Focused project with concise structure".to_string());
220 }
221
222 insights.push(format!("{} project", project_type));
224
225 let has_tests = nodes.iter().any(|n| {
227 let path_str = n.path.to_string_lossy();
228 path_str.contains("test") || path_str.contains("spec")
229 });
230 if has_tests {
231 insights.push("Includes test suite".to_string());
232 }
233
234 let has_docs = nodes.iter().any(|n| {
235 let path_str = n.path.to_string_lossy();
236 path_str.contains("README") || path_str.contains("doc")
237 });
238 if has_docs {
239 insights.push("Well-documented project".to_string());
240 }
241
242 insights
243 }
244}
245
246pub fn analyze_project(project_path: &Path) -> Result<ProjectAnalysis> {
248 let analyzer = ProjectAnalyzer::new();
249 analyzer.analyze_project(project_path)
250}
251
252pub fn quick_project_overview(project_path: &Path) -> Result<String> {
254 let analyzer = ProjectAnalyzer::new();
255 let analysis = analyzer.quick_analysis(project_path)?;
256
257 Ok(format!(
258 "{} | {} ({} files, {} dirs)",
259 analysis.project_name,
260 analysis.project_type,
261 analysis.total_files,
262 analysis.total_directories
263 ))
264}