use std::cell::{RefCell, RefMut}; use std::rc::Rc; use std::sync::Arc; use anyhow::anyhow; use rquickjs::Context; use warp_js::{JsFunctionId, JsFunctionRegistry, SerializedJsValue}; cfg_if::cfg_if! { if #[cfg(feature = "completions_v2")] { use warp_completer::signatures::CommandSignature; use crate::plugin::service::{ RegisterCommandSignatureRequest, RegisterCommandSignatureService, }; } } /// A handle on a single JS plugin. /// /// We use a `Rc>` to make it possible for the plugin "API" (JS functions implemented in /// Rust) _and_ native Rust logic to share ownership over the plugin "state". We know that, since /// JS is singlethreaded, the JS-side logic will never actually ask for a mutable borrow at the same /// time as host-side Rust. #[derive(Clone)] pub struct PluginHandle(Rc>); impl PluginHandle { pub(super) fn new( app_service_callers: AppServiceCallers, on_registered_js_function_callback: impl Fn(JsFunctionId) + 'static, ) -> Self { Self(Rc::new(RefCell::new(Plugin::new( app_service_callers, on_registered_js_function_callback, )))) } pub(super) fn get_mut(&self) -> RefMut<'_, Plugin> { (*self.0).borrow_mut() } } /// Container struct for holding ipc `ServiceCaller` dependencies of `Plugin`. pub(super) struct AppServiceCallers { #[cfg(feature = "completions_v2")] register_command_signatures_caller: Box>, } impl AppServiceCallers { pub fn new(#[allow(unused_variables)] app_client: Arc) -> Self { Self { #[cfg(feature = "completions_v2")] register_command_signatures_caller: ipc::service_caller::< RegisterCommandSignatureService, >(app_client), } } } /// Represents logic and functionality implemented by a Plugin in JS. /// /// Conceptually, this an intermediate abstraction between plugin JS and "host-side" Rust. /// /// Concretely, this can generally be viewed as a wrapper around functions implemented by the /// plugin in JavaScript, exposing APIs to call such JS functions from Rust. /// /// This ultimately the main backing data structure behind the JS plugin API exposed to plugins /// (see [`super::js_api`]). pub(super) struct Plugin { app_services: AppServiceCallers, js_function_registry: JsFunctionRegistry, } impl Plugin { /// Handles the given `request` and returns its corresponding response. /// /// Generally, should return a `PluginResponse` enum variant that matches the request variant. pub(super) fn handle_request( &mut self, request: PluginRequest, ctx: &Context, ) -> anyhow::Result { match request { PluginRequest::CallJsFunction { id, input } => self .call_js_function(input, &id, ctx) .map(|serialized_value| PluginResponse::CallJsFunctionResult { output: serialized_value, }), } } /// Registers the given command signatures. #[cfg(feature = "completions_v2")] pub(super) fn register_command_signatures(&mut self, signatures: Vec) { if let Err(e) = warpui::r#async::block_on( self.app_services .register_command_signatures_caller .call(RegisterCommandSignatureRequest { signatures }), ) { log::warn!("Failed to register command signature: {e:?}"); } } pub(super) fn js_function_registry_mut(&mut self) -> &mut JsFunctionRegistry { &mut self.js_function_registry } /// Calls the js function with the given function_id and input, if registered. /// /// The function with the given `function_id` is expected to be registered in this `Plugin`'s /// `JsFunctionRegistry`. If the function_id has no registered function, returns an error. fn call_js_function( &mut self, input: SerializedJsValue, function_id: &JsFunctionId, ctx: &Context, ) -> anyhow::Result { let Some(function) = self.js_function_registry.get_function(function_id) else { return Err(anyhow!( "Attempted to call unregistered JS Function with ID {:?}.", function_id )); }; Ok(ctx.with(|ctx| function.call(input, &mut self.js_function_registry, ctx))?) } fn new( app_services: AppServiceCallers, on_registered_js_function_callback: impl Fn(JsFunctionId) + 'static, ) -> Self { Self { js_function_registry: JsFunctionRegistry::new() .on_registered_js_function(on_registered_js_function_callback), app_services, } } } /// A request to be served by a plugin. #[derive(Debug, Clone)] pub(super) enum PluginRequest { /// Request for execution of a JS function with the contained `id` and `input` to be passed as /// a parameter. `input` should be the serialized bytes representation of the /// `IntoPluginJs`-implementing Rust struct to be passed to the JS function. CallJsFunction { id: JsFunctionId, input: SerializedJsValue, }, } /// The response to a PluginRequest. #[derive(Debug)] pub(super) enum PluginResponse { /// Result of JS function execution requested via `PluginRequest::CallJsFunction`. CallJsFunctionResult { /// The serialized bytes representation of the `FromPluginJs`-implementing Rust value that /// is the return value of the JS function. /// /// If this response is coming from a thread that does "host" the JS function (e.g. it was /// registered by a different plugin), this is `None`. output: SerializedJsValue, }, }