1use super::{calculate_dir_size, get_mtime, CleanableItem, SafetyLevel};
10use crate::error::Result;
11use std::path::PathBuf;
12
13pub struct IdeCleaner {
15 home: PathBuf,
16}
17
18impl IdeCleaner {
19 pub fn new() -> Option<Self> {
21 let home = dirs::home_dir()?;
22 Some(Self { home })
23 }
24
25 pub fn detect(&self) -> Result<Vec<CleanableItem>> {
27 let mut items = Vec::new();
28
29 items.extend(self.detect_jetbrains()?);
31
32 items.extend(self.detect_vscode()?);
34
35 items.extend(self.detect_cursor()?);
37
38 items.extend(self.detect_sublime()?);
40
41 items.extend(self.detect_zed()?);
43
44 Ok(items)
45 }
46
47 fn detect_jetbrains(&self) -> Result<Vec<CleanableItem>> {
49 let mut items = Vec::new();
50
51 #[cfg(target_os = "macos")]
53 {
54 items.extend(self.detect_jetbrains_macos()?);
55 }
56
57 #[cfg(target_os = "linux")]
58 {
59 items.extend(self.detect_jetbrains_linux()?);
60 }
61
62 #[cfg(target_os = "windows")]
63 {
64 items.extend(self.detect_jetbrains_windows()?);
65 }
66
67 Ok(items)
68 }
69
70 #[cfg(target_os = "macos")]
71 fn detect_jetbrains_macos(&self) -> Result<Vec<CleanableItem>> {
72 let mut items = Vec::new();
73
74 let jetbrains_products = [
75 "IntelliJIdea",
76 "PyCharm",
77 "WebStorm",
78 "PhpStorm",
79 "CLion",
80 "GoLand",
81 "Rider",
82 "RubyMine",
83 "DataGrip",
84 "AndroidStudio",
85 "Fleet",
86 ];
87
88 let caches_path = self.home.join("Library/Caches/JetBrains");
90 if caches_path.exists() {
91 if let Ok(entries) = std::fs::read_dir(&caches_path) {
92 for entry in entries.filter_map(|e| e.ok()) {
93 let path = entry.path();
94 if !path.is_dir() {
95 continue;
96 }
97
98 let name = path.file_name()
99 .map(|n| n.to_string_lossy().to_string())
100 .unwrap_or_default();
101
102 let (size, file_count) = calculate_dir_size(&path)?;
103 if size < 50_000_000 {
104 continue;
106 }
107
108 items.push(CleanableItem {
109 name: format!("JetBrains Cache: {}", name),
110 category: "IDE".to_string(),
111 subcategory: "JetBrains".to_string(),
112 icon: "🧠",
113 path,
114 size,
115 file_count: Some(file_count),
116 last_modified: get_mtime(&entry.path()),
117 description: "IDE cache and indexes. Will be rebuilt on next open.",
118 safe_to_delete: SafetyLevel::SafeWithCost,
119 clean_command: None,
120 });
121 }
122 }
123 }
124
125 let support_path = self.home.join("Library/Application Support/JetBrains");
127 if support_path.exists() {
128 if let Ok(entries) = std::fs::read_dir(&support_path) {
129 for entry in entries.filter_map(|e| e.ok()) {
130 let path = entry.path();
131 if !path.is_dir() {
132 continue;
133 }
134
135 let name = path.file_name()
136 .map(|n| n.to_string_lossy().to_string())
137 .unwrap_or_default();
138
139 let is_old_version = jetbrains_products.iter().any(|p| {
141 name.starts_with(p) && !name.contains("2024") && !name.contains("2025")
142 });
143
144 let (size, file_count) = calculate_dir_size(&path)?;
145 if size < 100_000_000 {
146 continue;
147 }
148
149 items.push(CleanableItem {
150 name: format!("JetBrains Data: {}", name),
151 category: "IDE".to_string(),
152 subcategory: "JetBrains".to_string(),
153 icon: "🧠",
154 path,
155 size,
156 file_count: Some(file_count),
157 last_modified: get_mtime(&entry.path()),
158 description: if is_old_version {
159 "Old IDE version data. Safe to delete if not using this version."
160 } else {
161 "IDE settings, plugins, and history."
162 },
163 safe_to_delete: if is_old_version {
164 SafetyLevel::Safe
165 } else {
166 SafetyLevel::Caution
167 },
168 clean_command: None,
169 });
170 }
171 }
172 }
173
174 let logs_path = self.home.join("Library/Logs/JetBrains");
176 if logs_path.exists() {
177 let (size, file_count) = calculate_dir_size(&logs_path)?;
178 if size > 10_000_000 {
179 items.push(CleanableItem {
180 name: "JetBrains Logs".to_string(),
181 category: "IDE".to_string(),
182 subcategory: "JetBrains".to_string(),
183 icon: "📝",
184 path: logs_path,
185 size,
186 file_count: Some(file_count),
187 last_modified: None,
188 description: "IDE log files. Safe to delete.",
189 safe_to_delete: SafetyLevel::Safe,
190 clean_command: None,
191 });
192 }
193 }
194
195 Ok(items)
196 }
197
198 #[cfg(target_os = "linux")]
199 fn detect_jetbrains_linux(&self) -> Result<Vec<CleanableItem>> {
200 let mut items = Vec::new();
201
202 let cache_path = self.home.join(".cache/JetBrains");
204 if cache_path.exists() {
205 let (size, file_count) = calculate_dir_size(&cache_path)?;
206 if size > 50_000_000 {
207 items.push(CleanableItem {
208 name: "JetBrains Cache".to_string(),
209 category: "IDE".to_string(),
210 subcategory: "JetBrains".to_string(),
211 icon: "🧠",
212 path: cache_path,
213 size,
214 file_count: Some(file_count),
215 last_modified: None,
216 description: "IDE cache and indexes.",
217 safe_to_delete: SafetyLevel::SafeWithCost,
218 clean_command: None,
219 });
220 }
221 }
222
223 let config_path = self.home.join(".config/JetBrains");
224 if config_path.exists() {
225 let (size, file_count) = calculate_dir_size(&config_path)?;
226 if size > 100_000_000 {
227 items.push(CleanableItem {
228 name: "JetBrains Config".to_string(),
229 category: "IDE".to_string(),
230 subcategory: "JetBrains".to_string(),
231 icon: "🧠",
232 path: config_path,
233 size,
234 file_count: Some(file_count),
235 last_modified: None,
236 description: "IDE settings and plugins.",
237 safe_to_delete: SafetyLevel::Caution,
238 clean_command: None,
239 });
240 }
241 }
242
243 Ok(items)
244 }
245
246 #[cfg(target_os = "windows")]
247 fn detect_jetbrains_windows(&self) -> Result<Vec<CleanableItem>> {
248 let mut items = Vec::new();
250
251 let local_path = self.home.join("AppData/Local/JetBrains");
252 if local_path.exists() {
253 let (size, file_count) = calculate_dir_size(&local_path)?;
254 if size > 50_000_000 {
255 items.push(CleanableItem {
256 name: "JetBrains Local Data".to_string(),
257 category: "IDE".to_string(),
258 subcategory: "JetBrains".to_string(),
259 icon: "🧠",
260 path: local_path,
261 size,
262 file_count: Some(file_count),
263 last_modified: None,
264 description: "IDE cache and indexes.",
265 safe_to_delete: SafetyLevel::SafeWithCost,
266 clean_command: None,
267 });
268 }
269 }
270
271 Ok(items)
272 }
273
274 fn detect_vscode(&self) -> Result<Vec<CleanableItem>> {
276 let mut items = Vec::new();
277
278 #[cfg(target_os = "macos")]
279 let vscode_paths = [
280 ("Library/Application Support/Code/CachedData", "VS Code Cached Data"),
281 ("Library/Application Support/Code/CachedExtensions", "VS Code Cached Extensions"),
282 ("Library/Application Support/Code/CachedExtensionVSIXs", "VS Code Extension VSIXs"),
283 ("Library/Application Support/Code/Cache", "VS Code Cache"),
284 ("Library/Application Support/Code/User/workspaceStorage", "VS Code Workspace Storage"),
285 ("Library/Caches/com.microsoft.VSCode", "VS Code System Cache"),
286 ("Library/Caches/com.microsoft.VSCode.ShipIt", "VS Code Update Cache"),
287 ];
288
289 #[cfg(target_os = "linux")]
290 let vscode_paths = [
291 (".config/Code/CachedData", "VS Code Cached Data"),
292 (".config/Code/CachedExtensions", "VS Code Cached Extensions"),
293 (".config/Code/Cache", "VS Code Cache"),
294 (".config/Code/User/workspaceStorage", "VS Code Workspace Storage"),
295 ];
296
297 #[cfg(target_os = "windows")]
298 let vscode_paths = [
299 ("AppData/Roaming/Code/CachedData", "VS Code Cached Data"),
300 ("AppData/Roaming/Code/CachedExtensions", "VS Code Cached Extensions"),
301 ("AppData/Roaming/Code/Cache", "VS Code Cache"),
302 ("AppData/Roaming/Code/User/workspaceStorage", "VS Code Workspace Storage"),
303 ];
304
305 for (rel_path, name) in vscode_paths {
306 let path = self.home.join(rel_path);
307 if !path.exists() {
308 continue;
309 }
310
311 let (size, file_count) = calculate_dir_size(&path)?;
312 if size < 50_000_000 {
313 continue;
315 }
316
317 let is_workspace = rel_path.contains("workspaceStorage");
318
319 items.push(CleanableItem {
320 name: name.to_string(),
321 category: "IDE".to_string(),
322 subcategory: "VS Code".to_string(),
323 icon: "💻",
324 path,
325 size,
326 file_count: Some(file_count),
327 last_modified: None,
328 description: if is_workspace {
329 "Workspace-specific cache. May include state for closed projects."
330 } else {
331 "VS Code cache. Will be rebuilt on next open."
332 },
333 safe_to_delete: SafetyLevel::SafeWithCost,
334 clean_command: None,
335 });
336 }
337
338 Ok(items)
339 }
340
341 fn detect_cursor(&self) -> Result<Vec<CleanableItem>> {
343 let mut items = Vec::new();
344
345 #[cfg(target_os = "macos")]
346 let cursor_paths = [
347 ("Library/Application Support/Cursor/CachedData", "Cursor Cached Data"),
348 ("Library/Application Support/Cursor/Cache", "Cursor Cache"),
349 ("Library/Application Support/Cursor/User/workspaceStorage", "Cursor Workspace Storage"),
350 ("Library/Caches/com.todesktop.230313mzl4w4u92", "Cursor System Cache"),
351 ];
352
353 #[cfg(not(target_os = "macos"))]
354 let cursor_paths: [(&str, &str); 0] = [];
355
356 for (rel_path, name) in cursor_paths {
357 let path = self.home.join(rel_path);
358 if !path.exists() {
359 continue;
360 }
361
362 let (size, file_count) = calculate_dir_size(&path)?;
363 if size < 50_000_000 {
364 continue;
365 }
366
367 items.push(CleanableItem {
368 name: name.to_string(),
369 category: "IDE".to_string(),
370 subcategory: "Cursor".to_string(),
371 icon: "🖱️",
372 path,
373 size,
374 file_count: Some(file_count),
375 last_modified: None,
376 description: "Cursor IDE cache. Will be rebuilt on next open.",
377 safe_to_delete: SafetyLevel::SafeWithCost,
378 clean_command: None,
379 });
380 }
381
382 Ok(items)
383 }
384
385 fn detect_sublime(&self) -> Result<Vec<CleanableItem>> {
387 let mut items = Vec::new();
388
389 #[cfg(target_os = "macos")]
390 let sublime_paths = [
391 ("Library/Application Support/Sublime Text/Cache", "Sublime Cache"),
392 ("Library/Application Support/Sublime Text/Index", "Sublime Index"),
393 ("Library/Caches/com.sublimetext.4", "Sublime System Cache"),
394 ];
395
396 #[cfg(target_os = "linux")]
397 let sublime_paths = [
398 (".config/sublime-text/Cache", "Sublime Cache"),
399 (".config/sublime-text/Index", "Sublime Index"),
400 ];
401
402 #[cfg(target_os = "windows")]
403 let sublime_paths = [
404 ("AppData/Roaming/Sublime Text/Cache", "Sublime Cache"),
405 ("AppData/Roaming/Sublime Text/Index", "Sublime Index"),
406 ];
407
408 for (rel_path, name) in sublime_paths {
409 let path = self.home.join(rel_path);
410 if !path.exists() {
411 continue;
412 }
413
414 let (size, file_count) = calculate_dir_size(&path)?;
415 if size < 10_000_000 {
416 continue;
417 }
418
419 items.push(CleanableItem {
420 name: name.to_string(),
421 category: "IDE".to_string(),
422 subcategory: "Sublime Text".to_string(),
423 icon: "📝",
424 path,
425 size,
426 file_count: Some(file_count),
427 last_modified: None,
428 description: "Sublime Text cache and index files.",
429 safe_to_delete: SafetyLevel::Safe,
430 clean_command: None,
431 });
432 }
433
434 Ok(items)
435 }
436
437 fn detect_zed(&self) -> Result<Vec<CleanableItem>> {
439 let mut items = Vec::new();
440
441 #[cfg(target_os = "macos")]
442 {
443 let zed_cache = self.home.join("Library/Caches/dev.zed.Zed");
444 if zed_cache.exists() {
445 let (size, file_count) = calculate_dir_size(&zed_cache)?;
446 if size > 50_000_000 {
447 items.push(CleanableItem {
448 name: "Zed Cache".to_string(),
449 category: "IDE".to_string(),
450 subcategory: "Zed".to_string(),
451 icon: "⚡",
452 path: zed_cache,
453 size,
454 file_count: Some(file_count),
455 last_modified: None,
456 description: "Zed editor cache files.",
457 safe_to_delete: SafetyLevel::Safe,
458 clean_command: None,
459 });
460 }
461 }
462 }
463
464 Ok(items)
465 }
466}
467
468impl Default for IdeCleaner {
469 fn default() -> Self {
470 Self::new().expect("IdeCleaner requires home directory")
471 }
472}
473
474#[cfg(test)]
475mod tests {
476 use super::*;
477
478 #[test]
479 fn test_ide_cleaner_creation() {
480 let cleaner = IdeCleaner::new();
481 assert!(cleaner.is_some());
482 }
483
484 #[test]
485 fn test_ide_detection() {
486 if let Some(cleaner) = IdeCleaner::new() {
487 let items = cleaner.detect().unwrap();
488 println!("Found {} IDE items", items.len());
489 for item in &items {
490 println!(" {} {} ({} bytes)", item.icon, item.name, item.size);
491 }
492 }
493 }
494}