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")]
322 pub fn execute_action_async(
323 &self,
324 action_name: &str,
325 ) -> Option<anyhow::Result<fresh_plugin_runtime::thread::oneshot::Receiver<anyhow::Result<()>>>>
326 {
327 self.inner
328 .as_ref()
329 .map(|m| m.execute_action_async(action_name))
330 }
331
332 #[cfg(feature = "plugins")]
334 pub fn list_plugins(
335 &self,
336 ) -> Vec<fresh_plugin_runtime::backend::quickjs_backend::TsPluginInfo> {
337 self.inner
338 .as_ref()
339 .map(|m| m.list_plugins())
340 .unwrap_or_default()
341 }
342
343 pub fn plugin_declarations(&self) -> Vec<(String, String)> {
351 #[cfg(feature = "plugins")]
352 {
353 self.list_plugins()
354 .into_iter()
355 .filter_map(|info| info.declarations.map(|d| (info.name, d)))
356 .collect()
357 }
358 #[cfg(not(feature = "plugins"))]
359 {
360 Vec::new()
361 }
362 }
363
364 #[cfg(feature = "plugins")]
366 pub fn reload_plugin(&self, name: &str) -> anyhow::Result<()> {
367 self.inner
368 .as_ref()
369 .ok_or_else(|| anyhow::anyhow!("Plugin system not active"))?
370 .reload_plugin(name)
371 }
372
373 #[cfg(feature = "plugins")]
378 pub fn load_plugins_from_dir_with_config_request(
379 &self,
380 dir: &Path,
381 plugin_configs: &HashMap<String, PluginConfig>,
382 ) -> Option<
383 fresh_plugin_runtime::thread::oneshot::Receiver<
384 fresh_plugin_runtime::thread::PluginsDirLoadResult,
385 >,
386 > {
387 self.inner.as_ref().and_then(|m| {
388 m.load_plugins_from_dir_with_config_request(dir, plugin_configs)
389 .ok()
390 })
391 }
392
393 #[cfg(feature = "plugins")]
396 pub fn load_plugin_from_source_request(
397 &self,
398 source: &str,
399 name: &str,
400 is_typescript: bool,
401 ) -> Option<fresh_plugin_runtime::thread::oneshot::Receiver<anyhow::Result<()>>> {
402 self.inner.as_ref().and_then(|m| {
403 m.load_plugin_from_source_request(source, name, is_typescript)
404 .ok()
405 })
406 }
407
408 #[cfg(feature = "plugins")]
412 pub fn list_plugins_request(
413 &self,
414 ) -> Option<
415 fresh_plugin_runtime::thread::oneshot::Receiver<
416 Vec<fresh_plugin_runtime::backend::quickjs_backend::TsPluginInfo>,
417 >,
418 > {
419 self.inner
420 .as_ref()
421 .and_then(|m| m.list_plugins_request().ok())
422 }
423
424 pub fn has_hook_handlers(&self, hook_name: &str) -> bool {
426 #[cfg(feature = "plugins")]
427 {
428 self.inner
429 .as_ref()
430 .map(|m| m.has_hook_handlers(hook_name))
431 .unwrap_or(false)
432 }
433 #[cfg(not(feature = "plugins"))]
434 {
435 let _ = hook_name;
436 false
437 }
438 }
439
440 #[cfg(feature = "plugins")]
442 pub fn resolve_callback(&self, callback_id: super::api::JsCallbackId, result_json: String) {
443 if let Some(inner) = &self.inner {
444 inner.resolve_callback(callback_id, result_json);
445 }
446 }
447
448 #[cfg(not(feature = "plugins"))]
450 pub fn resolve_callback(
451 &self,
452 callback_id: fresh_core::api::JsCallbackId,
453 result_json: String,
454 ) {
455 let _ = (callback_id, result_json);
456 }
457
458 #[cfg(feature = "plugins")]
460 pub fn reject_callback(&self, callback_id: super::api::JsCallbackId, error: String) {
461 if let Some(inner) = &self.inner {
462 inner.reject_callback(callback_id, error);
463 }
464 }
465
466 #[cfg(not(feature = "plugins"))]
468 pub fn reject_callback(&self, callback_id: fresh_core::api::JsCallbackId, error: String) {
469 let _ = (callback_id, error);
470 }
471
472 #[cfg(feature = "plugins")]
475 pub fn call_streaming_callback(
476 &self,
477 callback_id: fresh_core::api::JsCallbackId,
478 result_json: String,
479 done: bool,
480 ) {
481 if let Some(inner) = &self.inner {
482 inner.call_streaming_callback(callback_id, result_json, done);
483 }
484 }
485
486 #[cfg(not(feature = "plugins"))]
488 pub fn call_streaming_callback(
489 &self,
490 callback_id: fresh_core::api::JsCallbackId,
491 result_json: String,
492 done: bool,
493 ) {
494 let _ = (callback_id, result_json, done);
495 }
496}