Skip to main content

enya_plugin/
registry.rs

1//! Plugin registry for managing plugin lifecycle.
2
3use rustc_hash::FxHashMap;
4
5use crate::hooks::{
6    CommandHook, CommandHookResult, KeyCombo, KeyEvent, KeyboardHook, KeyboardHookResult,
7    LifecycleHook, PaneHook, ThemeHook,
8};
9use crate::theme::ThemeDefinition;
10use crate::traits::{CommandConfig, KeybindingConfig, PaneConfig, Plugin, PluginCapabilities};
11use crate::types::{PluginContext, Theme};
12use crate::{PluginError, PluginResult};
13
14/// Unique identifier for a registered plugin.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct PluginId(usize);
17
18impl PluginId {
19    /// Get the inner numeric value.
20    pub fn value(&self) -> usize {
21        self.0
22    }
23}
24
25/// Runtime state of a plugin.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum PluginState {
28    /// Plugin is registered but not initialized
29    Registered,
30    /// Plugin is initialized but not active
31    Inactive,
32    /// Plugin is active and running
33    Active,
34    /// Plugin failed to initialize or activate
35    Failed,
36    /// Plugin is disabled by user
37    Disabled,
38}
39
40/// Information about a registered plugin.
41#[derive(Debug, Clone)]
42pub struct PluginInfo {
43    /// Plugin identifier
44    pub id: PluginId,
45    /// Plugin name
46    pub name: String,
47    /// Plugin version
48    pub version: String,
49    /// Plugin description
50    pub description: String,
51    /// Plugin capabilities
52    pub capabilities: PluginCapabilities,
53    /// Current state
54    pub state: PluginState,
55    /// Whether the plugin is enabled by default
56    pub enabled_by_default: bool,
57}
58
59/// Registry entry for a plugin.
60struct PluginEntry {
61    /// The plugin instance
62    plugin: Box<dyn Plugin>,
63    /// Plugin metadata
64    info: PluginInfo,
65    /// Commands provided by the plugin
66    commands: Vec<CommandConfig>,
67    /// Pane types provided by the plugin
68    pane_types: Vec<PaneConfig>,
69    /// Keybindings provided by the plugin
70    keybindings: Vec<KeybindingConfig>,
71    /// Lifecycle hooks
72    lifecycle_hook: Option<Box<dyn LifecycleHook>>,
73    /// Command hooks
74    command_hook: Option<Box<dyn CommandHook>>,
75    /// Keyboard hooks
76    keyboard_hook: Option<Box<dyn KeyboardHook>>,
77    /// Theme hooks
78    theme_hook: Option<Box<dyn ThemeHook>>,
79    /// Pane hooks
80    pane_hook: Option<Box<dyn PaneHook>>,
81}
82
83/// Central registry for managing plugins.
84///
85/// The registry handles plugin lifecycle:
86/// - Registration: Adding plugins to the system
87/// - Initialization: Setting up plugins with context
88/// - Activation/Deactivation: Enabling/disabling plugins
89/// - Hook dispatch: Routing events to interested plugins
90pub struct PluginRegistry {
91    /// Registered plugins by ID
92    plugins: FxHashMap<PluginId, PluginEntry>,
93    /// Plugin name to ID mapping
94    name_to_id: FxHashMap<String, PluginId>,
95    /// Command name/alias to plugin ID mapping for O(1) command lookup
96    command_to_plugin: FxHashMap<String, PluginId>,
97    /// Next plugin ID
98    next_id: usize,
99    /// Plugin context (shared with all plugins)
100    context: Option<PluginContext>,
101    /// Plugins enabled by user configuration
102    enabled_plugins: FxHashMap<String, bool>,
103}
104
105impl Default for PluginRegistry {
106    fn default() -> Self {
107        Self::new()
108    }
109}
110
111impl PluginRegistry {
112    /// Create a new empty plugin registry.
113    pub fn new() -> Self {
114        Self {
115            plugins: FxHashMap::default(),
116            name_to_id: FxHashMap::default(),
117            command_to_plugin: FxHashMap::default(),
118            next_id: 0,
119            context: None,
120            enabled_plugins: FxHashMap::default(),
121        }
122    }
123
124    /// Initialize the registry with the plugin context.
125    pub fn init(&mut self, context: PluginContext) {
126        self.context = Some(context);
127    }
128
129    /// Get a reference to the plugin context.
130    pub fn context(&self) -> Option<&PluginContext> {
131        self.context.as_ref()
132    }
133
134    /// Set the enabled state for a plugin by name.
135    pub fn set_plugin_enabled(&mut self, name: &str, enabled: bool) {
136        self.enabled_plugins.insert(name.to_string(), enabled);
137    }
138
139    /// Check if a plugin is enabled.
140    pub fn is_plugin_enabled(&self, name: &str) -> bool {
141        self.enabled_plugins.get(name).copied().unwrap_or(true)
142    }
143
144    /// Register a plugin with the registry.
145    ///
146    /// This does not initialize or activate the plugin - call `init_plugin`
147    /// and `activate_plugin` separately.
148    pub fn register<P: Plugin + 'static>(
149        &mut self,
150        plugin: P,
151        enabled_by_default: bool,
152    ) -> PluginResult<PluginId> {
153        let name = plugin.name().to_string();
154
155        if self.name_to_id.contains_key(&name) {
156            return Err(PluginError::AlreadyRegistered(name));
157        }
158
159        let id = PluginId(self.next_id);
160        self.next_id += 1;
161
162        let info = PluginInfo {
163            id,
164            name: name.clone(),
165            version: plugin.version().to_string(),
166            description: plugin.description().to_string(),
167            capabilities: plugin.capabilities(),
168            state: PluginState::Registered,
169            enabled_by_default,
170        };
171
172        let entry = PluginEntry {
173            plugin: Box::new(plugin),
174            info,
175            commands: vec![],
176            pane_types: vec![],
177            keybindings: vec![],
178            lifecycle_hook: None,
179            command_hook: None,
180            keyboard_hook: None,
181            theme_hook: None,
182            pane_hook: None,
183        };
184
185        self.plugins.insert(id, entry);
186        self.name_to_id.insert(name, id);
187
188        Ok(id)
189    }
190
191    /// Initialize a registered plugin.
192    pub fn init_plugin(&mut self, id: PluginId) -> PluginResult<()> {
193        let ctx = self
194            .context
195            .as_ref()
196            .ok_or_else(|| PluginError::OperationFailed("Registry not initialized".to_string()))?;
197
198        let entry = self
199            .plugins
200            .get_mut(&id)
201            .ok_or_else(|| PluginError::NotFound(format!("Plugin ID {}", id.0)))?;
202
203        if entry.info.state != PluginState::Registered {
204            return Ok(()); // Already initialized
205        }
206
207        // Check minimum editor version
208        if let Some(min_version) = entry.plugin.min_editor_version() {
209            let current = ctx.editor_version();
210            if !Self::check_version(current, min_version) {
211                entry.info.state = PluginState::Failed;
212                return Err(PluginError::IncompatibleVersion {
213                    required: min_version.to_string(),
214                    actual: current.to_string(),
215                });
216            }
217        }
218
219        // Initialize the plugin
220        if let Err(e) = entry.plugin.init(ctx) {
221            entry.info.state = PluginState::Failed;
222            return Err(e);
223        }
224
225        // Collect plugin-provided items
226        entry.commands = entry.plugin.commands();
227        entry.pane_types = entry.plugin.pane_types();
228        entry.keybindings = entry.plugin.keybindings();
229
230        // Collect hooks
231        entry.lifecycle_hook = entry.plugin.lifecycle_hooks();
232        entry.command_hook = entry.plugin.command_hooks();
233        entry.keyboard_hook = entry.plugin.keyboard_hooks();
234        entry.theme_hook = entry.plugin.theme_hooks();
235        entry.pane_hook = entry.plugin.pane_hooks();
236
237        entry.info.state = PluginState::Inactive;
238        Ok(())
239    }
240
241    /// Activate a plugin (must be initialized first).
242    pub fn activate_plugin(&mut self, id: PluginId) -> PluginResult<()> {
243        let ctx = self
244            .context
245            .as_ref()
246            .ok_or_else(|| PluginError::OperationFailed("Registry not initialized".to_string()))?;
247
248        let entry = self
249            .plugins
250            .get_mut(&id)
251            .ok_or_else(|| PluginError::NotFound(format!("Plugin ID {}", id.0)))?;
252
253        if entry.info.state != PluginState::Inactive {
254            return Ok(()); // Already active or in wrong state
255        }
256
257        // Check if user has disabled this plugin
258        if !self
259            .enabled_plugins
260            .get(&entry.info.name)
261            .copied()
262            .unwrap_or(entry.info.enabled_by_default)
263        {
264            entry.info.state = PluginState::Disabled;
265            return Ok(());
266        }
267
268        if let Err(e) = entry.plugin.activate(ctx) {
269            entry.info.state = PluginState::Failed;
270            return Err(e);
271        }
272
273        entry.info.state = PluginState::Active;
274
275        // Index commands for O(1) lookup
276        for cmd in &entry.commands {
277            self.command_to_plugin.insert(cmd.name.clone(), id);
278            for alias in &cmd.aliases {
279                self.command_to_plugin.insert(alias.clone(), id);
280            }
281        }
282
283        Ok(())
284    }
285
286    /// Deactivate a plugin.
287    pub fn deactivate_plugin(&mut self, id: PluginId) -> PluginResult<()> {
288        let ctx = self
289            .context
290            .as_ref()
291            .ok_or_else(|| PluginError::OperationFailed("Registry not initialized".to_string()))?;
292
293        let entry = self
294            .plugins
295            .get_mut(&id)
296            .ok_or_else(|| PluginError::NotFound(format!("Plugin ID {}", id.0)))?;
297
298        if entry.info.state != PluginState::Active {
299            return Ok(()); // Not active
300        }
301
302        // Remove commands from index
303        for cmd in &entry.commands {
304            self.command_to_plugin.remove(&cmd.name);
305            for alias in &cmd.aliases {
306                self.command_to_plugin.remove(alias);
307            }
308        }
309
310        if let Err(e) = entry.plugin.deactivate(ctx) {
311            log::warn!("Plugin {} deactivation error: {e}", entry.info.name);
312        }
313
314        entry.info.state = PluginState::Inactive;
315        Ok(())
316    }
317
318    /// Unregister a plugin completely, removing it from the registry.
319    ///
320    /// This deactivates the plugin first if it's active, then removes all
321    /// references to it from the registry. Use this for hot-reload scenarios
322    /// where you need to replace a plugin with a new version.
323    pub fn unregister_plugin(&mut self, id: PluginId) -> PluginResult<()> {
324        // First deactivate if active
325        let _ = self.deactivate_plugin(id);
326
327        // Get the entry to clean up mappings
328        let entry = self
329            .plugins
330            .remove(&id)
331            .ok_or_else(|| PluginError::NotFound(format!("Plugin ID {}", id.0)))?;
332
333        // Remove name mapping
334        self.name_to_id.remove(&entry.info.name);
335
336        // Remove commands from index (in case deactivate didn't run)
337        for cmd in &entry.commands {
338            self.command_to_plugin.remove(&cmd.name);
339            for alias in &cmd.aliases {
340                self.command_to_plugin.remove(alias);
341            }
342        }
343
344        log::info!("Unregistered plugin: {}", entry.info.name);
345        Ok(())
346    }
347
348    /// Unregister a plugin by name.
349    pub fn unregister_plugin_by_name(&mut self, name: &str) -> PluginResult<()> {
350        let id = self
351            .name_to_id
352            .get(name)
353            .copied()
354            .ok_or_else(|| PluginError::NotFound(format!("Plugin '{name}'")))?;
355        self.unregister_plugin(id)
356    }
357
358    /// Get a plugin by ID.
359    pub fn get(&self, id: PluginId) -> Option<&dyn Plugin> {
360        self.plugins.get(&id).map(|e| e.plugin.as_ref())
361    }
362
363    /// Get a mutable plugin by ID.
364    pub fn get_mut(&mut self, id: PluginId) -> Option<&mut dyn Plugin> {
365        self.plugins.get_mut(&id).map(|e| e.plugin.as_mut())
366    }
367
368    /// Get a plugin by name.
369    pub fn get_by_name(&self, name: &str) -> Option<&dyn Plugin> {
370        self.name_to_id
371            .get(name)
372            .and_then(|id| self.plugins.get(id))
373            .map(|e| e.plugin.as_ref())
374    }
375
376    /// Get plugin info by ID.
377    pub fn info(&self, id: PluginId) -> Option<&PluginInfo> {
378        self.plugins.get(&id).map(|e| &e.info)
379    }
380
381    /// Get plugin info by name.
382    pub fn info_by_name(&self, name: &str) -> Option<&PluginInfo> {
383        self.name_to_id
384            .get(name)
385            .and_then(|id| self.plugins.get(id))
386            .map(|e| &e.info)
387    }
388
389    /// List all registered plugins.
390    pub fn list_plugins(&self) -> Vec<&PluginInfo> {
391        self.plugins.values().map(|e| &e.info).collect()
392    }
393
394    /// List active plugins.
395    pub fn active_plugins(&self) -> Vec<&PluginInfo> {
396        self.plugins
397            .values()
398            .filter(|e| e.info.state == PluginState::Active)
399            .map(|e| &e.info)
400            .collect()
401    }
402
403    /// Get all commands from active plugins.
404    pub fn all_commands(&self) -> Vec<(&PluginInfo, &CommandConfig)> {
405        self.plugins
406            .values()
407            .filter(|e| e.info.state == PluginState::Active)
408            .flat_map(|e| e.commands.iter().map(move |c| (&e.info, c)))
409            .collect()
410    }
411
412    /// Get all pane types from active plugins.
413    pub fn all_pane_types(&self) -> Vec<(&PluginInfo, &PaneConfig)> {
414        self.plugins
415            .values()
416            .filter(|e| e.info.state == PluginState::Active)
417            .flat_map(|e| e.pane_types.iter().map(move |p| (&e.info, p)))
418            .collect()
419    }
420
421    /// Get all keybindings from active plugins.
422    pub fn all_keybindings(&self) -> Vec<(&PluginInfo, &KeybindingConfig)> {
423        self.plugins
424            .values()
425            .filter(|e| e.info.state == PluginState::Active)
426            .flat_map(|e| e.keybindings.iter().map(move |k| (&e.info, k)))
427            .collect()
428    }
429
430    /// Get all custom themes from active plugins.
431    pub fn all_themes(&self) -> Vec<ThemeDefinition> {
432        self.plugins
433            .values()
434            .filter(|e| e.info.state == PluginState::Active)
435            .flat_map(|e| e.plugin.themes())
436            .collect()
437    }
438
439    /// Get all custom table pane configurations from active plugins.
440    pub fn all_custom_table_panes(&self) -> Vec<crate::CustomTableConfig> {
441        self.plugins
442            .values()
443            .filter(|e| e.info.state == PluginState::Active)
444            .flat_map(|e| e.plugin.custom_table_panes())
445            .collect()
446    }
447
448    /// Get all custom chart pane configurations from active plugins.
449    pub fn all_custom_chart_panes(&self) -> Vec<crate::CustomChartConfig> {
450        self.plugins
451            .values()
452            .filter(|e| e.info.state == PluginState::Active)
453            .flat_map(|e| e.plugin.custom_chart_panes())
454            .collect()
455    }
456
457    /// Get all custom stat pane configurations from active plugins.
458    pub fn all_custom_stat_panes(&self) -> Vec<crate::StatPaneConfig> {
459        self.plugins
460            .values()
461            .filter(|e| e.info.state == PluginState::Active)
462            .flat_map(|e| e.plugin.custom_stat_panes())
463            .collect()
464    }
465
466    /// Get all custom gauge pane configurations from active plugins.
467    pub fn all_custom_gauge_panes(&self) -> Vec<crate::GaugePaneConfig> {
468        self.plugins
469            .values()
470            .filter(|e| e.info.state == PluginState::Active)
471            .flat_map(|e| e.plugin.custom_gauge_panes())
472            .collect()
473    }
474
475    /// Get all pane types that support auto-refresh from active plugins.
476    ///
477    /// Returns a vector of (pane_type_name, refresh_interval_seconds) tuples.
478    pub fn all_refreshable_pane_types(&self) -> Vec<(String, u32)> {
479        self.plugins
480            .values()
481            .filter(|e| e.info.state == PluginState::Active)
482            .flat_map(|e| {
483                e.plugin
484                    .refreshable_pane_types()
485                    .into_iter()
486                    .map(|(name, interval)| (name.to_string(), interval))
487            })
488            .collect()
489    }
490
491    /// Trigger a refresh for a specific pane type.
492    ///
493    /// Finds the plugin that owns this pane type and calls its refresh callback.
494    /// Returns true if the refresh was triggered successfully.
495    pub fn trigger_pane_refresh(&mut self, pane_type: &str) -> bool {
496        let ctx = match &self.context {
497            Some(c) => c,
498            None => return false,
499        };
500
501        for entry in self.plugins.values_mut() {
502            if entry.info.state != PluginState::Active {
503                continue;
504            }
505
506            // Check if this plugin has this pane type as refreshable
507            let has_pane_type = entry
508                .plugin
509                .refreshable_pane_types()
510                .iter()
511                .any(|(name, _)| *name == pane_type);
512
513            if has_pane_type {
514                return entry.plugin.trigger_pane_refresh(pane_type, ctx);
515            }
516        }
517
518        false
519    }
520
521    /// Get commands for a specific plugin.
522    pub fn commands_for_plugin(&self, id: PluginId) -> Vec<&CommandConfig> {
523        self.plugins
524            .get(&id)
525            .map(|e| e.commands.iter().collect())
526            .unwrap_or_default()
527    }
528
529    /// Get keybindings for a specific plugin.
530    pub fn keybindings_for_plugin(&self, id: PluginId) -> Vec<&KeybindingConfig> {
531        self.plugins
532            .get(&id)
533            .map(|e| e.keybindings.iter().collect())
534            .unwrap_or_default()
535    }
536
537    /// Execute a plugin command.
538    pub fn execute_command(&mut self, command: &str, args: &str) -> bool {
539        let ctx = match &self.context {
540            Some(c) => c,
541            None => return false,
542        };
543
544        // O(1) lookup using command index
545        let plugin_id = match self.command_to_plugin.get(command) {
546            Some(&id) => id,
547            None => return false,
548        };
549
550        let entry = match self.plugins.get_mut(&plugin_id) {
551            Some(e) if e.info.state == PluginState::Active => e,
552            _ => return false,
553        };
554
555        entry.plugin.execute_command(command, args, ctx)
556    }
557
558    // ==================== Hook Dispatch ====================
559
560    /// Dispatch lifecycle: workspace loaded.
561    pub fn on_workspace_loaded(&mut self) {
562        for entry in self.plugins.values_mut() {
563            if entry.info.state == PluginState::Active {
564                if let Some(ref mut hook) = entry.lifecycle_hook {
565                    hook.on_workspace_loaded();
566                }
567            }
568        }
569    }
570
571    /// Dispatch lifecycle: workspace saving.
572    pub fn on_workspace_saving(&mut self) {
573        for entry in self.plugins.values_mut() {
574            if entry.info.state == PluginState::Active {
575                if let Some(ref mut hook) = entry.lifecycle_hook {
576                    hook.on_workspace_saving();
577                }
578            }
579        }
580    }
581
582    /// Dispatch lifecycle: pane added.
583    pub fn on_pane_added(&mut self, pane_id: usize) {
584        for entry in self.plugins.values_mut() {
585            if entry.info.state == PluginState::Active {
586                if let Some(ref mut hook) = entry.lifecycle_hook {
587                    hook.on_pane_added(pane_id);
588                }
589            }
590        }
591    }
592
593    /// Dispatch lifecycle: pane removing.
594    pub fn on_pane_removing(&mut self, pane_id: usize) {
595        for entry in self.plugins.values_mut() {
596            if entry.info.state == PluginState::Active {
597                if let Some(ref mut hook) = entry.lifecycle_hook {
598                    hook.on_pane_removing(pane_id);
599                }
600            }
601        }
602    }
603
604    /// Dispatch lifecycle: pane focused.
605    pub fn on_pane_focused(&mut self, pane_id: usize) {
606        for entry in self.plugins.values_mut() {
607            if entry.info.state == PluginState::Active {
608                if let Some(ref mut hook) = entry.lifecycle_hook {
609                    hook.on_pane_focused(pane_id);
610                }
611            }
612        }
613    }
614
615    /// Dispatch lifecycle: closing.
616    pub fn on_closing(&mut self) {
617        for entry in self.plugins.values_mut() {
618            if entry.info.state == PluginState::Active {
619                if let Some(ref mut hook) = entry.lifecycle_hook {
620                    hook.on_closing();
621                }
622            }
623        }
624    }
625
626    /// Dispatch lifecycle: frame update.
627    pub fn on_frame(&mut self) {
628        for entry in self.plugins.values_mut() {
629            if entry.info.state == PluginState::Active {
630                if let Some(ref mut hook) = entry.lifecycle_hook {
631                    hook.on_frame();
632                }
633            }
634        }
635    }
636
637    /// Dispatch command hook: before command.
638    pub fn before_command(&mut self, command: &str, args: &str) -> CommandHookResult {
639        for entry in self.plugins.values_mut() {
640            if entry.info.state == PluginState::Active {
641                if let Some(ref mut hook) = entry.command_hook {
642                    let result = hook.before_command(command, args);
643                    if result != CommandHookResult::Continue {
644                        return result;
645                    }
646                }
647            }
648        }
649        CommandHookResult::Continue
650    }
651
652    /// Dispatch command hook: after command.
653    pub fn after_command(&mut self, command: &str, args: &str, success: bool) {
654        for entry in self.plugins.values_mut() {
655            if entry.info.state == PluginState::Active {
656                if let Some(ref mut hook) = entry.command_hook {
657                    hook.after_command(command, args, success);
658                }
659            }
660        }
661    }
662
663    /// Dispatch keyboard hook: key pressed.
664    pub fn on_key_pressed(&mut self, key: &KeyEvent) -> KeyboardHookResult {
665        for entry in self.plugins.values_mut() {
666            if entry.info.state == PluginState::Active {
667                if let Some(ref mut hook) = entry.keyboard_hook {
668                    let result = hook.on_key_pressed(key);
669                    if result != KeyboardHookResult::Continue {
670                        return result;
671                    }
672                }
673            }
674        }
675        KeyboardHookResult::Continue
676    }
677
678    /// Dispatch keyboard hook: key combo.
679    pub fn on_key_combo(&mut self, combo: &KeyCombo) -> KeyboardHookResult {
680        for entry in self.plugins.values_mut() {
681            if entry.info.state == PluginState::Active {
682                if let Some(ref mut hook) = entry.keyboard_hook {
683                    let result = hook.on_key_combo(combo);
684                    if result != KeyboardHookResult::Continue {
685                        return result;
686                    }
687                }
688            }
689        }
690        KeyboardHookResult::Continue
691    }
692
693    /// Dispatch theme hook: theme changing.
694    pub fn on_theme_changing(&mut self, old_theme: Theme, new_theme: Theme) {
695        for entry in self.plugins.values_mut() {
696            if entry.info.state == PluginState::Active {
697                if let Some(ref mut hook) = entry.theme_hook {
698                    hook.before_theme_change(old_theme, new_theme);
699                }
700            }
701        }
702    }
703
704    /// Dispatch theme hook: theme changed.
705    pub fn on_theme_changed(&mut self, theme: Theme) {
706        for entry in self.plugins.values_mut() {
707            if entry.info.state == PluginState::Active {
708                // Notify the plugin trait method
709                entry.plugin.on_theme_changed(theme);
710                // Notify the theme hook
711                if let Some(ref mut hook) = entry.theme_hook {
712                    hook.after_theme_change(theme);
713                }
714            }
715        }
716    }
717
718    /// Dispatch pane hook: pane created.
719    pub fn on_pane_created(&mut self, pane_id: usize, pane_type: &str) {
720        for entry in self.plugins.values_mut() {
721            if entry.info.state == PluginState::Active {
722                if let Some(ref mut hook) = entry.pane_hook {
723                    hook.on_pane_created(pane_id, pane_type);
724                }
725            }
726        }
727    }
728
729    /// Dispatch pane hook: query changed.
730    pub fn on_query_changed(&mut self, pane_id: usize, query: &str) {
731        for entry in self.plugins.values_mut() {
732            if entry.info.state == PluginState::Active {
733                if let Some(ref mut hook) = entry.pane_hook {
734                    hook.on_query_changed(pane_id, query);
735                }
736            }
737        }
738    }
739
740    /// Dispatch pane hook: data received.
741    pub fn on_data_received(&mut self, pane_id: usize) {
742        for entry in self.plugins.values_mut() {
743            if entry.info.state == PluginState::Active {
744                if let Some(ref mut hook) = entry.pane_hook {
745                    hook.on_data_received(pane_id);
746                }
747            }
748        }
749    }
750
751    /// Dispatch pane hook: pane error.
752    pub fn on_pane_error(&mut self, pane_id: usize, error: &str) {
753        for entry in self.plugins.values_mut() {
754            if entry.info.state == PluginState::Active {
755                if let Some(ref mut hook) = entry.pane_hook {
756                    hook.on_pane_error(pane_id, error);
757                }
758            }
759        }
760    }
761
762    // ==================== Private Helpers ====================
763
764    /// Simple semver check (major.minor.patch).
765    fn check_version(current: &str, required: &str) -> bool {
766        let parse = |v: &str| -> (u32, u32, u32) {
767            let parts: Vec<&str> = v.split('.').collect();
768            (
769                parts.first().and_then(|s| s.parse().ok()).unwrap_or(0),
770                parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
771                parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
772            )
773        };
774
775        let curr = parse(current);
776        let req = parse(required);
777
778        // Current must be >= required
779        curr >= req
780    }
781}
782
783#[cfg(test)]
784mod tests {
785    use super::*;
786    use crate::types::{
787        BoxFuture, HttpError, HttpResponse, LogLevel, NotificationLevel, PluginHost,
788    };
789    use std::any::Any;
790    use std::sync::Arc;
791
792    /// Mock plugin host for testing.
793    struct MockPluginHost;
794
795    impl PluginHost for MockPluginHost {
796        fn notify(&self, _level: NotificationLevel, _message: &str) {}
797        fn request_repaint(&self) {}
798        fn log(&self, _level: LogLevel, _message: &str) {}
799        fn version(&self) -> &'static str {
800            "1.0.0"
801        }
802        fn is_wasm(&self) -> bool {
803            false
804        }
805        fn theme(&self) -> Theme {
806            Theme::Dark
807        }
808        fn theme_name(&self) -> &'static str {
809            "dark"
810        }
811        fn clipboard_write(&self, _text: &str) -> bool {
812            true
813        }
814        fn clipboard_read(&self) -> Option<String> {
815            None
816        }
817        fn spawn(&self, _future: BoxFuture<()>) {}
818        fn http_get(
819            &self,
820            _url: &str,
821            _headers: &rustc_hash::FxHashMap<String, String>,
822        ) -> Result<HttpResponse, HttpError> {
823            Err(HttpError {
824                message: "Not implemented".to_string(),
825            })
826        }
827        fn http_post(
828            &self,
829            _url: &str,
830            _body: &str,
831            _headers: &rustc_hash::FxHashMap<String, String>,
832        ) -> Result<HttpResponse, HttpError> {
833            Err(HttpError {
834                message: "Not implemented".to_string(),
835            })
836        }
837
838        // Pane management (no-op for mock)
839        fn add_query_pane(&self, _query: &str, _title: Option<&str>) {}
840        fn add_logs_pane(&self) {}
841        fn add_tracing_pane(&self, _trace_id: Option<&str>) {}
842        fn add_terminal_pane(&self) {}
843        fn add_sql_pane(&self) {}
844        fn close_focused_pane(&self) {}
845        fn focus_pane(&self, _direction: &str) {}
846
847        // Time range (no-op for mock)
848        fn set_time_range_preset(&self, _preset: &str) {}
849        fn set_time_range_absolute(&self, _start_secs: f64, _end_secs: f64) {}
850        fn get_time_range(&self) -> (f64, f64) {
851            (0.0, 0.0)
852        }
853
854        // Custom table panes (no-op for mock)
855        fn register_custom_table_pane(&self, _config: crate::CustomTableConfig) {}
856        fn add_custom_table_pane(&self, _pane_type: &str) {}
857        fn update_custom_table_data(&self, _pane_id: usize, _data: crate::CustomTableData) {}
858        fn update_custom_table_data_by_type(
859            &self,
860            _pane_type: &str,
861            _data: crate::CustomTableData,
862        ) {
863        }
864
865        // Custom chart panes (no-op for mock)
866        fn register_custom_chart_pane(&self, _config: crate::CustomChartConfig) {}
867        fn add_custom_chart_pane(&self, _pane_type: &str) {}
868        fn update_custom_chart_data_by_type(
869            &self,
870            _pane_type: &str,
871            _data: crate::CustomChartData,
872        ) {
873        }
874
875        // Custom stat panes (no-op for mock)
876        fn register_stat_pane(&self, _config: crate::StatPaneConfig) {}
877        fn add_stat_pane(&self, _pane_type: &str) {}
878        fn update_stat_data_by_type(&self, _pane_type: &str, _data: crate::StatPaneData) {}
879
880        // Custom gauge panes (no-op for mock)
881        fn register_gauge_pane(&self, _config: crate::GaugePaneConfig) {}
882        fn add_gauge_pane(&self, _pane_type: &str) {}
883        fn update_gauge_data_by_type(&self, _pane_type: &str, _data: crate::GaugePaneData) {}
884
885        // Focused pane info (no-op for mock)
886        fn get_focused_pane_info(&self) -> Option<crate::FocusedPaneInfo> {
887            None
888        }
889    }
890
891    /// Simple test plugin for testing registry operations.
892    struct TestPlugin {
893        name: &'static str,
894        version: &'static str,
895        min_version: Option<&'static str>,
896        commands: Vec<CommandConfig>,
897        executed_commands: std::sync::atomic::AtomicUsize,
898    }
899
900    impl TestPlugin {
901        fn new(name: &'static str) -> Self {
902            Self {
903                name,
904                version: "1.0.0",
905                min_version: None,
906                commands: vec![],
907                executed_commands: std::sync::atomic::AtomicUsize::new(0),
908            }
909        }
910
911        fn with_version(mut self, version: &'static str) -> Self {
912            self.version = version;
913            self
914        }
915
916        fn with_min_version(mut self, min_version: &'static str) -> Self {
917            self.min_version = Some(min_version);
918            self
919        }
920
921        fn with_command(mut self, name: &str) -> Self {
922            self.commands.push(CommandConfig {
923                name: name.to_string(),
924                aliases: vec![],
925                description: format!("Test command: {name}"),
926                accepts_args: false,
927            });
928            self
929        }
930    }
931
932    impl crate::traits::Plugin for TestPlugin {
933        fn name(&self) -> &'static str {
934            self.name
935        }
936
937        fn version(&self) -> &'static str {
938            self.version
939        }
940
941        fn description(&self) -> &'static str {
942            "A test plugin"
943        }
944
945        fn capabilities(&self) -> PluginCapabilities {
946            if self.commands.is_empty() {
947                PluginCapabilities::empty()
948            } else {
949                PluginCapabilities::COMMANDS
950            }
951        }
952
953        fn min_editor_version(&self) -> Option<&'static str> {
954            self.min_version
955        }
956
957        fn init(&mut self, _ctx: &PluginContext) -> crate::PluginResult<()> {
958            Ok(())
959        }
960
961        fn commands(&self) -> Vec<CommandConfig> {
962            self.commands.clone()
963        }
964
965        fn execute_command(&mut self, command: &str, _args: &str, _ctx: &PluginContext) -> bool {
966            if self.commands.iter().any(|c| c.name == command) {
967                self.executed_commands
968                    .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
969                true
970            } else {
971                false
972            }
973        }
974
975        fn as_any(&self) -> &dyn Any {
976            self
977        }
978
979        fn as_any_mut(&mut self) -> &mut dyn Any {
980            self
981        }
982    }
983
984    fn create_test_context() -> PluginContext {
985        PluginContext::new(Arc::new(MockPluginHost))
986    }
987
988    #[test]
989    fn test_registry_new() {
990        let registry = PluginRegistry::new();
991        assert!(registry.list_plugins().is_empty());
992        assert!(registry.context().is_none());
993    }
994
995    #[test]
996    fn test_registry_init() {
997        let mut registry = PluginRegistry::new();
998        let ctx = create_test_context();
999        registry.init(ctx);
1000        assert!(registry.context().is_some());
1001    }
1002
1003    #[test]
1004    fn test_register_plugin() {
1005        let mut registry = PluginRegistry::new();
1006        let plugin = TestPlugin::new("test-plugin").with_version("2.5.0");
1007
1008        let id = registry.register(plugin, true).unwrap();
1009        assert_eq!(id.value(), 0);
1010        assert_eq!(registry.list_plugins().len(), 1);
1011
1012        let info = registry.info(id).unwrap();
1013        assert_eq!(info.name, "test-plugin");
1014        assert_eq!(info.version, "2.5.0");
1015        assert_eq!(info.state, PluginState::Registered);
1016    }
1017
1018    #[test]
1019    fn test_register_duplicate_fails() {
1020        let mut registry = PluginRegistry::new();
1021        registry.register(TestPlugin::new("dupe"), true).unwrap();
1022
1023        let result = registry.register(TestPlugin::new("dupe"), true);
1024        assert!(matches!(result, Err(PluginError::AlreadyRegistered(_))));
1025    }
1026
1027    #[test]
1028    fn test_plugin_lifecycle() {
1029        let mut registry = PluginRegistry::new();
1030        registry.init(create_test_context());
1031
1032        let id = registry
1033            .register(TestPlugin::new("lifecycle"), true)
1034            .unwrap();
1035
1036        // After register: Registered state
1037        assert_eq!(registry.info(id).unwrap().state, PluginState::Registered);
1038
1039        // After init: Inactive state
1040        registry.init_plugin(id).unwrap();
1041        assert_eq!(registry.info(id).unwrap().state, PluginState::Inactive);
1042
1043        // After activate: Active state
1044        registry.activate_plugin(id).unwrap();
1045        assert_eq!(registry.info(id).unwrap().state, PluginState::Active);
1046
1047        // After deactivate: Inactive state
1048        registry.deactivate_plugin(id).unwrap();
1049        assert_eq!(registry.info(id).unwrap().state, PluginState::Inactive);
1050    }
1051
1052    #[test]
1053    fn test_active_plugins() {
1054        let mut registry = PluginRegistry::new();
1055        registry.init(create_test_context());
1056
1057        let id1 = registry
1058            .register(TestPlugin::new("plugin-1"), true)
1059            .unwrap();
1060        let id2 = registry
1061            .register(TestPlugin::new("plugin-2"), true)
1062            .unwrap();
1063
1064        // Neither active yet
1065        assert!(registry.active_plugins().is_empty());
1066
1067        // Activate first
1068        registry.init_plugin(id1).unwrap();
1069        registry.activate_plugin(id1).unwrap();
1070        assert_eq!(registry.active_plugins().len(), 1);
1071
1072        // Activate second
1073        registry.init_plugin(id2).unwrap();
1074        registry.activate_plugin(id2).unwrap();
1075        assert_eq!(registry.active_plugins().len(), 2);
1076    }
1077
1078    #[test]
1079    fn test_get_by_name() {
1080        let mut registry = PluginRegistry::new();
1081        registry
1082            .register(TestPlugin::new("named-plugin"), true)
1083            .unwrap();
1084
1085        assert!(registry.get_by_name("named-plugin").is_some());
1086        assert!(registry.get_by_name("nonexistent").is_none());
1087
1088        let info = registry.info_by_name("named-plugin").unwrap();
1089        assert_eq!(info.name, "named-plugin");
1090    }
1091
1092    #[test]
1093    fn test_version_check() {
1094        // Equal versions
1095        assert!(PluginRegistry::check_version("1.0.0", "1.0.0"));
1096
1097        // Current > required
1098        assert!(PluginRegistry::check_version("2.0.0", "1.0.0"));
1099        assert!(PluginRegistry::check_version("1.1.0", "1.0.0"));
1100        assert!(PluginRegistry::check_version("1.0.1", "1.0.0"));
1101
1102        // Current < required
1103        assert!(!PluginRegistry::check_version("1.0.0", "2.0.0"));
1104        assert!(!PluginRegistry::check_version("1.0.0", "1.1.0"));
1105        assert!(!PluginRegistry::check_version("1.0.0", "1.0.1"));
1106
1107        // Partial versions
1108        assert!(PluginRegistry::check_version("1.0", "1.0.0"));
1109        assert!(PluginRegistry::check_version("1", "1.0.0"));
1110    }
1111
1112    #[test]
1113    fn test_min_version_enforcement() {
1114        let mut registry = PluginRegistry::new();
1115        registry.init(create_test_context()); // Host version is "1.0.0"
1116
1117        // Plugin requires 2.0.0 but host is 1.0.0
1118        let id = registry
1119            .register(
1120                TestPlugin::new("future-plugin").with_min_version("2.0.0"),
1121                true,
1122            )
1123            .unwrap();
1124
1125        let result = registry.init_plugin(id);
1126        assert!(matches!(
1127            result,
1128            Err(PluginError::IncompatibleVersion { .. })
1129        ));
1130        assert_eq!(registry.info(id).unwrap().state, PluginState::Failed);
1131    }
1132
1133    #[test]
1134    fn test_command_collection() {
1135        let mut registry = PluginRegistry::new();
1136        registry.init(create_test_context());
1137
1138        let id = registry
1139            .register(
1140                TestPlugin::new("cmd-plugin")
1141                    .with_command("cmd-1")
1142                    .with_command("cmd-2"),
1143                true,
1144            )
1145            .unwrap();
1146
1147        registry.init_plugin(id).unwrap();
1148        registry.activate_plugin(id).unwrap();
1149
1150        let commands = registry.all_commands();
1151        assert_eq!(commands.len(), 2);
1152
1153        let plugin_cmds = registry.commands_for_plugin(id);
1154        assert_eq!(plugin_cmds.len(), 2);
1155    }
1156
1157    #[test]
1158    fn test_execute_command() {
1159        let mut registry = PluginRegistry::new();
1160        registry.init(create_test_context());
1161
1162        let id = registry
1163            .register(TestPlugin::new("exec-plugin").with_command("my-cmd"), true)
1164            .unwrap();
1165
1166        registry.init_plugin(id).unwrap();
1167        registry.activate_plugin(id).unwrap();
1168
1169        // Execute existing command
1170        assert!(registry.execute_command("my-cmd", ""));
1171
1172        // Execute non-existent command
1173        assert!(!registry.execute_command("nonexistent", ""));
1174    }
1175
1176    #[test]
1177    fn test_disabled_plugin() {
1178        let mut registry = PluginRegistry::new();
1179        registry.init(create_test_context());
1180
1181        // Disable the plugin before activation
1182        registry.set_plugin_enabled("disabled-plugin", false);
1183
1184        let id = registry
1185            .register(TestPlugin::new("disabled-plugin"), true)
1186            .unwrap();
1187
1188        registry.init_plugin(id).unwrap();
1189        registry.activate_plugin(id).unwrap();
1190
1191        // Plugin should be in Disabled state, not Active
1192        assert_eq!(registry.info(id).unwrap().state, PluginState::Disabled);
1193        assert!(registry.active_plugins().is_empty());
1194    }
1195
1196    #[test]
1197    fn test_plugin_enabled_check() {
1198        let mut registry = PluginRegistry::new();
1199
1200        // Unknown plugin defaults to enabled
1201        assert!(registry.is_plugin_enabled("unknown"));
1202
1203        // Explicitly disabled
1204        registry.set_plugin_enabled("my-plugin", false);
1205        assert!(!registry.is_plugin_enabled("my-plugin"));
1206
1207        // Explicitly enabled
1208        registry.set_plugin_enabled("my-plugin", true);
1209        assert!(registry.is_plugin_enabled("my-plugin"));
1210    }
1211}