use rustc_hash::FxHashMap;
use crate::hooks::{
CommandHook, CommandHookResult, KeyCombo, KeyEvent, KeyboardHook, KeyboardHookResult,
LifecycleHook, PaneHook, ThemeHook,
};
use crate::theme::ThemeDefinition;
use crate::traits::{CommandConfig, KeybindingConfig, PaneConfig, Plugin, PluginCapabilities};
use crate::types::{PluginContext, Theme};
use crate::{PluginError, PluginResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PluginId(usize);
impl PluginId {
pub fn value(&self) -> usize {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PluginState {
Registered,
Inactive,
Active,
Failed,
Disabled,
}
#[derive(Debug, Clone)]
pub struct PluginInfo {
pub id: PluginId,
pub name: String,
pub version: String,
pub description: String,
pub capabilities: PluginCapabilities,
pub state: PluginState,
pub enabled_by_default: bool,
}
struct PluginEntry {
plugin: Box<dyn Plugin>,
info: PluginInfo,
commands: Vec<CommandConfig>,
pane_types: Vec<PaneConfig>,
keybindings: Vec<KeybindingConfig>,
lifecycle_hook: Option<Box<dyn LifecycleHook>>,
command_hook: Option<Box<dyn CommandHook>>,
keyboard_hook: Option<Box<dyn KeyboardHook>>,
theme_hook: Option<Box<dyn ThemeHook>>,
pane_hook: Option<Box<dyn PaneHook>>,
}
pub struct PluginRegistry {
plugins: FxHashMap<PluginId, PluginEntry>,
name_to_id: FxHashMap<String, PluginId>,
command_to_plugin: FxHashMap<String, PluginId>,
next_id: usize,
context: Option<PluginContext>,
enabled_plugins: FxHashMap<String, bool>,
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: FxHashMap::default(),
name_to_id: FxHashMap::default(),
command_to_plugin: FxHashMap::default(),
next_id: 0,
context: None,
enabled_plugins: FxHashMap::default(),
}
}
pub fn init(&mut self, context: PluginContext) {
self.context = Some(context);
}
pub fn context(&self) -> Option<&PluginContext> {
self.context.as_ref()
}
pub fn set_plugin_enabled(&mut self, name: &str, enabled: bool) {
self.enabled_plugins.insert(name.to_string(), enabled);
}
pub fn is_plugin_enabled(&self, name: &str) -> bool {
self.enabled_plugins.get(name).copied().unwrap_or(true)
}
pub fn register<P: Plugin + 'static>(
&mut self,
plugin: P,
enabled_by_default: bool,
) -> PluginResult<PluginId> {
let name = plugin.name().to_string();
if self.name_to_id.contains_key(&name) {
return Err(PluginError::AlreadyRegistered(name));
}
let id = PluginId(self.next_id);
self.next_id += 1;
let info = PluginInfo {
id,
name: name.clone(),
version: plugin.version().to_string(),
description: plugin.description().to_string(),
capabilities: plugin.capabilities(),
state: PluginState::Registered,
enabled_by_default,
};
let entry = PluginEntry {
plugin: Box::new(plugin),
info,
commands: vec![],
pane_types: vec![],
keybindings: vec![],
lifecycle_hook: None,
command_hook: None,
keyboard_hook: None,
theme_hook: None,
pane_hook: None,
};
self.plugins.insert(id, entry);
self.name_to_id.insert(name, id);
Ok(id)
}
pub fn init_plugin(&mut self, id: PluginId) -> PluginResult<()> {
let ctx = self
.context
.as_ref()
.ok_or_else(|| PluginError::OperationFailed("Registry not initialized".to_string()))?;
let entry = self
.plugins
.get_mut(&id)
.ok_or_else(|| PluginError::NotFound(format!("Plugin ID {}", id.0)))?;
if entry.info.state != PluginState::Registered {
return Ok(()); }
if let Some(min_version) = entry.plugin.min_editor_version() {
let current = ctx.editor_version();
if !Self::check_version(current, min_version) {
entry.info.state = PluginState::Failed;
return Err(PluginError::IncompatibleVersion {
required: min_version.to_string(),
actual: current.to_string(),
});
}
}
if let Err(e) = entry.plugin.init(ctx) {
entry.info.state = PluginState::Failed;
return Err(e);
}
entry.commands = entry.plugin.commands();
entry.pane_types = entry.plugin.pane_types();
entry.keybindings = entry.plugin.keybindings();
entry.lifecycle_hook = entry.plugin.lifecycle_hooks();
entry.command_hook = entry.plugin.command_hooks();
entry.keyboard_hook = entry.plugin.keyboard_hooks();
entry.theme_hook = entry.plugin.theme_hooks();
entry.pane_hook = entry.plugin.pane_hooks();
entry.info.state = PluginState::Inactive;
Ok(())
}
pub fn activate_plugin(&mut self, id: PluginId) -> PluginResult<()> {
let ctx = self
.context
.as_ref()
.ok_or_else(|| PluginError::OperationFailed("Registry not initialized".to_string()))?;
let entry = self
.plugins
.get_mut(&id)
.ok_or_else(|| PluginError::NotFound(format!("Plugin ID {}", id.0)))?;
if entry.info.state != PluginState::Inactive {
return Ok(()); }
if !self
.enabled_plugins
.get(&entry.info.name)
.copied()
.unwrap_or(entry.info.enabled_by_default)
{
entry.info.state = PluginState::Disabled;
return Ok(());
}
if let Err(e) = entry.plugin.activate(ctx) {
entry.info.state = PluginState::Failed;
return Err(e);
}
entry.info.state = PluginState::Active;
for cmd in &entry.commands {
self.command_to_plugin.insert(cmd.name.clone(), id);
for alias in &cmd.aliases {
self.command_to_plugin.insert(alias.clone(), id);
}
}
Ok(())
}
pub fn deactivate_plugin(&mut self, id: PluginId) -> PluginResult<()> {
let ctx = self
.context
.as_ref()
.ok_or_else(|| PluginError::OperationFailed("Registry not initialized".to_string()))?;
let entry = self
.plugins
.get_mut(&id)
.ok_or_else(|| PluginError::NotFound(format!("Plugin ID {}", id.0)))?;
if entry.info.state != PluginState::Active {
return Ok(()); }
for cmd in &entry.commands {
self.command_to_plugin.remove(&cmd.name);
for alias in &cmd.aliases {
self.command_to_plugin.remove(alias);
}
}
if let Err(e) = entry.plugin.deactivate(ctx) {
log::warn!("Plugin {} deactivation error: {e}", entry.info.name);
}
entry.info.state = PluginState::Inactive;
Ok(())
}
pub fn unregister_plugin(&mut self, id: PluginId) -> PluginResult<()> {
let _ = self.deactivate_plugin(id);
let entry = self
.plugins
.remove(&id)
.ok_or_else(|| PluginError::NotFound(format!("Plugin ID {}", id.0)))?;
self.name_to_id.remove(&entry.info.name);
for cmd in &entry.commands {
self.command_to_plugin.remove(&cmd.name);
for alias in &cmd.aliases {
self.command_to_plugin.remove(alias);
}
}
log::info!("Unregistered plugin: {}", entry.info.name);
Ok(())
}
pub fn unregister_plugin_by_name(&mut self, name: &str) -> PluginResult<()> {
let id = self
.name_to_id
.get(name)
.copied()
.ok_or_else(|| PluginError::NotFound(format!("Plugin '{name}'")))?;
self.unregister_plugin(id)
}
pub fn get(&self, id: PluginId) -> Option<&dyn Plugin> {
self.plugins.get(&id).map(|e| e.plugin.as_ref())
}
pub fn get_mut(&mut self, id: PluginId) -> Option<&mut dyn Plugin> {
self.plugins.get_mut(&id).map(|e| e.plugin.as_mut())
}
pub fn get_by_name(&self, name: &str) -> Option<&dyn Plugin> {
self.name_to_id
.get(name)
.and_then(|id| self.plugins.get(id))
.map(|e| e.plugin.as_ref())
}
pub fn info(&self, id: PluginId) -> Option<&PluginInfo> {
self.plugins.get(&id).map(|e| &e.info)
}
pub fn info_by_name(&self, name: &str) -> Option<&PluginInfo> {
self.name_to_id
.get(name)
.and_then(|id| self.plugins.get(id))
.map(|e| &e.info)
}
pub fn list_plugins(&self) -> Vec<&PluginInfo> {
self.plugins.values().map(|e| &e.info).collect()
}
pub fn active_plugins(&self) -> Vec<&PluginInfo> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.map(|e| &e.info)
.collect()
}
pub fn all_commands(&self) -> Vec<(&PluginInfo, &CommandConfig)> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.flat_map(|e| e.commands.iter().map(move |c| (&e.info, c)))
.collect()
}
pub fn all_pane_types(&self) -> Vec<(&PluginInfo, &PaneConfig)> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.flat_map(|e| e.pane_types.iter().map(move |p| (&e.info, p)))
.collect()
}
pub fn all_keybindings(&self) -> Vec<(&PluginInfo, &KeybindingConfig)> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.flat_map(|e| e.keybindings.iter().map(move |k| (&e.info, k)))
.collect()
}
pub fn all_themes(&self) -> Vec<ThemeDefinition> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.flat_map(|e| e.plugin.themes())
.collect()
}
pub fn all_custom_table_panes(&self) -> Vec<crate::CustomTableConfig> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.flat_map(|e| e.plugin.custom_table_panes())
.collect()
}
pub fn all_custom_chart_panes(&self) -> Vec<crate::CustomChartConfig> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.flat_map(|e| e.plugin.custom_chart_panes())
.collect()
}
pub fn all_custom_stat_panes(&self) -> Vec<crate::StatPaneConfig> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.flat_map(|e| e.plugin.custom_stat_panes())
.collect()
}
pub fn all_custom_gauge_panes(&self) -> Vec<crate::GaugePaneConfig> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.flat_map(|e| e.plugin.custom_gauge_panes())
.collect()
}
pub fn all_refreshable_pane_types(&self) -> Vec<(String, u32)> {
self.plugins
.values()
.filter(|e| e.info.state == PluginState::Active)
.flat_map(|e| {
e.plugin
.refreshable_pane_types()
.into_iter()
.map(|(name, interval)| (name.to_string(), interval))
})
.collect()
}
pub fn trigger_pane_refresh(&mut self, pane_type: &str) -> bool {
let ctx = match &self.context {
Some(c) => c,
None => return false,
};
for entry in self.plugins.values_mut() {
if entry.info.state != PluginState::Active {
continue;
}
let has_pane_type = entry
.plugin
.refreshable_pane_types()
.iter()
.any(|(name, _)| *name == pane_type);
if has_pane_type {
return entry.plugin.trigger_pane_refresh(pane_type, ctx);
}
}
false
}
pub fn commands_for_plugin(&self, id: PluginId) -> Vec<&CommandConfig> {
self.plugins
.get(&id)
.map(|e| e.commands.iter().collect())
.unwrap_or_default()
}
pub fn keybindings_for_plugin(&self, id: PluginId) -> Vec<&KeybindingConfig> {
self.plugins
.get(&id)
.map(|e| e.keybindings.iter().collect())
.unwrap_or_default()
}
pub fn execute_command(&mut self, command: &str, args: &str) -> bool {
let ctx = match &self.context {
Some(c) => c,
None => return false,
};
let plugin_id = match self.command_to_plugin.get(command) {
Some(&id) => id,
None => return false,
};
let entry = match self.plugins.get_mut(&plugin_id) {
Some(e) if e.info.state == PluginState::Active => e,
_ => return false,
};
entry.plugin.execute_command(command, args, ctx)
}
pub fn on_workspace_loaded(&mut self) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.lifecycle_hook {
hook.on_workspace_loaded();
}
}
}
}
pub fn on_workspace_saving(&mut self) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.lifecycle_hook {
hook.on_workspace_saving();
}
}
}
}
pub fn on_pane_added(&mut self, pane_id: usize) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.lifecycle_hook {
hook.on_pane_added(pane_id);
}
}
}
}
pub fn on_pane_removing(&mut self, pane_id: usize) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.lifecycle_hook {
hook.on_pane_removing(pane_id);
}
}
}
}
pub fn on_pane_focused(&mut self, pane_id: usize) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.lifecycle_hook {
hook.on_pane_focused(pane_id);
}
}
}
}
pub fn on_closing(&mut self) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.lifecycle_hook {
hook.on_closing();
}
}
}
}
pub fn on_frame(&mut self) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.lifecycle_hook {
hook.on_frame();
}
}
}
}
pub fn before_command(&mut self, command: &str, args: &str) -> CommandHookResult {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.command_hook {
let result = hook.before_command(command, args);
if result != CommandHookResult::Continue {
return result;
}
}
}
}
CommandHookResult::Continue
}
pub fn after_command(&mut self, command: &str, args: &str, success: bool) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.command_hook {
hook.after_command(command, args, success);
}
}
}
}
pub fn on_key_pressed(&mut self, key: &KeyEvent) -> KeyboardHookResult {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.keyboard_hook {
let result = hook.on_key_pressed(key);
if result != KeyboardHookResult::Continue {
return result;
}
}
}
}
KeyboardHookResult::Continue
}
pub fn on_key_combo(&mut self, combo: &KeyCombo) -> KeyboardHookResult {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.keyboard_hook {
let result = hook.on_key_combo(combo);
if result != KeyboardHookResult::Continue {
return result;
}
}
}
}
KeyboardHookResult::Continue
}
pub fn on_theme_changing(&mut self, old_theme: Theme, new_theme: Theme) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.theme_hook {
hook.before_theme_change(old_theme, new_theme);
}
}
}
}
pub fn on_theme_changed(&mut self, theme: Theme) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
entry.plugin.on_theme_changed(theme);
if let Some(ref mut hook) = entry.theme_hook {
hook.after_theme_change(theme);
}
}
}
}
pub fn on_pane_created(&mut self, pane_id: usize, pane_type: &str) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.pane_hook {
hook.on_pane_created(pane_id, pane_type);
}
}
}
}
pub fn on_query_changed(&mut self, pane_id: usize, query: &str) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.pane_hook {
hook.on_query_changed(pane_id, query);
}
}
}
}
pub fn on_data_received(&mut self, pane_id: usize) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.pane_hook {
hook.on_data_received(pane_id);
}
}
}
}
pub fn on_pane_error(&mut self, pane_id: usize, error: &str) {
for entry in self.plugins.values_mut() {
if entry.info.state == PluginState::Active {
if let Some(ref mut hook) = entry.pane_hook {
hook.on_pane_error(pane_id, error);
}
}
}
}
fn check_version(current: &str, required: &str) -> bool {
let parse = |v: &str| -> (u32, u32, u32) {
let parts: Vec<&str> = v.split('.').collect();
(
parts.first().and_then(|s| s.parse().ok()).unwrap_or(0),
parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0),
parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
)
};
let curr = parse(current);
let req = parse(required);
curr >= req
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{
BoxFuture, HttpError, HttpResponse, LogLevel, NotificationLevel, PluginHost,
};
use std::any::Any;
use std::sync::Arc;
struct MockPluginHost;
impl PluginHost for MockPluginHost {
fn notify(&self, _level: NotificationLevel, _message: &str) {}
fn request_repaint(&self) {}
fn log(&self, _level: LogLevel, _message: &str) {}
fn version(&self) -> &'static str {
"1.0.0"
}
fn is_wasm(&self) -> bool {
false
}
fn theme(&self) -> Theme {
Theme::Dark
}
fn theme_name(&self) -> &'static str {
"dark"
}
fn clipboard_write(&self, _text: &str) -> bool {
true
}
fn clipboard_read(&self) -> Option<String> {
None
}
fn spawn(&self, _future: BoxFuture<()>) {}
fn http_get(
&self,
_url: &str,
_headers: &rustc_hash::FxHashMap<String, String>,
) -> Result<HttpResponse, HttpError> {
Err(HttpError {
message: "Not implemented".to_string(),
})
}
fn http_post(
&self,
_url: &str,
_body: &str,
_headers: &rustc_hash::FxHashMap<String, String>,
) -> Result<HttpResponse, HttpError> {
Err(HttpError {
message: "Not implemented".to_string(),
})
}
fn add_query_pane(&self, _query: &str, _title: Option<&str>) {}
fn add_logs_pane(&self) {}
fn add_tracing_pane(&self, _trace_id: Option<&str>) {}
fn add_terminal_pane(&self) {}
fn add_sql_pane(&self) {}
fn close_focused_pane(&self) {}
fn focus_pane(&self, _direction: &str) {}
fn set_time_range_preset(&self, _preset: &str) {}
fn set_time_range_absolute(&self, _start_secs: f64, _end_secs: f64) {}
fn get_time_range(&self) -> (f64, f64) {
(0.0, 0.0)
}
fn register_custom_table_pane(&self, _config: crate::CustomTableConfig) {}
fn add_custom_table_pane(&self, _pane_type: &str) {}
fn update_custom_table_data(&self, _pane_id: usize, _data: crate::CustomTableData) {}
fn update_custom_table_data_by_type(
&self,
_pane_type: &str,
_data: crate::CustomTableData,
) {
}
fn register_custom_chart_pane(&self, _config: crate::CustomChartConfig) {}
fn add_custom_chart_pane(&self, _pane_type: &str) {}
fn update_custom_chart_data_by_type(
&self,
_pane_type: &str,
_data: crate::CustomChartData,
) {
}
fn register_stat_pane(&self, _config: crate::StatPaneConfig) {}
fn add_stat_pane(&self, _pane_type: &str) {}
fn update_stat_data_by_type(&self, _pane_type: &str, _data: crate::StatPaneData) {}
fn register_gauge_pane(&self, _config: crate::GaugePaneConfig) {}
fn add_gauge_pane(&self, _pane_type: &str) {}
fn update_gauge_data_by_type(&self, _pane_type: &str, _data: crate::GaugePaneData) {}
fn get_focused_pane_info(&self) -> Option<crate::FocusedPaneInfo> {
None
}
}
struct TestPlugin {
name: &'static str,
version: &'static str,
min_version: Option<&'static str>,
commands: Vec<CommandConfig>,
executed_commands: std::sync::atomic::AtomicUsize,
}
impl TestPlugin {
fn new(name: &'static str) -> Self {
Self {
name,
version: "1.0.0",
min_version: None,
commands: vec![],
executed_commands: std::sync::atomic::AtomicUsize::new(0),
}
}
fn with_version(mut self, version: &'static str) -> Self {
self.version = version;
self
}
fn with_min_version(mut self, min_version: &'static str) -> Self {
self.min_version = Some(min_version);
self
}
fn with_command(mut self, name: &str) -> Self {
self.commands.push(CommandConfig {
name: name.to_string(),
aliases: vec![],
description: format!("Test command: {name}"),
accepts_args: false,
});
self
}
}
impl crate::traits::Plugin for TestPlugin {
fn name(&self) -> &'static str {
self.name
}
fn version(&self) -> &'static str {
self.version
}
fn description(&self) -> &'static str {
"A test plugin"
}
fn capabilities(&self) -> PluginCapabilities {
if self.commands.is_empty() {
PluginCapabilities::empty()
} else {
PluginCapabilities::COMMANDS
}
}
fn min_editor_version(&self) -> Option<&'static str> {
self.min_version
}
fn init(&mut self, _ctx: &PluginContext) -> crate::PluginResult<()> {
Ok(())
}
fn commands(&self) -> Vec<CommandConfig> {
self.commands.clone()
}
fn execute_command(&mut self, command: &str, _args: &str, _ctx: &PluginContext) -> bool {
if self.commands.iter().any(|c| c.name == command) {
self.executed_commands
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
true
} else {
false
}
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
fn create_test_context() -> PluginContext {
PluginContext::new(Arc::new(MockPluginHost))
}
#[test]
fn test_registry_new() {
let registry = PluginRegistry::new();
assert!(registry.list_plugins().is_empty());
assert!(registry.context().is_none());
}
#[test]
fn test_registry_init() {
let mut registry = PluginRegistry::new();
let ctx = create_test_context();
registry.init(ctx);
assert!(registry.context().is_some());
}
#[test]
fn test_register_plugin() {
let mut registry = PluginRegistry::new();
let plugin = TestPlugin::new("test-plugin").with_version("2.5.0");
let id = registry.register(plugin, true).unwrap();
assert_eq!(id.value(), 0);
assert_eq!(registry.list_plugins().len(), 1);
let info = registry.info(id).unwrap();
assert_eq!(info.name, "test-plugin");
assert_eq!(info.version, "2.5.0");
assert_eq!(info.state, PluginState::Registered);
}
#[test]
fn test_register_duplicate_fails() {
let mut registry = PluginRegistry::new();
registry.register(TestPlugin::new("dupe"), true).unwrap();
let result = registry.register(TestPlugin::new("dupe"), true);
assert!(matches!(result, Err(PluginError::AlreadyRegistered(_))));
}
#[test]
fn test_plugin_lifecycle() {
let mut registry = PluginRegistry::new();
registry.init(create_test_context());
let id = registry
.register(TestPlugin::new("lifecycle"), true)
.unwrap();
assert_eq!(registry.info(id).unwrap().state, PluginState::Registered);
registry.init_plugin(id).unwrap();
assert_eq!(registry.info(id).unwrap().state, PluginState::Inactive);
registry.activate_plugin(id).unwrap();
assert_eq!(registry.info(id).unwrap().state, PluginState::Active);
registry.deactivate_plugin(id).unwrap();
assert_eq!(registry.info(id).unwrap().state, PluginState::Inactive);
}
#[test]
fn test_active_plugins() {
let mut registry = PluginRegistry::new();
registry.init(create_test_context());
let id1 = registry
.register(TestPlugin::new("plugin-1"), true)
.unwrap();
let id2 = registry
.register(TestPlugin::new("plugin-2"), true)
.unwrap();
assert!(registry.active_plugins().is_empty());
registry.init_plugin(id1).unwrap();
registry.activate_plugin(id1).unwrap();
assert_eq!(registry.active_plugins().len(), 1);
registry.init_plugin(id2).unwrap();
registry.activate_plugin(id2).unwrap();
assert_eq!(registry.active_plugins().len(), 2);
}
#[test]
fn test_get_by_name() {
let mut registry = PluginRegistry::new();
registry
.register(TestPlugin::new("named-plugin"), true)
.unwrap();
assert!(registry.get_by_name("named-plugin").is_some());
assert!(registry.get_by_name("nonexistent").is_none());
let info = registry.info_by_name("named-plugin").unwrap();
assert_eq!(info.name, "named-plugin");
}
#[test]
fn test_version_check() {
assert!(PluginRegistry::check_version("1.0.0", "1.0.0"));
assert!(PluginRegistry::check_version("2.0.0", "1.0.0"));
assert!(PluginRegistry::check_version("1.1.0", "1.0.0"));
assert!(PluginRegistry::check_version("1.0.1", "1.0.0"));
assert!(!PluginRegistry::check_version("1.0.0", "2.0.0"));
assert!(!PluginRegistry::check_version("1.0.0", "1.1.0"));
assert!(!PluginRegistry::check_version("1.0.0", "1.0.1"));
assert!(PluginRegistry::check_version("1.0", "1.0.0"));
assert!(PluginRegistry::check_version("1", "1.0.0"));
}
#[test]
fn test_min_version_enforcement() {
let mut registry = PluginRegistry::new();
registry.init(create_test_context());
let id = registry
.register(
TestPlugin::new("future-plugin").with_min_version("2.0.0"),
true,
)
.unwrap();
let result = registry.init_plugin(id);
assert!(matches!(
result,
Err(PluginError::IncompatibleVersion { .. })
));
assert_eq!(registry.info(id).unwrap().state, PluginState::Failed);
}
#[test]
fn test_command_collection() {
let mut registry = PluginRegistry::new();
registry.init(create_test_context());
let id = registry
.register(
TestPlugin::new("cmd-plugin")
.with_command("cmd-1")
.with_command("cmd-2"),
true,
)
.unwrap();
registry.init_plugin(id).unwrap();
registry.activate_plugin(id).unwrap();
let commands = registry.all_commands();
assert_eq!(commands.len(), 2);
let plugin_cmds = registry.commands_for_plugin(id);
assert_eq!(plugin_cmds.len(), 2);
}
#[test]
fn test_execute_command() {
let mut registry = PluginRegistry::new();
registry.init(create_test_context());
let id = registry
.register(TestPlugin::new("exec-plugin").with_command("my-cmd"), true)
.unwrap();
registry.init_plugin(id).unwrap();
registry.activate_plugin(id).unwrap();
assert!(registry.execute_command("my-cmd", ""));
assert!(!registry.execute_command("nonexistent", ""));
}
#[test]
fn test_disabled_plugin() {
let mut registry = PluginRegistry::new();
registry.init(create_test_context());
registry.set_plugin_enabled("disabled-plugin", false);
let id = registry
.register(TestPlugin::new("disabled-plugin"), true)
.unwrap();
registry.init_plugin(id).unwrap();
registry.activate_plugin(id).unwrap();
assert_eq!(registry.info(id).unwrap().state, PluginState::Disabled);
assert!(registry.active_plugins().is_empty());
}
#[test]
fn test_plugin_enabled_check() {
let mut registry = PluginRegistry::new();
assert!(registry.is_plugin_enabled("unknown"));
registry.set_plugin_enabled("my-plugin", false);
assert!(!registry.is_plugin_enabled("my-plugin"));
registry.set_plugin_enabled("my-plugin", true);
assert!(registry.is_plugin_enabled("my-plugin"));
}
}