fresh/services/plugins/
manager.rs

1//! Unified Plugin Manager
2//!
3//! This module provides a unified interface for the plugin system that works
4//! regardless of whether the `plugins` feature is enabled. When plugins are
5//! disabled, all methods are no-ops, avoiding the need for cfg attributes
6//! scattered throughout the codebase.
7
8use crate::config_io::DirectoryContext;
9use crate::input::command_registry::CommandRegistry;
10use fresh_core::config::PluginConfig;
11use std::collections::HashMap;
12use std::path::Path;
13use std::sync::{Arc, RwLock};
14
15#[cfg(feature = "plugins")]
16use super::bridge::EditorServiceBridge;
17#[cfg(feature = "plugins")]
18use fresh_plugin_runtime::PluginThreadHandle;
19
20/// Unified plugin manager that abstracts over the plugin system.
21///
22/// When the `plugins` feature is enabled, this wraps `PluginThreadHandle`.
23/// When disabled, all methods are no-ops.
24pub struct PluginManager {
25    #[cfg(feature = "plugins")]
26    inner: Option<PluginThreadHandle>,
27    #[cfg(not(feature = "plugins"))]
28    _phantom: std::marker::PhantomData<()>,
29}
30
31impl PluginManager {
32    /// Create a new plugin manager.
33    ///
34    /// When `plugins` feature is enabled and `enable` is true, spawns the plugin thread.
35    /// Otherwise, creates a no-op manager.
36    pub fn new(
37        enable: bool,
38        command_registry: Arc<RwLock<CommandRegistry>>,
39        dir_context: DirectoryContext,
40    ) -> Self {
41        #[cfg(feature = "plugins")]
42        {
43            if enable {
44                let services = Arc::new(EditorServiceBridge {
45                    command_registry: command_registry.clone(),
46                    dir_context,
47                });
48                match PluginThreadHandle::spawn(services) {
49                    Ok(handle) => {
50                        return Self {
51                            inner: Some(handle),
52                        }
53                    }
54                    Err(e) => {
55                        tracing::error!("Failed to spawn TypeScript plugin thread: {}", e);
56                        #[cfg(debug_assertions)]
57                        panic!("TypeScript plugin thread creation failed: {}", e);
58                    }
59                }
60            } else {
61                tracing::info!("Plugins disabled via --no-plugins flag");
62            }
63            Self { inner: None }
64        }
65
66        #[cfg(not(feature = "plugins"))]
67        {
68            let _ = command_registry; // Suppress unused warning
69            let _ = dir_context; // Suppress unused warning
70            if enable {
71                tracing::warn!("Plugins requested but compiled without plugin support");
72            }
73            Self {
74                _phantom: std::marker::PhantomData,
75            }
76        }
77    }
78
79    /// Check if the plugin system is active (has a running plugin thread).
80    pub fn is_active(&self) -> bool {
81        #[cfg(feature = "plugins")]
82        {
83            self.inner.is_some()
84        }
85        #[cfg(not(feature = "plugins"))]
86        {
87            false
88        }
89    }
90
91    /// Check if the plugin thread is still alive
92    pub fn is_alive(&self) -> bool {
93        #[cfg(feature = "plugins")]
94        {
95            self.inner.as_ref().map(|h| h.is_alive()).unwrap_or(false)
96        }
97        #[cfg(not(feature = "plugins"))]
98        {
99            false
100        }
101    }
102
103    /// Check thread health and panic if the plugin thread died due to a panic.
104    /// This propagates plugin thread panics to the calling thread.
105    /// Call this periodically (e.g., in wait loops) to fail fast on plugin errors.
106    pub fn check_thread_health(&mut self) {
107        #[cfg(feature = "plugins")]
108        {
109            if let Some(ref mut handle) = self.inner {
110                handle.check_thread_health();
111            }
112        }
113    }
114
115    /// Load plugins from a directory.
116    pub fn load_plugins_from_dir(&self, dir: &Path) -> Vec<String> {
117        #[cfg(feature = "plugins")]
118        {
119            if let Some(ref manager) = self.inner {
120                return manager.load_plugins_from_dir(dir);
121            }
122            Vec::new()
123        }
124        #[cfg(not(feature = "plugins"))]
125        {
126            let _ = dir;
127            Vec::new()
128        }
129    }
130
131    /// Load plugins from a directory with config support.
132    /// Returns (errors, discovered_plugins) where discovered_plugins is a map of
133    /// plugin name -> PluginConfig with paths populated.
134    #[cfg(feature = "plugins")]
135    pub fn load_plugins_from_dir_with_config(
136        &self,
137        dir: &Path,
138        plugin_configs: &HashMap<String, PluginConfig>,
139    ) -> (Vec<String>, HashMap<String, PluginConfig>) {
140        if let Some(ref manager) = self.inner {
141            return manager.load_plugins_from_dir_with_config(dir, plugin_configs);
142        }
143        (Vec::new(), HashMap::new())
144    }
145
146    /// Load plugins from a directory with config support (no-op when plugins disabled).
147    #[cfg(not(feature = "plugins"))]
148    pub fn load_plugins_from_dir_with_config(
149        &self,
150        dir: &Path,
151        plugin_configs: &HashMap<String, PluginConfig>,
152    ) -> (Vec<String>, HashMap<String, PluginConfig>) {
153        let _ = (dir, plugin_configs);
154        (Vec::new(), HashMap::new())
155    }
156
157    /// Unload a plugin by name.
158    pub fn unload_plugin(&self, name: &str) -> anyhow::Result<()> {
159        #[cfg(feature = "plugins")]
160        {
161            self.inner
162                .as_ref()
163                .ok_or_else(|| anyhow::anyhow!("Plugin system not active"))?
164                .unload_plugin(name)
165        }
166        #[cfg(not(feature = "plugins"))]
167        {
168            let _ = name;
169            Ok(())
170        }
171    }
172
173    /// Load a single plugin by path.
174    pub fn load_plugin(&self, path: &Path) -> anyhow::Result<()> {
175        #[cfg(feature = "plugins")]
176        {
177            self.inner
178                .as_ref()
179                .ok_or_else(|| anyhow::anyhow!("Plugin system not active"))?
180                .load_plugin(path)
181        }
182        #[cfg(not(feature = "plugins"))]
183        {
184            let _ = path;
185            Ok(())
186        }
187    }
188
189    /// Run a hook (fire-and-forget).
190    pub fn run_hook(&self, hook_name: &str, args: super::hooks::HookArgs) {
191        #[cfg(feature = "plugins")]
192        {
193            if let Some(ref manager) = self.inner {
194                manager.run_hook(hook_name, args);
195            }
196        }
197        #[cfg(not(feature = "plugins"))]
198        {
199            let _ = (hook_name, args);
200        }
201    }
202
203    /// Deliver a response to a pending async plugin operation.
204    pub fn deliver_response(&self, response: super::api::PluginResponse) {
205        #[cfg(feature = "plugins")]
206        {
207            if let Some(ref manager) = self.inner {
208                manager.deliver_response(response);
209            }
210        }
211        #[cfg(not(feature = "plugins"))]
212        {
213            let _ = response;
214        }
215    }
216
217    /// Process pending plugin commands (non-blocking).
218    pub fn process_commands(&mut self) -> Vec<super::api::PluginCommand> {
219        #[cfg(feature = "plugins")]
220        {
221            if let Some(ref mut manager) = self.inner {
222                return manager.process_commands();
223            }
224            Vec::new()
225        }
226        #[cfg(not(feature = "plugins"))]
227        {
228            Vec::new()
229        }
230    }
231
232    /// Get the state snapshot handle for updating editor state.
233    #[cfg(feature = "plugins")]
234    pub fn state_snapshot_handle(&self) -> Option<Arc<RwLock<super::api::EditorStateSnapshot>>> {
235        self.inner.as_ref().map(|m| m.state_snapshot_handle())
236    }
237
238    /// Execute a plugin action asynchronously.
239    #[cfg(feature = "plugins")]
240    pub fn execute_action_async(
241        &self,
242        action_name: &str,
243    ) -> Option<anyhow::Result<fresh_plugin_runtime::thread::oneshot::Receiver<anyhow::Result<()>>>>
244    {
245        self.inner
246            .as_ref()
247            .map(|m| m.execute_action_async(action_name))
248    }
249
250    /// List all loaded plugins.
251    #[cfg(feature = "plugins")]
252    pub fn list_plugins(
253        &self,
254    ) -> Vec<fresh_plugin_runtime::backend::quickjs_backend::TsPluginInfo> {
255        self.inner
256            .as_ref()
257            .map(|m| m.list_plugins())
258            .unwrap_or_default()
259    }
260
261    /// Reload a plugin by name.
262    #[cfg(feature = "plugins")]
263    pub fn reload_plugin(&self, name: &str) -> anyhow::Result<()> {
264        self.inner
265            .as_ref()
266            .ok_or_else(|| anyhow::anyhow!("Plugin system not active"))?
267            .reload_plugin(name)
268    }
269
270    /// Check if any handlers are registered for a hook.
271    pub fn has_hook_handlers(&self, hook_name: &str) -> bool {
272        #[cfg(feature = "plugins")]
273        {
274            self.inner
275                .as_ref()
276                .map(|m| m.has_hook_handlers(hook_name))
277                .unwrap_or(false)
278        }
279        #[cfg(not(feature = "plugins"))]
280        {
281            let _ = hook_name;
282            false
283        }
284    }
285
286    /// Resolve an async callback in the plugin runtime
287    #[cfg(feature = "plugins")]
288    pub fn resolve_callback(&self, callback_id: super::api::JsCallbackId, result_json: String) {
289        if let Some(inner) = &self.inner {
290            inner.resolve_callback(callback_id, result_json);
291        }
292    }
293
294    /// Resolve an async callback in the plugin runtime (no-op when plugins disabled)
295    #[cfg(not(feature = "plugins"))]
296    pub fn resolve_callback(
297        &self,
298        callback_id: fresh_core::api::JsCallbackId,
299        result_json: String,
300    ) {
301        let _ = (callback_id, result_json);
302    }
303
304    /// Reject an async callback in the plugin runtime
305    #[cfg(feature = "plugins")]
306    pub fn reject_callback(&self, callback_id: super::api::JsCallbackId, error: String) {
307        if let Some(inner) = &self.inner {
308            inner.reject_callback(callback_id, error);
309        }
310    }
311
312    /// Reject an async callback in the plugin runtime (no-op when plugins disabled)
313    #[cfg(not(feature = "plugins"))]
314    pub fn reject_callback(&self, callback_id: fresh_core::api::JsCallbackId, error: String) {
315        let _ = (callback_id, error);
316    }
317}