use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde_json::Value as JsonValue;
use super::events::*;
use super::manifest::{PluginManifest, UiContribution};
use super::{
FilePlugin, Plugin, PluginContext, PluginError, PluginHealth, PluginId, WorkspacePlugin,
};
struct PluginHealthTracker {
health: HashMap<PluginId, PluginHealth>,
}
pub struct PluginRegistry {
plugins: Vec<Arc<dyn Plugin>>,
workspace_plugins: Vec<Arc<dyn WorkspacePlugin>>,
file_plugins: Vec<Arc<dyn FilePlugin>>,
health: Mutex<PluginHealthTracker>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: Vec::new(),
workspace_plugins: Vec::new(),
file_plugins: Vec::new(),
health: Mutex::new(PluginHealthTracker {
health: HashMap::new(),
}),
}
}
pub fn register_workspace_plugin(&mut self, plugin: Arc<dyn WorkspacePlugin>) {
if !self.plugins.iter().any(|p| p.id() == plugin.id()) {
self.plugins.push(plugin.clone());
}
self.workspace_plugins.push(plugin);
}
pub fn register_file_plugin(&mut self, plugin: Arc<dyn FilePlugin>) {
if !self.plugins.iter().any(|p| p.id() == plugin.id()) {
self.plugins.push(plugin.clone());
}
self.file_plugins.push(plugin);
}
pub fn plugin_ids(&self) -> Vec<PluginId> {
self.plugins.iter().map(|p| p.id()).collect()
}
pub fn workspace_plugins(&self) -> &[Arc<dyn WorkspacePlugin>] {
&self.workspace_plugins
}
fn set_health(&self, id: PluginId, health: PluginHealth) {
if let Ok(mut tracker) = self.health.lock() {
tracker.health.insert(id, health);
}
}
fn is_plugin_healthy(&self, id: &PluginId) -> bool {
if let Ok(tracker) = self.health.lock() {
!matches!(tracker.health.get(id), Some(PluginHealth::Failed(_)))
} else {
true
}
}
pub fn get_plugin_health(&self, plugin_id: &PluginId) -> PluginHealth {
if let Ok(tracker) = self.health.lock() {
tracker
.health
.get(plugin_id)
.cloned()
.unwrap_or(PluginHealth::Healthy)
} else {
PluginHealth::Healthy
}
}
pub fn get_all_plugin_health(&self) -> Vec<(PluginId, PluginHealth)> {
self.plugins
.iter()
.map(|p| {
let id = p.id();
let health = self.get_plugin_health(&id);
(id, health)
})
.collect()
}
pub fn get_all_manifests(&self) -> Vec<PluginManifest> {
self.plugins.iter().map(|p| p.manifest()).collect()
}
pub fn get_all_ui_contributions(&self) -> Vec<(PluginId, Vec<UiContribution>)> {
self.plugins
.iter()
.map(|p| {
let m = p.manifest();
(m.id, m.ui)
})
.collect()
}
pub async fn init_all(&self, ctx: &PluginContext) -> Vec<(PluginId, PluginError)> {
let mut errors = Vec::new();
for plugin in &self.plugins {
match plugin.init(ctx).await {
Ok(()) => {
self.set_health(plugin.id(), PluginHealth::Healthy);
}
Err(e) => {
let id = plugin.id();
log::error!("Plugin {} failed to init: {}", id, e);
self.set_health(id.clone(), PluginHealth::Failed(e.to_string()));
errors.push((id, e));
}
}
}
errors
}
pub async fn shutdown_all(&self) -> Result<(), PluginError> {
for plugin in self.plugins.iter().rev() {
plugin.shutdown().await?;
}
Ok(())
}
pub async fn emit_workspace_opened(&self, event: &WorkspaceOpenedEvent) {
for plugin in &self.workspace_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_workspace_opened(event).await;
}
}
pub async fn emit_workspace_closed(&self, event: &WorkspaceClosedEvent) {
for plugin in &self.workspace_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_workspace_closed(event).await;
}
}
pub async fn emit_workspace_changed(&self, event: &WorkspaceChangedEvent) {
for plugin in &self.workspace_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_workspace_changed(event).await;
}
}
pub async fn emit_workspace_committed(&self, event: &WorkspaceCommittedEvent) {
for plugin in &self.workspace_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_workspace_committed(event).await;
}
}
pub async fn emit_file_saved(&self, event: &FileSavedEvent) {
for plugin in &self.file_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_file_saved(event).await;
}
}
pub async fn emit_file_created(&self, event: &FileCreatedEvent) {
for plugin in &self.file_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_file_created(event).await;
}
}
pub async fn emit_file_deleted(&self, event: &FileDeletedEvent) {
for plugin in &self.file_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_file_deleted(event).await;
}
}
pub async fn emit_file_moved(&self, event: &FileMovedEvent) {
for plugin in &self.file_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_file_moved(event).await;
}
}
pub async fn notify_workspace_modified(&self) {
for plugin in &self.workspace_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.notify_workspace_modified().await;
}
}
pub async fn emit_body_doc_renamed(&self, old_path: &str, new_path: &str) {
for plugin in &self.workspace_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_body_doc_renamed(old_path, new_path).await;
}
}
pub async fn emit_body_doc_deleted(&self, path: &str) {
for plugin in &self.workspace_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.on_body_doc_deleted(path).await;
}
}
pub async fn track_file_for_sync(&self, canonical_path: &str) {
for plugin in &self.workspace_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.track_file_for_sync(canonical_path).await;
}
}
pub fn track_content_for_sync(&self, canonical_path: &str, content: &str) {
for plugin in &self.workspace_plugins {
if !self.is_plugin_healthy(&plugin.id()) {
continue;
}
plugin.track_content_for_sync(canonical_path, content);
}
}
pub fn get_canonical_path(&self, storage_path: &str) -> Option<String> {
for plugin in &self.workspace_plugins {
if let Some(canonical) = plugin.get_canonical_path(storage_path) {
return Some(canonical);
}
}
None
}
pub fn get_file_title(&self, canonical_path: &str) -> Option<String> {
for plugin in &self.workspace_plugins {
if let Some(title) = plugin.get_file_title(canonical_path) {
return Some(title);
}
}
None
}
pub async fn handle_plugin_command(
&self,
plugin_id: &str,
cmd: &str,
params: JsonValue,
) -> Option<Result<JsonValue, PluginError>> {
for plugin in &self.workspace_plugins {
if plugin.id().0 == plugin_id {
if !self.is_plugin_healthy(&plugin.id()) {
return Some(Err(PluginError::Other(format!(
"Plugin '{}' is in failed state",
plugin_id
))));
}
return plugin.handle_command(cmd, params).await;
}
}
None
}
}
impl PluginRegistry {
pub async fn forward_fs_event(&self, event: &crate::fs::FileSystemEvent) {
use crate::fs::FileSystemEvent;
match event {
FileSystemEvent::FileCreated { path, .. } => {
let path_str = path.to_string_lossy().to_string();
self.emit_file_created(&FileCreatedEvent { path: path_str })
.await;
}
FileSystemEvent::FileDeleted { path, .. } => {
let path_str = path.to_string_lossy().to_string();
self.emit_file_deleted(&FileDeletedEvent { path: path_str })
.await;
}
FileSystemEvent::FileRenamed {
old_path, new_path, ..
} => {
let old = old_path.to_string_lossy().to_string();
let new = new_path.to_string_lossy().to_string();
self.emit_file_moved(&FileMovedEvent {
old_path: old,
new_path: new,
})
.await;
}
FileSystemEvent::FileMoved { path, .. } => {
let path_str = path.to_string_lossy().to_string();
self.emit_file_saved(&FileSavedEvent { path: path_str })
.await;
}
FileSystemEvent::MetadataChanged { path, .. }
| FileSystemEvent::ContentsChanged { path, .. } => {
let path_str = path.to_string_lossy().to_string();
self.emit_file_saved(&FileSavedEvent { path: path_str })
.await;
}
_ => {}
}
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}