Skip to main content

null_e/plugins/
registry.rs

1//! Plugin registry - central management for all plugins
2
3use super::Plugin;
4use crate::core::{ProjectKind, ProjectMarker};
5use parking_lot::RwLock;
6use std::collections::HashMap;
7use std::path::Path;
8use std::sync::Arc;
9
10/// Central registry for all plugins
11pub struct PluginRegistry {
12    /// All registered plugins
13    plugins: RwLock<Vec<Arc<dyn Plugin>>>,
14    /// Markers indexed for fast lookup
15    markers: RwLock<Vec<(ProjectMarker, Arc<dyn Plugin>)>>,
16    /// Plugins indexed by project kind
17    by_kind: RwLock<HashMap<ProjectKind, Vec<Arc<dyn Plugin>>>>,
18    /// Quick lookup for cleanable directory names
19    cleanable_dirs: RwLock<HashMap<&'static str, Vec<Arc<dyn Plugin>>>>,
20}
21
22impl PluginRegistry {
23    /// Create an empty registry
24    pub fn new() -> Self {
25        Self {
26            plugins: RwLock::new(Vec::new()),
27            markers: RwLock::new(Vec::new()),
28            by_kind: RwLock::new(HashMap::new()),
29            cleanable_dirs: RwLock::new(HashMap::new()),
30        }
31    }
32
33    /// Create registry with all built-in plugins
34    pub fn with_builtins() -> Self {
35        let registry = Self::new();
36
37        for plugin in super::builtin_plugins() {
38            registry.register(Arc::from(plugin));
39        }
40
41        registry
42    }
43
44    /// Register a new plugin
45    pub fn register(&self, plugin: Arc<dyn Plugin>) {
46        // Add markers for fast lookup
47        for marker in plugin.markers() {
48            self.markers.write().push((marker, Arc::clone(&plugin)));
49        }
50
51        // Index by project kind
52        for kind in plugin.supported_kinds() {
53            self.by_kind
54                .write()
55                .entry(*kind)
56                .or_default()
57                .push(Arc::clone(&plugin));
58        }
59
60        // Index cleanable directories
61        for dir in plugin.cleanable_dirs() {
62            self.cleanable_dirs
63                .write()
64                .entry(dir)
65                .or_default()
66                .push(Arc::clone(&plugin));
67        }
68
69        self.plugins.write().push(plugin);
70    }
71
72    /// Get all registered plugins
73    pub fn all(&self) -> Vec<Arc<dyn Plugin>> {
74        self.plugins.read().clone()
75    }
76
77    /// Get all registered markers for scanning
78    pub fn all_markers(&self) -> Vec<ProjectMarker> {
79        self.markers.read().iter().map(|(m, _)| m.clone()).collect()
80    }
81
82    /// Find plugins that handle a project kind
83    pub fn plugins_for_kind(&self, kind: ProjectKind) -> Vec<Arc<dyn Plugin>> {
84        self.by_kind.read().get(&kind).cloned().unwrap_or_default()
85    }
86
87    /// Check if a directory name is a known cleanable artifact
88    pub fn is_cleanable_dir(&self, name: &str) -> bool {
89        self.cleanable_dirs.read().contains_key(name)
90    }
91
92    /// Get plugins that can handle a cleanable directory
93    pub fn plugins_for_cleanable_dir(&self, name: &str) -> Vec<Arc<dyn Plugin>> {
94        self.cleanable_dirs
95            .read()
96            .get(name)
97            .cloned()
98            .unwrap_or_default()
99    }
100
101    /// Detect project type at path
102    pub fn detect_project(&self, path: &Path) -> Option<(ProjectKind, Arc<dyn Plugin>)> {
103        let plugins = self.plugins.read();
104
105        // Collect all matches with their priorities
106        let mut candidates: Vec<_> = plugins
107            .iter()
108            .filter_map(|p| p.detect(path).map(|k| (k, Arc::clone(p), p.priority())))
109            .collect();
110
111        // Sort by priority (descending)
112        candidates.sort_by(|a, b| b.2.cmp(&a.2));
113
114        candidates.into_iter().next().map(|(k, p, _)| (k, p))
115    }
116
117    /// Get all unique cleanable directory names
118    pub fn all_cleanable_dir_names(&self) -> Vec<&'static str> {
119        self.cleanable_dirs.read().keys().copied().collect()
120    }
121
122    /// Get plugin by ID
123    pub fn get_by_id(&self, id: &str) -> Option<Arc<dyn Plugin>> {
124        self.plugins.read().iter().find(|p| p.id() == id).cloned()
125    }
126
127    /// Get number of registered plugins
128    pub fn len(&self) -> usize {
129        self.plugins.read().len()
130    }
131
132    /// Check if registry is empty
133    pub fn is_empty(&self) -> bool {
134        self.plugins.read().is_empty()
135    }
136}
137
138impl Default for PluginRegistry {
139    fn default() -> Self {
140        Self::with_builtins()
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_registry_with_builtins() {
150        let registry = PluginRegistry::with_builtins();
151        assert!(!registry.is_empty());
152        assert!(registry.len() >= 5); // At least our main plugins
153    }
154
155    #[test]
156    fn test_cleanable_dirs() {
157        let registry = PluginRegistry::with_builtins();
158        assert!(registry.is_cleanable_dir("node_modules"));
159        assert!(registry.is_cleanable_dir("target"));
160        assert!(registry.is_cleanable_dir("__pycache__"));
161        assert!(!registry.is_cleanable_dir("src"));
162    }
163
164    #[test]
165    fn test_get_by_id() {
166        let registry = PluginRegistry::with_builtins();
167        let node = registry.get_by_id("node");
168        assert!(node.is_some());
169        assert_eq!(node.unwrap().id(), "node");
170    }
171}