1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct PluginId(usize);
17
18impl PluginId {
19 pub fn value(&self) -> usize {
21 self.0
22 }
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum PluginState {
28 Registered,
30 Inactive,
32 Active,
34 Failed,
36 Disabled,
38}
39
40#[derive(Debug, Clone)]
42pub struct PluginInfo {
43 pub id: PluginId,
45 pub name: String,
47 pub version: String,
49 pub description: String,
51 pub capabilities: PluginCapabilities,
53 pub state: PluginState,
55 pub enabled_by_default: bool,
57}
58
59struct PluginEntry {
61 plugin: Box<dyn Plugin>,
63 info: PluginInfo,
65 commands: Vec<CommandConfig>,
67 pane_types: Vec<PaneConfig>,
69 keybindings: Vec<KeybindingConfig>,
71 lifecycle_hook: Option<Box<dyn LifecycleHook>>,
73 command_hook: Option<Box<dyn CommandHook>>,
75 keyboard_hook: Option<Box<dyn KeyboardHook>>,
77 theme_hook: Option<Box<dyn ThemeHook>>,
79 pane_hook: Option<Box<dyn PaneHook>>,
81}
82
83pub struct PluginRegistry {
91 plugins: FxHashMap<PluginId, PluginEntry>,
93 name_to_id: FxHashMap<String, PluginId>,
95 command_to_plugin: FxHashMap<String, PluginId>,
97 next_id: usize,
99 context: Option<PluginContext>,
101 enabled_plugins: FxHashMap<String, bool>,
103}
104
105impl Default for PluginRegistry {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl PluginRegistry {
112 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 pub fn init(&mut self, context: PluginContext) {
126 self.context = Some(context);
127 }
128
129 pub fn context(&self) -> Option<&PluginContext> {
131 self.context.as_ref()
132 }
133
134 pub fn set_plugin_enabled(&mut self, name: &str, enabled: bool) {
136 self.enabled_plugins.insert(name.to_string(), enabled);
137 }
138
139 pub fn is_plugin_enabled(&self, name: &str) -> bool {
141 self.enabled_plugins.get(name).copied().unwrap_or(true)
142 }
143
144 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 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(()); }
206
207 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 if let Err(e) = entry.plugin.init(ctx) {
221 entry.info.state = PluginState::Failed;
222 return Err(e);
223 }
224
225 entry.commands = entry.plugin.commands();
227 entry.pane_types = entry.plugin.pane_types();
228 entry.keybindings = entry.plugin.keybindings();
229
230 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 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(()); }
256
257 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 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 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(()); }
301
302 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 pub fn unregister_plugin(&mut self, id: PluginId) -> PluginResult<()> {
324 let _ = self.deactivate_plugin(id);
326
327 let entry = self
329 .plugins
330 .remove(&id)
331 .ok_or_else(|| PluginError::NotFound(format!("Plugin ID {}", id.0)))?;
332
333 self.name_to_id.remove(&entry.info.name);
335
336 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 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 pub fn get(&self, id: PluginId) -> Option<&dyn Plugin> {
360 self.plugins.get(&id).map(|e| e.plugin.as_ref())
361 }
362
363 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 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 pub fn info(&self, id: PluginId) -> Option<&PluginInfo> {
378 self.plugins.get(&id).map(|e| &e.info)
379 }
380
381 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 pub fn list_plugins(&self) -> Vec<&PluginInfo> {
391 self.plugins.values().map(|e| &e.info).collect()
392 }
393
394 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 entry.plugin.on_theme_changed(theme);
710 if let Some(ref mut hook) = entry.theme_hook {
712 hook.after_theme_change(theme);
713 }
714 }
715 }
716 }
717
718 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 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 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 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 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 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 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 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 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 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 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 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 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 fn get_focused_pane_info(&self) -> Option<crate::FocusedPaneInfo> {
887 None
888 }
889 }
890
891 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 assert_eq!(registry.info(id).unwrap().state, PluginState::Registered);
1038
1039 registry.init_plugin(id).unwrap();
1041 assert_eq!(registry.info(id).unwrap().state, PluginState::Inactive);
1042
1043 registry.activate_plugin(id).unwrap();
1045 assert_eq!(registry.info(id).unwrap().state, PluginState::Active);
1046
1047 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 assert!(registry.active_plugins().is_empty());
1066
1067 registry.init_plugin(id1).unwrap();
1069 registry.activate_plugin(id1).unwrap();
1070 assert_eq!(registry.active_plugins().len(), 1);
1071
1072 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 assert!(PluginRegistry::check_version("1.0.0", "1.0.0"));
1096
1097 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 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 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()); 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 assert!(registry.execute_command("my-cmd", ""));
1171
1172 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 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 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 assert!(registry.is_plugin_enabled("unknown"));
1202
1203 registry.set_plugin_enabled("my-plugin", false);
1205 assert!(!registry.is_plugin_enabled("my-plugin"));
1206
1207 registry.set_plugin_enabled("my-plugin", true);
1209 assert!(registry.is_plugin_enabled("my-plugin"));
1210 }
1211}