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 run_hook_for_plugin(&self, plugin: &str, hook_name: &str, args: super::hooks::HookArgs) {
261 #[cfg(feature = "plugins")]
262 {
263 if let Some(ref manager) = self.inner {
264 manager.run_hook_for_plugin(plugin, hook_name, args);
265 }
266 }
267 #[cfg(not(feature = "plugins"))]
268 {
269 let _ = (plugin, hook_name, args);
270 }
271 }
272
273 pub fn deliver_response(&self, response: super::api::PluginResponse) {
275 #[cfg(feature = "plugins")]
276 {
277 if let Some(ref manager) = self.inner {
278 manager.deliver_response(response);
279 }
280 }
281 #[cfg(not(feature = "plugins"))]
282 {
283 let _ = response;
284 }
285 }
286
287 pub fn process_commands(&mut self) -> Vec<super::api::PluginCommand> {
289 let mut commands = std::mem::take(&mut self.pending_injected_commands);
294 #[cfg(feature = "plugins")]
295 {
296 if let Some(ref mut manager) = self.inner {
297 commands.extend(manager.process_commands());
298 }
299 }
300 commands
301 }
302
303 pub fn process_commands_until_hook_completed(
311 &mut self,
312 hook_name: &str,
313 timeout: std::time::Duration,
314 ) -> Vec<super::api::PluginCommand> {
315 #[cfg(feature = "plugins")]
316 {
317 if let Some(ref mut manager) = self.inner {
318 return manager.process_commands_until_hook_completed(hook_name, timeout);
319 }
320 Vec::new()
321 }
322 #[cfg(not(feature = "plugins"))]
323 {
324 let _ = (hook_name, timeout);
325 Vec::new()
326 }
327 }
328
329 #[cfg(feature = "plugins")]
331 pub fn state_snapshot_handle(&self) -> Option<Arc<RwLock<super::api::EditorStateSnapshot>>> {
332 self.inner.as_ref().map(|m| m.state_snapshot_handle())
333 }
334
335 #[cfg(feature = "plugins")]
340 pub fn search_handles_handle(&self) -> Option<fresh_core::api::SearchHandleRegistry> {
341 self.inner.as_ref().map(|m| m.search_handles_handle())
342 }
343
344 #[cfg(not(feature = "plugins"))]
346 pub fn search_handles_handle(&self) -> Option<fresh_core::api::SearchHandleRegistry> {
347 None
348 }
349
350 #[cfg(feature = "plugins")]
352 pub fn execute_action_async(
353 &self,
354 action_name: &str,
355 ) -> Option<anyhow::Result<fresh_plugin_runtime::thread::oneshot::Receiver<anyhow::Result<()>>>>
356 {
357 self.inner
358 .as_ref()
359 .map(|m| m.execute_action_async(action_name))
360 }
361
362 #[cfg(feature = "plugins")]
364 pub fn list_plugins(
365 &self,
366 ) -> Vec<fresh_plugin_runtime::backend::quickjs_backend::TsPluginInfo> {
367 self.inner
368 .as_ref()
369 .map(|m| m.list_plugins())
370 .unwrap_or_default()
371 }
372
373 pub fn plugin_declarations(&self) -> Vec<(String, String)> {
381 #[cfg(feature = "plugins")]
382 {
383 self.list_plugins()
384 .into_iter()
385 .filter_map(|info| info.declarations.map(|d| (info.name, d)))
386 .collect()
387 }
388 #[cfg(not(feature = "plugins"))]
389 {
390 Vec::new()
391 }
392 }
393
394 #[cfg(feature = "plugins")]
396 pub fn reload_plugin(&self, name: &str) -> anyhow::Result<()> {
397 self.inner
398 .as_ref()
399 .ok_or_else(|| anyhow::anyhow!("Plugin system not active"))?
400 .reload_plugin(name)
401 }
402
403 #[cfg(feature = "plugins")]
408 pub fn load_plugins_from_dir_with_config_request(
409 &self,
410 dir: &Path,
411 plugin_configs: &HashMap<String, PluginConfig>,
412 ) -> Option<
413 fresh_plugin_runtime::thread::oneshot::Receiver<
414 fresh_plugin_runtime::thread::PluginsDirLoadResult,
415 >,
416 > {
417 self.inner.as_ref().and_then(|m| {
418 m.load_plugins_from_dir_with_config_request(dir, plugin_configs)
419 .ok()
420 })
421 }
422
423 #[cfg(feature = "plugins")]
426 pub fn load_plugin_from_source_request(
427 &self,
428 source: &str,
429 name: &str,
430 is_typescript: bool,
431 ) -> Option<fresh_plugin_runtime::thread::oneshot::Receiver<anyhow::Result<()>>> {
432 self.inner.as_ref().and_then(|m| {
433 m.load_plugin_from_source_request(source, name, is_typescript)
434 .ok()
435 })
436 }
437
438 #[cfg(feature = "plugins")]
442 pub fn list_plugins_request(
443 &self,
444 ) -> Option<
445 fresh_plugin_runtime::thread::oneshot::Receiver<
446 Vec<fresh_plugin_runtime::backend::quickjs_backend::TsPluginInfo>,
447 >,
448 > {
449 self.inner
450 .as_ref()
451 .and_then(|m| m.list_plugins_request().ok())
452 }
453
454 pub fn has_hook_handlers(&self, hook_name: &str) -> bool {
460 #[cfg(feature = "plugins")]
461 {
462 self.inner
463 .as_ref()
464 .map(|m| m.has_hook_handlers(hook_name))
465 .unwrap_or(false)
466 }
467 #[cfg(not(feature = "plugins"))]
468 {
469 let _ = hook_name;
470 false
471 }
472 }
473
474 pub fn has_subscribers(&self, hook_name: &str) -> bool {
478 #[cfg(feature = "plugins")]
479 {
480 self.inner
481 .as_ref()
482 .map(|m| m.has_subscribers(hook_name))
483 .unwrap_or(false)
484 }
485 #[cfg(not(feature = "plugins"))]
486 {
487 let _ = hook_name;
488 false
489 }
490 }
491
492 #[cfg(feature = "plugins")]
494 pub fn resolve_callback(&self, callback_id: super::api::JsCallbackId, result_json: String) {
495 if let Some(inner) = &self.inner {
496 inner.resolve_callback(callback_id, result_json);
497 }
498 }
499
500 #[cfg(not(feature = "plugins"))]
502 pub fn resolve_callback(
503 &self,
504 callback_id: fresh_core::api::JsCallbackId,
505 result_json: String,
506 ) {
507 let _ = (callback_id, result_json);
508 }
509
510 #[cfg(feature = "plugins")]
512 pub fn reject_callback(&self, callback_id: super::api::JsCallbackId, error: String) {
513 if let Some(inner) = &self.inner {
514 inner.reject_callback(callback_id, error);
515 }
516 }
517
518 #[cfg(not(feature = "plugins"))]
520 pub fn reject_callback(&self, callback_id: fresh_core::api::JsCallbackId, error: String) {
521 let _ = (callback_id, error);
522 }
523}