1use 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
20pub struct PluginManager {
25 #[cfg(feature = "plugins")]
26 inner: Option<PluginThreadHandle>,
27 #[cfg(not(feature = "plugins"))]
28 _phantom: std::marker::PhantomData<()>,
29 pending_injected_commands: Vec<super::api::PluginCommand>,
36}
37
38impl PluginManager {
39 pub fn new(
44 enable: bool,
45 command_registry: Arc<RwLock<CommandRegistry>>,
46 dir_context: DirectoryContext,
47 theme_cache: Arc<RwLock<HashMap<String, serde_json::Value>>>,
48 ) -> Self {
49 #[cfg(feature = "plugins")]
50 {
51 if enable {
52 let services = Arc::new(EditorServiceBridge {
53 command_registry: command_registry.clone(),
54 dir_context,
55 theme_cache,
56 });
57 match PluginThreadHandle::spawn(services) {
58 Ok(handle) => {
59 return Self {
60 inner: Some(handle),
61 pending_injected_commands: Vec::new(),
62 }
63 }
64 Err(e) => {
65 tracing::error!("Failed to spawn TypeScript plugin thread: {}", e);
66 #[cfg(debug_assertions)]
67 panic!("TypeScript plugin thread creation failed: {}", e);
68 }
69 }
70 } else {
71 tracing::info!("Plugins disabled via --no-plugins flag");
72 }
73 Self {
74 inner: None,
75 pending_injected_commands: Vec::new(),
76 }
77 }
78
79 #[cfg(not(feature = "plugins"))]
80 {
81 let _ = command_registry; let _ = dir_context; let _ = theme_cache; if enable {
85 tracing::warn!("Plugins requested but compiled without plugin support");
86 }
87 Self {
88 _phantom: std::marker::PhantomData,
89 pending_injected_commands: Vec::new(),
90 }
91 }
92 }
93
94 pub fn test_inject_command(&mut self, command: super::api::PluginCommand) {
103 self.pending_injected_commands.push(command);
104 }
105
106 pub fn is_active(&self) -> bool {
109 if !self.pending_injected_commands.is_empty() {
110 return true;
111 }
112 #[cfg(feature = "plugins")]
113 {
114 self.inner.is_some()
115 }
116 #[cfg(not(feature = "plugins"))]
117 {
118 false
119 }
120 }
121
122 pub fn is_alive(&self) -> bool {
124 #[cfg(feature = "plugins")]
125 {
126 self.inner.as_ref().map(|h| h.is_alive()).unwrap_or(false)
127 }
128 #[cfg(not(feature = "plugins"))]
129 {
130 false
131 }
132 }
133
134 pub fn check_thread_health(&mut self) {
138 #[cfg(feature = "plugins")]
139 {
140 if let Some(ref mut handle) = self.inner {
141 handle.check_thread_health();
142 }
143 }
144 }
145
146 pub fn load_plugins_from_dir(&self, dir: &Path) -> Vec<String> {
148 #[cfg(feature = "plugins")]
149 {
150 if let Some(ref manager) = self.inner {
151 return manager.load_plugins_from_dir(dir);
152 }
153 Vec::new()
154 }
155 #[cfg(not(feature = "plugins"))]
156 {
157 let _ = dir;
158 Vec::new()
159 }
160 }
161
162 #[cfg(feature = "plugins")]
166 pub fn load_plugins_from_dir_with_config(
167 &self,
168 dir: &Path,
169 plugin_configs: &HashMap<String, PluginConfig>,
170 ) -> (Vec<String>, HashMap<String, PluginConfig>) {
171 if let Some(ref manager) = self.inner {
172 return manager.load_plugins_from_dir_with_config(dir, plugin_configs);
173 }
174 (Vec::new(), HashMap::new())
175 }
176
177 #[cfg(not(feature = "plugins"))]
179 pub fn load_plugins_from_dir_with_config(
180 &self,
181 dir: &Path,
182 plugin_configs: &HashMap<String, PluginConfig>,
183 ) -> (Vec<String>, HashMap<String, PluginConfig>) {
184 let _ = (dir, plugin_configs);
185 (Vec::new(), HashMap::new())
186 }
187
188 pub fn unload_plugin(&self, name: &str) -> anyhow::Result<()> {
190 #[cfg(feature = "plugins")]
191 {
192 self.inner
193 .as_ref()
194 .ok_or_else(|| anyhow::anyhow!("Plugin system not active"))?
195 .unload_plugin(name)
196 }
197 #[cfg(not(feature = "plugins"))]
198 {
199 let _ = name;
200 Ok(())
201 }
202 }
203
204 pub fn load_plugin(&self, path: &Path) -> anyhow::Result<()> {
206 #[cfg(feature = "plugins")]
207 {
208 self.inner
209 .as_ref()
210 .ok_or_else(|| anyhow::anyhow!("Plugin system not active"))?
211 .load_plugin(path)
212 }
213 #[cfg(not(feature = "plugins"))]
214 {
215 let _ = path;
216 Ok(())
217 }
218 }
219
220 pub fn load_plugin_from_source(
225 &self,
226 source: &str,
227 name: &str,
228 is_typescript: bool,
229 ) -> anyhow::Result<()> {
230 #[cfg(feature = "plugins")]
231 {
232 self.inner
233 .as_ref()
234 .ok_or_else(|| anyhow::anyhow!("Plugin system not active"))?
235 .load_plugin_from_source(source, name, is_typescript)
236 }
237 #[cfg(not(feature = "plugins"))]
238 {
239 let _ = (source, name, is_typescript);
240 Ok(())
241 }
242 }
243
244 pub fn run_hook(&self, hook_name: &str, args: super::hooks::HookArgs) {
246 #[cfg(feature = "plugins")]
247 {
248 if let Some(ref manager) = self.inner {
249 manager.run_hook(hook_name, args);
250 }
251 }
252 #[cfg(not(feature = "plugins"))]
253 {
254 let _ = (hook_name, args);
255 }
256 }
257
258 pub fn deliver_response(&self, response: super::api::PluginResponse) {
260 #[cfg(feature = "plugins")]
261 {
262 if let Some(ref manager) = self.inner {
263 manager.deliver_response(response);
264 }
265 }
266 #[cfg(not(feature = "plugins"))]
267 {
268 let _ = response;
269 }
270 }
271
272 pub fn process_commands(&mut self) -> Vec<super::api::PluginCommand> {
274 let mut commands = std::mem::take(&mut self.pending_injected_commands);
279 #[cfg(feature = "plugins")]
280 {
281 if let Some(ref mut manager) = self.inner {
282 commands.extend(manager.process_commands());
283 }
284 }
285 commands
286 }
287
288 pub fn process_commands_until_hook_completed(
296 &mut self,
297 hook_name: &str,
298 timeout: std::time::Duration,
299 ) -> Vec<super::api::PluginCommand> {
300 #[cfg(feature = "plugins")]
301 {
302 if let Some(ref mut manager) = self.inner {
303 return manager.process_commands_until_hook_completed(hook_name, timeout);
304 }
305 Vec::new()
306 }
307 #[cfg(not(feature = "plugins"))]
308 {
309 let _ = (hook_name, timeout);
310 Vec::new()
311 }
312 }
313
314 #[cfg(feature = "plugins")]
316 pub fn state_snapshot_handle(&self) -> Option<Arc<RwLock<super::api::EditorStateSnapshot>>> {
317 self.inner.as_ref().map(|m| m.state_snapshot_handle())
318 }
319
320 #[cfg(feature = "plugins")]
325 pub fn search_handles_handle(&self) -> Option<fresh_core::api::SearchHandleRegistry> {
326 self.inner.as_ref().map(|m| m.search_handles_handle())
327 }
328
329 #[cfg(not(feature = "plugins"))]
331 pub fn search_handles_handle(&self) -> Option<fresh_core::api::SearchHandleRegistry> {
332 None
333 }
334
335 #[cfg(feature = "plugins")]
337 pub fn execute_action_async(
338 &self,
339 action_name: &str,
340 ) -> Option<anyhow::Result<fresh_plugin_runtime::thread::oneshot::Receiver<anyhow::Result<()>>>>
341 {
342 self.inner
343 .as_ref()
344 .map(|m| m.execute_action_async(action_name))
345 }
346
347 #[cfg(feature = "plugins")]
349 pub fn list_plugins(
350 &self,
351 ) -> Vec<fresh_plugin_runtime::backend::quickjs_backend::TsPluginInfo> {
352 self.inner
353 .as_ref()
354 .map(|m| m.list_plugins())
355 .unwrap_or_default()
356 }
357
358 pub fn plugin_declarations(&self) -> Vec<(String, String)> {
366 #[cfg(feature = "plugins")]
367 {
368 self.list_plugins()
369 .into_iter()
370 .filter_map(|info| info.declarations.map(|d| (info.name, d)))
371 .collect()
372 }
373 #[cfg(not(feature = "plugins"))]
374 {
375 Vec::new()
376 }
377 }
378
379 #[cfg(feature = "plugins")]
381 pub fn reload_plugin(&self, name: &str) -> anyhow::Result<()> {
382 self.inner
383 .as_ref()
384 .ok_or_else(|| anyhow::anyhow!("Plugin system not active"))?
385 .reload_plugin(name)
386 }
387
388 #[cfg(feature = "plugins")]
393 pub fn load_plugins_from_dir_with_config_request(
394 &self,
395 dir: &Path,
396 plugin_configs: &HashMap<String, PluginConfig>,
397 ) -> Option<
398 fresh_plugin_runtime::thread::oneshot::Receiver<
399 fresh_plugin_runtime::thread::PluginsDirLoadResult,
400 >,
401 > {
402 self.inner.as_ref().and_then(|m| {
403 m.load_plugins_from_dir_with_config_request(dir, plugin_configs)
404 .ok()
405 })
406 }
407
408 #[cfg(feature = "plugins")]
411 pub fn load_plugin_from_source_request(
412 &self,
413 source: &str,
414 name: &str,
415 is_typescript: bool,
416 ) -> Option<fresh_plugin_runtime::thread::oneshot::Receiver<anyhow::Result<()>>> {
417 self.inner.as_ref().and_then(|m| {
418 m.load_plugin_from_source_request(source, name, is_typescript)
419 .ok()
420 })
421 }
422
423 #[cfg(feature = "plugins")]
427 pub fn list_plugins_request(
428 &self,
429 ) -> Option<
430 fresh_plugin_runtime::thread::oneshot::Receiver<
431 Vec<fresh_plugin_runtime::backend::quickjs_backend::TsPluginInfo>,
432 >,
433 > {
434 self.inner
435 .as_ref()
436 .and_then(|m| m.list_plugins_request().ok())
437 }
438
439 pub fn has_hook_handlers(&self, hook_name: &str) -> bool {
445 #[cfg(feature = "plugins")]
446 {
447 self.inner
448 .as_ref()
449 .map(|m| m.has_hook_handlers(hook_name))
450 .unwrap_or(false)
451 }
452 #[cfg(not(feature = "plugins"))]
453 {
454 let _ = hook_name;
455 false
456 }
457 }
458
459 pub fn has_subscribers(&self, hook_name: &str) -> bool {
463 #[cfg(feature = "plugins")]
464 {
465 self.inner
466 .as_ref()
467 .map(|m| m.has_subscribers(hook_name))
468 .unwrap_or(false)
469 }
470 #[cfg(not(feature = "plugins"))]
471 {
472 let _ = hook_name;
473 false
474 }
475 }
476
477 #[cfg(feature = "plugins")]
479 pub fn resolve_callback(&self, callback_id: super::api::JsCallbackId, result_json: String) {
480 if let Some(inner) = &self.inner {
481 inner.resolve_callback(callback_id, result_json);
482 }
483 }
484
485 #[cfg(not(feature = "plugins"))]
487 pub fn resolve_callback(
488 &self,
489 callback_id: fresh_core::api::JsCallbackId,
490 result_json: String,
491 ) {
492 let _ = (callback_id, result_json);
493 }
494
495 #[cfg(feature = "plugins")]
497 pub fn reject_callback(&self, callback_id: super::api::JsCallbackId, error: String) {
498 if let Some(inner) = &self.inner {
499 inner.reject_callback(callback_id, error);
500 }
501 }
502
503 #[cfg(not(feature = "plugins"))]
505 pub fn reject_callback(&self, callback_id: fresh_core::api::JsCallbackId, error: String) {
506 let _ = (callback_id, error);
507 }
508}