Skip to main content

null_e/cleaners/
ide.rs

1//! IDE cleanup module
2//!
3//! Handles cleanup of IDE caches and data:
4//! - JetBrains IDEs (IntelliJ, PyCharm, WebStorm, etc.)
5//! - VS Code
6//! - Sublime Text
7//! - Cursor
8
9use super::{calculate_dir_size, get_mtime, CleanableItem, SafetyLevel};
10use crate::error::Result;
11use std::path::PathBuf;
12
13/// IDE cleaner
14pub struct IdeCleaner {
15    home: PathBuf,
16}
17
18impl IdeCleaner {
19    /// Create a new IDE cleaner
20    pub fn new() -> Option<Self> {
21        let home = dirs::home_dir()?;
22        Some(Self { home })
23    }
24
25    /// Detect all IDE cleanable items
26    pub fn detect(&self) -> Result<Vec<CleanableItem>> {
27        let mut items = Vec::new();
28
29        // JetBrains
30        items.extend(self.detect_jetbrains()?);
31
32        // VS Code
33        items.extend(self.detect_vscode()?);
34
35        // Cursor (VS Code fork for AI)
36        items.extend(self.detect_cursor()?);
37
38        // Sublime Text
39        items.extend(self.detect_sublime()?);
40
41        // Zed
42        items.extend(self.detect_zed()?);
43
44        Ok(items)
45    }
46
47    /// Detect JetBrains IDE caches
48    fn detect_jetbrains(&self) -> Result<Vec<CleanableItem>> {
49        let mut items = Vec::new();
50
51        // JetBrains stores data in different locations per OS
52        #[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        // Check Library/Caches
89        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                        // Skip if less than 50MB
105                        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        // Check Library/Application Support
126        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                    // Check if this is an old version
140                    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        // Check Library/Logs
175        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        // Linux: ~/.cache/JetBrains and ~/.local/share/JetBrains
203        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        // Windows: ~/AppData/Local/JetBrains and ~/AppData/Roaming/JetBrains
249        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    /// Detect VS Code caches
275    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                // Skip if less than 50MB
314                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    /// Detect Cursor IDE caches (VS Code fork)
342    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    /// Detect Sublime Text caches
386    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    /// Detect Zed editor caches
438    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}