diff --git a/Cargo.toml b/Cargo.toml index 529aec6..4bbbc02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ authors = [ ] license = "GPL-3.0-only" edition = "2021" -version = "0.7.2" +version = "0.7.3" exclude = ["dist/*"] [lib] diff --git a/README.md b/README.md index 697614b..aecd2c9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Crates.io Version](https://img.shields.io/crates/v/codemp)](https://crates.io/crates/codemp) [![Gitter Chat](https://img.shields.io/gitter/room/hexedtech/codemp)](https://gitter.im/hexedtech/codemp) [![GitHub last commit](https://img.shields.io/github/last-commit/hexedtech/codemp)](https://github.com/hexedtech/codemp/commits/dev/) -[![GitHub commits since tagged version](https://img.shields.io/github/commits-since/hexedtech/codemp/v0.7.2)](https://github.com/hexedtech/codemp/releases/tag/v0.7.2) +[![GitHub commits since tagged version](https://img.shields.io/github/commits-since/hexedtech/codemp/v0.7.3)](https://github.com/hexedtech/codemp/releases/tag/v0.7.3) > `codemp` is a **collaborative** text editing solution to work remotely. diff --git a/build.rs b/build.rs index 2710634..e305a3f 100644 --- a/build.rs +++ b/build.rs @@ -20,7 +20,7 @@ fn main() { { if let Ok("macos") = std::env::var("CARGO_CFG_TARGET_OS").as_deref() { println!("cargo:rustc-cdylib-link-arg=-undefined"); - println!("cargo:rustc-cdylib-link-arg=dynamic_lookup"); - } + println!("cargo:rustc-cdylib-link-arg=dynamic_lookup"); + } } } diff --git a/dist/java/build.gradle b/dist/java/build.gradle index d2cb7b7..240dacc 100644 --- a/dist/java/build.gradle +++ b/dist/java/build.gradle @@ -5,7 +5,7 @@ plugins { } group = 'mp.code' -version = '0.7.2' +version = '0.7.3' java { sourceCompatibility = targetCompatibility = JavaVersion.VERSION_11 diff --git a/dist/js/package.json b/dist/js/package.json index 7f7cebe..3e53e17 100644 --- a/dist/js/package.json +++ b/dist/js/package.json @@ -1,6 +1,6 @@ { "name": "@codemp/native", - "version": "0.7.2", + "version": "0.7.3", "description": "code multiplexer -- javascript bindings", "keywords": [ "codemp", @@ -35,8 +35,8 @@ } }, "optionalDependencies": { - "@codemp/native-win32-x64-msvc": "0.7.2", - "@codemp/native-darwin-arm64": "0.7.2", - "@codemp/native-linux-x64-gnu": "0.7.2" + "@codemp/native-win32-x64-msvc": "0.7.3", + "@codemp/native-darwin-arm64": "0.7.3", + "@codemp/native-linux-x64-gnu": "0.7.3" } } diff --git a/dist/lua/annotations.lua b/dist/lua/annotations.lua index dc6d625..0e573fa 100644 --- a/dist/lua/annotations.lua +++ b/dist/lua/annotations.lua @@ -18,6 +18,9 @@ local NilPromise = {} --- block until promise is ready function NilPromise:await() end +--- cancel promise execution +function NilPromise:cancel() end + ---@param cb fun() callback to invoke ---invoke callback asynchronously as soon as promise is ready function NilPromise:and_then(cb) end @@ -30,6 +33,9 @@ local StringPromise = {} --- @return string function StringPromise:await() end +--- cancel promise execution +function StringPromise:cancel() end + ---@param cb fun(x: string) callback to invoke ---invoke callback asynchronously as soon as promise is ready function StringPromise:and_then(cb) end @@ -40,6 +46,8 @@ local StringArrayPromise = {} --- block until promise is ready and return value --- @return string[] function StringArrayPromise:await() end +--- cancel promise execution +function StringArrayPromise:cancel() end ---@param cb fun(x: string[]) callback to invoke ---invoke callback asynchronously as soon as promise is ready function StringArrayPromise:and_then(cb) end @@ -50,6 +58,8 @@ local ClientPromise = {} --- block until promise is ready and return value --- @return Client function ClientPromise:await() end +--- cancel promise execution +function ClientPromise:cancel() end ---@param cb fun(x: Client) callback to invoke ---invoke callback asynchronously as soon as promise is ready function ClientPromise:and_then(cb) end @@ -60,6 +70,8 @@ local WorkspacePromise = {} --- block until promise is ready and return value --- @return Workspace function WorkspacePromise:await() end +--- cancel promise execution +function WorkspacePromise:cancel() end ---@param cb fun(x: Workspace) callback to invoke ---invoke callback asynchronously as soon as promise is ready function WorkspacePromise:and_then(cb) end @@ -70,6 +82,8 @@ local WorkspaceEventPromise = {} --- block until promise is ready and return value --- @return WorkspaceEvent function WorkspaceEventPromise:await() end +--- cancel promise execution +function WorkspaceEventPromise:cancel() end ---@param cb fun(x: WorkspaceEvent) callback to invoke ---invoke callback asynchronously as soon as promise is ready function WorkspaceEventPromise:and_then(cb) end @@ -90,6 +104,8 @@ local BufferControllerPromise = {} --- block until promise is ready and return value --- @return BufferController function BufferControllerPromise:await() end +--- cancel promise execution +function BufferControllerPromise:cancel() end ---@param cb fun(x: BufferController) callback to invoke ---invoke callback asynchronously as soon as promise is ready function BufferControllerPromise:and_then(cb) end @@ -100,6 +116,8 @@ local CursorPromise = {} --- block until promise is ready and return value --- @return Cursor function CursorPromise:await() end +--- cancel promise execution +function CursorPromise:cancel() end ---@param cb fun(x: Cursor) callback to invoke ---invoke callback asynchronously as soon as promise is ready function CursorPromise:and_then(cb) end @@ -110,6 +128,8 @@ local MaybeCursorPromise = {} --- block until promise is ready and return value --- @return Cursor | nil function MaybeCursorPromise:await() end +--- cancel promise execution +function MaybeCursorPromise:cancel() end ---@param cb fun(x: Cursor | nil) callback to invoke ---invoke callback asynchronously as soon as promise is ready function MaybeCursorPromise:and_then(cb) end @@ -120,6 +140,8 @@ local TextChangePromise = {} --- block until promise is ready and return value --- @return TextChange function TextChangePromise:await() end +--- cancel promise execution +function TextChangePromise:cancel() end ---@param cb fun(x: TextChange) callback to invoke ---invoke callback asynchronously as soon as promise is ready function TextChangePromise:and_then(cb) end @@ -130,6 +152,8 @@ local MaybeTextChangePromise = {} --- block until promise is ready and return value --- @return TextChange | nil function MaybeTextChangePromise:await() end +--- cancel promise execution +function MaybeTextChangePromise:cancel() end ---@param cb fun(x: TextChange | nil) callback to invoke ---invoke callback asynchronously as soon as promise is ready function MaybeTextChangePromise:and_then(cb) end diff --git a/dist/lua/codemp-0.7.2-1.rockspec b/dist/lua/codemp-0.7.3-1.rockspec similarity index 95% rename from dist/lua/codemp-0.7.2-1.rockspec rename to dist/lua/codemp-0.7.3-1.rockspec index ee654cd..db7c5e9 100644 --- a/dist/lua/codemp-0.7.2-1.rockspec +++ b/dist/lua/codemp-0.7.3-1.rockspec @@ -1,9 +1,9 @@ package = "codemp" -version = "0.7.2-1" +version = "0.7.3-1" source = { url = "git+https://github.com/hexedtech/codemp", - tag = "v0.7.2", + tag = "v0.7.3", } dependencies = { diff --git a/dist/py/pyproject.toml b/dist/py/pyproject.toml index c031b8b..d78d1a5 100644 --- a/dist/py/pyproject.toml +++ b/dist/py/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "codemp" -version = "0.7.2" +version = "0.7.3" description = "code multiplexer" requires-python = ">=3.8" license = "GPL-3.0-only" diff --git a/src/api/controller.rs b/src/api/controller.rs index d2e81b6..e15d7bb 100644 --- a/src/api/controller.rs +++ b/src/api/controller.rs @@ -1,5 +1,5 @@ //! # Controller -//! +//! //! A bidirectional stream handler to easily manage asynchronous operations between local buffers //! and the server. @@ -75,7 +75,6 @@ pub trait AsyncReceiver : Sized + Send + Sync { async fn try_recv(&self) -> ControllerResult>; } - /// Type wrapper for Boxed dynamic callback. pub struct ControllerCallback(pub Box); diff --git a/src/api/mod.rs b/src/api/mod.rs index 2e5f00d..54be1da 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -19,9 +19,9 @@ pub mod event; /// data structure for remote users pub mod user; -pub use controller::Controller; pub use change::TextChange; pub use config::Config; +pub use controller::Controller; pub use cursor::Cursor; pub use event::Event; pub use user::User; diff --git a/src/buffer/worker.rs b/src/buffer/worker.rs index e1f568a..de669b3 100644 --- a/src/buffer/worker.rs +++ b/src/buffer/worker.rs @@ -34,7 +34,12 @@ struct BufferWorker { } impl BufferController { - pub(crate) fn spawn(user_id: Uuid, path: &str, tx: mpsc::Sender, rx: Streaming) -> Self { + pub(crate) fn spawn( + user_id: Uuid, + path: &str, + tx: mpsc::Sender, + rx: Streaming, + ) -> Self { let init = diamond_types::LocalVersion::default(); let (latest_version_tx, latest_version_rx) = watch::channel(init.clone()); @@ -75,17 +80,21 @@ impl BufferController { timer: Timer::new(10), // TODO configurable! }; - tokio::spawn(async move { - BufferController::work(worker, tx, rx).await - }); + tokio::spawn(async move { BufferController::work(worker, tx, rx).await }); BufferController(controller) } - async fn work(mut worker: BufferWorker, tx: mpsc::Sender, mut rx: Streaming) { + async fn work( + mut worker: BufferWorker, + tx: mpsc::Sender, + mut rx: Streaming, + ) { tracing::debug!("controller worker started"); loop { - if worker.controller.upgrade().is_none() { break }; + if worker.controller.upgrade().is_none() { + break; + }; // block until one of these is ready tokio::select! { diff --git a/src/client.rs b/src/client.rs index 2d4d03a..7cba102 100644 --- a/src/client.rs +++ b/src/client.rs @@ -182,7 +182,10 @@ impl Client { /// Leave the [`Workspace`] with the given name. pub fn leave_workspace(&self, id: &str) -> bool { - self.0.workspaces.remove(id).is_some() + match self.0.workspaces.remove(id) { + None => true, + Some(x) => x.1.consume(), + } } /// Gets a [`Workspace`] handle by name. diff --git a/src/cursor/worker.rs b/src/cursor/worker.rs index 56e455f..aab8c34 100644 --- a/src/cursor/worker.rs +++ b/src/cursor/worker.rs @@ -4,8 +4,11 @@ use tokio::sync::{mpsc, oneshot, watch}; use tonic::Streaming; use uuid::Uuid; -use crate::{api::{controller::ControllerCallback, Cursor, User}, ext::IgnorableError}; -use codemp_proto::cursor::{CursorPosition, CursorEvent}; +use crate::{ + api::{controller::ControllerCallback, Cursor, User}, + ext::IgnorableError, +}; +use codemp_proto::cursor::{CursorEvent, CursorPosition}; use super::controller::{CursorController, CursorControllerInner}; @@ -21,7 +24,11 @@ struct CursorWorker { } impl CursorController { - pub(crate) fn spawn(user_map: Arc>, tx: mpsc::Sender, rx: Streaming) -> Self { + pub(crate) fn spawn( + user_map: Arc>, + tx: mpsc::Sender, + rx: Streaming, + ) -> Self { // TODO we should tweak the channel buffer size to better propagate backpressure let (op_tx, op_rx) = mpsc::channel(64); let (stream_tx, stream_rx) = mpsc::channel(1); @@ -52,11 +59,17 @@ impl CursorController { CursorController(controller) } - async fn work(mut worker: CursorWorker, tx: mpsc::Sender, mut rx: Streaming) { + async fn work( + mut worker: CursorWorker, + tx: mpsc::Sender, + mut rx: Streaming, + ) { loop { tracing::debug!("cursor worker polling"); - if worker.controller.upgrade().is_none() { break }; // clean exit: all controllers dropped - tokio::select!{ + if worker.controller.upgrade().is_none() { + break; + }; // clean exit: all controllers dropped + tokio::select! { biased; // new poller diff --git a/src/errors.rs b/src/errors.rs index 500d8ce..e4b97dc 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -61,4 +61,3 @@ impl From for ControllerError { /// Wraps [std::result::Result] with a [ControllerError]. pub type ControllerResult = std::result::Result; - diff --git a/src/ext.rs b/src/ext.rs index 7228793..06a3cc1 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -16,7 +16,7 @@ use tokio::sync::mpsc; pub async fn select_buffer( buffers: &[crate::buffer::Controller], timeout: Option, - runtime: &tokio::runtime::Runtime + runtime: &tokio::runtime::Runtime, ) -> ControllerResult> { let (tx, mut rx) = mpsc::unbounded_channel(); let mut tasks = Vec::new(); @@ -46,13 +46,13 @@ pub async fn select_buffer( t.abort(); } return Ok(x); - }, + } } } } /// Hash a given byte array with the internally used algorithm. -/// +/// /// Currently, it uses [`xxhash_rust::xxh3::xxh3_64`]. pub fn hash(data: impl AsRef<[u8]>) -> i64 { let hash = xxhash_rust::xxh3::xxh3_64(data.as_ref()); @@ -104,11 +104,13 @@ pub trait IgnorableError { } impl IgnorableError for std::result::Result -where E : std::fmt::Debug { +where + E: std::fmt::Debug, +{ /// Logs the error as a warning and returns a unit. fn unwrap_or_warn(self, msg: &str) { match self { - Ok(_) => {}, + Ok(_) => {} Err(e) => tracing::warn!("{}: {:?}", msg, e), } } diff --git a/src/ffi/java/buffer.rs b/src/ffi/java/buffer.rs index 158ac03..3df1243 100644 --- a/src/ffi/java/buffer.rs +++ b/src/ffi/java/buffer.rs @@ -1,11 +1,14 @@ use jni::{objects::JObject, JNIEnv}; use jni_toolbox::jni; -use crate::{api::{controller::{AsyncReceiver, AsyncSender}, TextChange}, errors::ControllerError}; +use crate::{ + api::{controller::{AsyncReceiver, AsyncSender}, TextChange}, + errors::ControllerError, +}; use super::null_check; -/// Get the name of the buffer. +/// Get the name of the buffer. #[jni(package = "mp.code", class = "BufferController")] fn get_name(controller: &mut crate::buffer::Controller) -> String { controller.path().to_string() //TODO: &str is built into the newer version @@ -19,7 +22,9 @@ fn get_content(controller: &mut crate::buffer::Controller) -> Result Result, ControllerError> { +fn try_recv( + controller: &mut crate::buffer::Controller, +) -> Result, ControllerError> { super::tokio().block_on(controller.try_recv()) } @@ -31,23 +36,34 @@ fn recv(controller: &mut crate::buffer::Controller) -> Result Result<(), ControllerError> { +fn send( + controller: &mut crate::buffer::Controller, + change: TextChange, +) -> Result<(), ControllerError> { super::tokio().block_on(controller.send(change)) } /// Register a callback for buffer changes. #[jni(package = "mp.code", class = "BufferController")] -fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::buffer::Controller, cb: JObject<'local>) { +fn callback<'local>( + env: &mut JNIEnv<'local>, + controller: &mut crate::buffer::Controller, + cb: JObject<'local>, +) { null_check!(env, cb, {}); let Ok(cb_ref) = env.new_global_ref(cb) else { - env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!") - .expect("Failed to throw exception!"); + env.throw_new( + "mp/code/exceptions/JNIException", + "Failed to pin callback reference!", + ) + .expect("Failed to throw exception!"); return; }; controller.callback(move |controller: crate::buffer::Controller| { let jvm = super::jvm(); - let mut env = jvm.attach_current_thread_permanently() + let mut env = jvm + .attach_current_thread_permanently() .expect("failed attaching to main JVM thread"); if let Err(e) = env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; @@ -56,7 +72,7 @@ fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::buffer::Co &cb_ref, "accept", "(Ljava/lang/Object;)V", - &[jni::objects::JValueGen::Object(&jcontroller)] + &[jni::objects::JValueGen::Object(&jcontroller)], ) { tracing::error!("error invoking callback: {e:?}"); }; diff --git a/src/ffi/java/client.rs b/src/ffi/java/client.rs index e420171..ff14472 100644 --- a/src/ffi/java/client.rs +++ b/src/ffi/java/client.rs @@ -1,5 +1,10 @@ +use crate::{ + api::Config, + client::Client, + errors::{ConnectionError, RemoteError}, + Workspace, +}; use jni_toolbox::jni; -use crate::{api::Config, client::Client, errors::{ConnectionError, RemoteError}, Workspace}; /// Connect using the given credentials to the default server, and return a [Client] to interact with it. #[jni(package = "mp.code", class = "Client", ptr)] @@ -33,13 +38,21 @@ fn delete_workspace(client: &mut Client, workspace: String) -> Result<(), Remote /// Invite another user to an owned workspace. #[jni(package = "mp.code", class = "Client")] -fn invite_to_workspace(client: &mut Client, workspace: String, user: String) -> Result<(), RemoteError> { +fn invite_to_workspace( + client: &mut Client, + workspace: String, + user: String, +) -> Result<(), RemoteError> { super::tokio().block_on(client.invite_to_workspace(workspace, user)) } /// List available workspaces. #[jni(package = "mp.code", class = "Client")] -fn list_workspaces(client: &mut Client, owned: bool, invited: bool) -> Result, RemoteError> { +fn list_workspaces( + client: &mut Client, + owned: bool, + invited: bool, +) -> Result, RemoteError> { super::tokio().block_on(client.list_workspaces(owned, invited)) } @@ -52,7 +65,7 @@ fn active_workspaces(client: &mut Client) -> Vec { /// Leave a [Workspace] and return whether or not the client was in such workspace. #[jni(package = "mp.code", class = "Client")] fn leave_workspace(client: &mut Client, workspace: String) -> bool { - client.leave_workspace(&workspace) + client.leave_workspace(&workspace) } /// Get a [Workspace] by name and returns a pointer to it. diff --git a/src/ffi/java/cursor.rs b/src/ffi/java/cursor.rs index 0514b39..c4c8c94 100644 --- a/src/ffi/java/cursor.rs +++ b/src/ffi/java/cursor.rs @@ -1,6 +1,13 @@ +use crate::{ + api::{Controller, Cursor}, + errors::ControllerError, +}; use jni::{objects::JObject, JNIEnv}; use jni_toolbox::jni; -use crate::{api::{controller::{AsyncSender, AsyncReceiver}, Cursor}, errors::ControllerError}; +use crate::{ + api::{controller::{AsyncSender, AsyncReceiver}, Cursor}, + errors::ControllerError +}; use super::null_check; @@ -24,17 +31,25 @@ fn send(controller: &mut crate::cursor::Controller, cursor: Cursor) -> Result<() /// Register a callback for cursor changes. #[jni(package = "mp.code", class = "CursorController")] -fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::cursor::Controller, cb: JObject<'local>) { - null_check!(env, cb, {}); +fn callback<'local>( + env: &mut JNIEnv<'local>, + controller: &mut crate::cursor::Controller, + cb: JObject<'local>, +) { + null_check!(env, cb, {}); let Ok(cb_ref) = env.new_global_ref(cb) else { - env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!") - .expect("Failed to throw exception!"); + env.throw_new( + "mp/code/exceptions/JNIException", + "Failed to pin callback reference!", + ) + .expect("Failed to throw exception!"); return; }; controller.callback(move |controller: crate::cursor::Controller| { let jvm = super::jvm(); - let mut env = jvm.attach_current_thread_permanently() + let mut env = jvm + .attach_current_thread_permanently() .expect("failed attaching to main JVM thread"); if let Err(e) = env.with_local_frame(5, |env| { use jni_toolbox::IntoJavaObject; @@ -43,7 +58,7 @@ fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::cursor::Co &cb_ref, "accept", "(Ljava/lang/Object;)V", - &[jni::objects::JValueGen::Object(&jcontroller)] + &[jni::objects::JValueGen::Object(&jcontroller)], ) { tracing::error!("error invoking callback: {e:?}"); }; @@ -58,7 +73,7 @@ fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::cursor::Co /// Clear the callback for cursor changes. #[jni(package = "mp.code", class = "CursorController")] fn clear_callback(controller: &mut crate::cursor::Controller) { - controller.clear_callback() + controller.clear_callback() } /// Block until there is a new value available. diff --git a/src/ffi/java/ext.rs b/src/ffi/java/ext.rs index 2729e97..47dd939 100644 --- a/src/ffi/java/ext.rs +++ b/src/ffi/java/ext.rs @@ -4,7 +4,7 @@ use jni_toolbox::jni; #[allow(non_snake_case)] #[jni(package = "mp.code", class = "Extensions")] fn version() -> String { - crate::version() + crate::version() } /// Calculate the XXH3 hash for a given String. diff --git a/src/ffi/java/mod.rs b/src/ffi/java/mod.rs index 1c7ec45..cad7768 100644 --- a/src/ffi/java/mod.rs +++ b/src/ffi/java/mod.rs @@ -1,19 +1,19 @@ -pub mod client; -pub mod workspace; -pub mod cursor; pub mod buffer; +pub mod client; +pub mod cursor; pub mod ext; +pub mod workspace; /// Gets or creates the relevant [tokio::runtime::Runtime]. fn tokio() -> &'static tokio::runtime::Runtime { use std::sync::OnceLock; static RT: OnceLock = OnceLock::new(); - RT.get_or_init(|| + RT.get_or_init(|| { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("could not create tokio runtime") - ) + }) } /// A static reference to [jni::JavaVM] that is set on JNI load. @@ -27,10 +27,7 @@ pub(crate) fn jvm() -> std::sync::Arc { /// Called upon initialisation of the JVM. #[allow(non_snake_case)] #[no_mangle] -pub extern "system" fn JNI_OnLoad( - vm: jni::JavaVM, - _: *mut std::ffi::c_void -) -> jni::sys::jint { +pub extern "system" fn JNI_OnLoad(vm: jni::JavaVM, _: *mut std::ffi::c_void) -> jni::sys::jint { unsafe { JVM = Some(std::sync::Arc::new(vm)) }; jni::sys::JNI_VERSION_1_1 } @@ -48,7 +45,11 @@ pub(crate) fn setup_logger(debug: bool, path: Option) { .with_source_location(false) .compact(); - let level = if debug { tracing::Level::DEBUG } else {tracing::Level::INFO }; + let level = if debug { + tracing::Level::DEBUG + } else { + tracing::Level::INFO + }; let builder = tracing_subscriber::fmt() .event_format(format) @@ -58,13 +59,16 @@ pub(crate) fn setup_logger(debug: bool, path: Option) { let logfile = std::fs::File::create(path).expect("failed creating logfile"); builder.with_writer(std::sync::Mutex::new(logfile)).init(); } else { - builder.with_writer(std::sync::Mutex::new(std::io::stdout())).init(); + builder + .with_writer(std::sync::Mutex::new(std::io::stdout())) + .init(); } } /// Performs a null check on the given variable and throws a NullPointerException on the Java side /// if it is null. Finally, it returns with the given default value. -macro_rules! null_check { // TODO replace +macro_rules! null_check { + // TODO replace ($env: ident, $var: ident, $return: expr) => { if $var.is_null() { let mut message = stringify!($var).to_string(); @@ -79,10 +83,15 @@ pub(crate) use null_check; impl jni_toolbox::JniToolboxError for crate::errors::ConnectionError { fn jclass(&self) -> String { - match self { - crate::errors::ConnectionError::Transport(_) => "mp/code/exceptions/ConnectionTransportException", - crate::errors::ConnectionError::Remote(_) => "mp/code/exceptions/ConnectionRemoteException" - }.to_string() + match self { + crate::errors::ConnectionError::Transport(_) => { + "mp/code/exceptions/ConnectionTransportException" + } + crate::errors::ConnectionError::Remote(_) => { + "mp/code/exceptions/ConnectionRemoteException" + } + } + .to_string() } } @@ -94,10 +103,15 @@ impl jni_toolbox::JniToolboxError for crate::errors::RemoteError { impl jni_toolbox::JniToolboxError for crate::errors::ControllerError { fn jclass(&self) -> String { - match self { - crate::errors::ControllerError::Stopped => "mp/code/exceptions/ControllerStoppedException", - crate::errors::ControllerError::Unfulfilled => "mp/code/exceptions/ControllerUnfulfilledException", - }.to_string() + match self { + crate::errors::ControllerError::Stopped => { + "mp/code/exceptions/ControllerStoppedException" + } + crate::errors::ControllerError::Unfulfilled => { + "mp/code/exceptions/ControllerUnfulfilledException" + } + } + .to_string() } } @@ -106,12 +120,17 @@ macro_rules! into_java_ptr_class { ($type: ty, $jclass: literal) => { impl<'j> jni_toolbox::IntoJavaObject<'j> for $type { const CLASS: &'static str = $jclass; - fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + fn into_java_object( + self, + env: &mut jni::JNIEnv<'j>, + ) -> Result, jni::errors::Error> { let class = env.find_class(Self::CLASS)?; env.new_object( class, "(J)V", - &[jni::objects::JValueGen::Long(Box::into_raw(Box::new(self)) as jni::sys::jlong)] + &[jni::objects::JValueGen::Long( + Box::into_raw(Box::new(self)) as jni::sys::jlong + )], ) } } @@ -125,7 +144,10 @@ into_java_ptr_class!(crate::buffer::Controller, "mp/code/BufferController"); impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::User { const CLASS: &'static str = "mp/code/data/User"; - fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + fn into_java_object( + self, + env: &mut jni::JNIEnv<'j>, + ) -> Result, jni::errors::Error> { let id_field = self.id.into_java_object(env)?; let name_field = env.new_string(self.name)?; let class = env.find_class(Self::CLASS)?; @@ -134,15 +156,18 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::User { "(Ljava/util/UUID;Ljava/lang/String;)V", &[ jni::objects::JValueGen::Object(&id_field), - jni::objects::JValueGen::Object(&name_field) - ] + jni::objects::JValueGen::Object(&name_field), + ], ) } } impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event { const CLASS: &'static str = "mp/code/Workspace$Event"; - fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + fn into_java_object( + self, + env: &mut jni::JNIEnv<'j>, + ) -> Result, jni::errors::Error> { let (ordinal, arg) = match self { crate::api::Event::UserJoin(arg) => (0, env.new_string(arg)?), crate::api::Event::UserLeave(arg) => (1, env.new_string(arg)?), @@ -150,37 +175,44 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event { }; let type_class = env.find_class("mp/code/Workspace$Event$Type")?; - let variants: jni::objects::JObjectArray = env.call_method( - type_class, - "getEnumConstants", - "()[Ljava/lang/Object;", - &[] - )?.l()?.into(); + let variants: jni::objects::JObjectArray = env + .call_method(type_class, "getEnumConstants", "()[Ljava/lang/Object;", &[])? + .l()? + .into(); let event_type = env.get_object_array_element(variants, ordinal)?; - + let event_class = env.find_class(Self::CLASS)?; env.new_object( event_class, "(Lmp/code/Workspace$Event$Type;Ljava/lang/String;)V", &[ jni::objects::JValueGen::Object(&event_type), - jni::objects::JValueGen::Object(&arg) - ] + jni::objects::JValueGen::Object(&arg), + ], ) } } impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::TextChange { const CLASS: &'static str = "mp/code/data/TextChange"; - fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + fn into_java_object( + self, + env: &mut jni::JNIEnv<'j>, + ) -> Result, jni::errors::Error> { let content = env.new_string(self.content)?; let hash_class = env.find_class("java/util/OptionalLong")?; let hash = if let Some(h) = self.hash { - env.call_static_method(hash_class, "of", "(J)Ljava/util/OptionalLong;", &[jni::objects::JValueGen::Long(h)]) + env.call_static_method( + hash_class, + "of", + "(J)Ljava/util/OptionalLong;", + &[jni::objects::JValueGen::Long(h)], + ) } else { env.call_static_method(hash_class, "empty", "()Ljava/util/OptionalLong;", &[]) - }?.l()?; + }? + .l()?; let class = env.find_class(Self::CLASS)?; env.new_object( @@ -190,15 +222,18 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::TextChange { jni::objects::JValueGen::Long(self.start.into()), jni::objects::JValueGen::Long(self.end.into()), jni::objects::JValueGen::Object(&content), - jni::objects::JValueGen::Object(&hash) - ] + jni::objects::JValueGen::Object(&hash), + ], ) } } impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Cursor { const CLASS: &'static str = "mp/code/data/Cursor"; - fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result, jni::errors::Error> { + fn into_java_object( + self, + env: &mut jni::JNIEnv<'j>, + ) -> Result, jni::errors::Error> { let class = env.find_class("mp/code/data/Cursor")?; let buffer = env.new_string(&self.buffer)?; let user = if let Some(user) = self.user { @@ -216,8 +251,8 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Cursor { jni::objects::JValueGen::Int(self.end.0), jni::objects::JValueGen::Int(self.end.1), jni::objects::JValueGen::Object(&buffer), - jni::objects::JValueGen::Object(&user) - ] + jni::objects::JValueGen::Object(&user), + ], ) } } @@ -226,7 +261,10 @@ macro_rules! from_java_ptr { ($type: ty) => { impl<'j> jni_toolbox::FromJava<'j> for &mut $type { type From = jni::sys::jobject; - fn from_java(_env: &mut jni::JNIEnv<'j>, value: Self::From) -> Result { + fn from_java( + _env: &mut jni::JNIEnv<'j>, + value: Self::From, + ) -> Result { Ok(unsafe { Box::leak(Box::from_raw(value as *mut $type)) }) } } @@ -240,9 +278,14 @@ from_java_ptr!(crate::buffer::Controller); impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config { type From = jni::objects::JObject<'j>; - fn from_java(env: &mut jni::JNIEnv<'j>, config: Self::From) -> Result { + fn from_java( + env: &mut jni::JNIEnv<'j>, + config: Self::From, + ) -> Result { let username = { - let jfield = env.get_field(&config, "username", "Ljava/lang/String;")?.l()?; + let jfield = env + .get_field(&config, "username", "Ljava/lang/String;")? + .l()?; if jfield.is_null() { return Err(jni::errors::Error::NullPtr("Username can never be null!")); } @@ -250,7 +293,9 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config { }; let password = { - let jfield = env.get_field(&config, "password", "Ljava/lang/String;")?.l()?; + let jfield = env + .get_field(&config, "password", "Ljava/lang/String;")? + .l()?; if jfield.is_null() { return Err(jni::errors::Error::NullPtr("Password can never be null!")); } @@ -258,9 +303,13 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config { }; let host = { - let jfield = env.get_field(&config, "host", "Ljava/util/Optional;")?.l()?; + let jfield = env + .get_field(&config, "host", "Ljava/util/Optional;")? + .l()?; if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? { - let field = env.call_method(&jfield, "get", "()Ljava/lang/Object;", &[])?.l()?; + let field = env + .call_method(&jfield, "get", "()Ljava/lang/Object;", &[])? + .l()?; Some(unsafe { env.get_string_unchecked(&field.into()) }?.into()) } else { None @@ -268,7 +317,9 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config { }; let port = { - let jfield = env.get_field(&config, "port", "Ljava/util/OptionalInt;")?.l()?; + let jfield = env + .get_field(&config, "port", "Ljava/util/OptionalInt;")? + .l()?; if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? { let ivalue = env.call_method(&jfield, "getAsInt", "()I", &[])?.i()?; Some(ivalue.clamp(0, 65535) as u16) @@ -278,41 +329,61 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config { }; let tls = { - let jfield = env.get_field(&config, "host", "Ljava/util/Optional;")?.l()?; + let jfield = env + .get_field(&config, "host", "Ljava/util/Optional;")? + .l()?; if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? { - let field = env.call_method(&jfield, "get", "()Ljava/lang/Object;", &[])?.l()?; - let bool_true = env.get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;")?.l()?; - Some(env.call_method( - field, - "equals", - "(Ljava/lang/Object;)Z", - &[jni::objects::JValueGen::Object(&bool_true)] - )?.z()?) // what a joke + let field = env + .call_method(&jfield, "get", "()Ljava/lang/Object;", &[])? + .l()?; + let bool_true = env + .get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;")? + .l()?; + Some( + env.call_method( + field, + "equals", + "(Ljava/lang/Object;)Z", + &[jni::objects::JValueGen::Object(&bool_true)], + )? + .z()?, + ) // what a joke } else { None } }; - Ok(Self { username, password, host, port, tls }) + Ok(Self { + username, + password, + host, + port, + tls, + }) } } impl<'j> jni_toolbox::FromJava<'j> for crate::api::Cursor { type From = jni::objects::JObject<'j>; - fn from_java(env: &mut jni::JNIEnv<'j>, cursor: Self::From) -> Result { + fn from_java( + env: &mut jni::JNIEnv<'j>, + cursor: Self::From, + ) -> Result { let start_row = env.get_field(&cursor, "startRow", "I")?.i()?; let start_col = env.get_field(&cursor, "startCol", "I")?.i()?; let end_row = env.get_field(&cursor, "endRow", "I")?.i()?; let end_col = env.get_field(&cursor, "endCol", "I")?.i()?; let buffer = { - let jfield = env.get_field(&cursor, "buffer", "Ljava/lang/String;")?.l()?; + let jfield = env + .get_field(&cursor, "buffer", "Ljava/lang/String;")? + .l()?; if jfield.is_null() { return Err(jni::errors::Error::NullPtr("Buffer can never be null!")); } unsafe { env.get_string_unchecked(&jfield.into()) }?.into() }; - + let user = { let jfield = env.get_field(&cursor, "user", "Ljava/lang/String;")?.l()?; if jfield.is_null() { @@ -322,18 +393,34 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Cursor { } }; - Ok(Self { start: (start_row, start_col), end: (end_row, end_col), buffer, user }) + Ok(Self { + start: (start_row, start_col), + end: (end_row, end_col), + buffer, + user, + }) } } impl<'j> jni_toolbox::FromJava<'j> for crate::api::TextChange { type From = jni::objects::JObject<'j>; - fn from_java(env: &mut jni::JNIEnv<'j>, change: Self::From) -> Result { - let start = env.get_field(&change, "start", "J")?.j()?.clamp(0, u32::MAX.into()) as u32; - let end = env.get_field(&change, "end", "J")?.j()?.clamp(0, u32::MAX.into()) as u32; + fn from_java( + env: &mut jni::JNIEnv<'j>, + change: Self::From, + ) -> Result { + let start = env + .get_field(&change, "start", "J")? + .j()? + .clamp(0, u32::MAX.into()) as u32; + let end = env + .get_field(&change, "end", "J")? + .j()? + .clamp(0, u32::MAX.into()) as u32; let content = { - let jfield = env.get_field(&change, "content", "Ljava/lang/String;")?.l()?; + let jfield = env + .get_field(&change, "content", "Ljava/lang/String;")? + .l()?; if jfield.is_null() { return Err(jni::errors::Error::NullPtr("Content can never be null!")); } @@ -341,13 +428,20 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::TextChange { }; let hash = { - let jfield = env.get_field(&change, "hash", "Ljava/util/OptionalLong;")?.l()?; + let jfield = env + .get_field(&change, "hash", "Ljava/util/OptionalLong;")? + .l()?; if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? { Some(env.call_method(&jfield, "getAsLong", "()J", &[])?.j()?) } else { None } }; - Ok(Self { start, end, content, hash }) + Ok(Self { + start, + end, + content, + hash, + }) } } diff --git a/src/ffi/java/workspace.rs b/src/ffi/java/workspace.rs index c59ebc0..50dde7a 100644 --- a/src/ffi/java/workspace.rs +++ b/src/ffi/java/workspace.rs @@ -1,6 +1,9 @@ -use jni::{objects::JObject, JNIEnv}; +use crate::{ + api::controller::AsyncReceiver, + errors::{ConnectionError, ControllerError, RemoteError}, + Workspace, +}; use jni_toolbox::jni; -use crate::{api::controller::AsyncReceiver, errors::{ConnectionError, ControllerError, RemoteError}, ffi::java::null_check, Workspace}; /// Get the workspace id. #[jni(package = "mp.code", class = "Workspace")] @@ -46,7 +49,10 @@ fn create_buffer(workspace: &mut Workspace, path: String) -> Result<(), RemoteEr /// Attach to a buffer and return a pointer to its [crate::buffer::Controller]. #[jni(package = "mp.code", class = "Workspace")] -fn attach_to_buffer(workspace: &mut Workspace, path: String) -> Result { +fn attach_to_buffer( + workspace: &mut Workspace, + path: String, +) -> Result { super::tokio().block_on(workspace.attach(&path)) } @@ -70,7 +76,10 @@ fn fetch_users(workspace: &mut Workspace) -> Result<(), RemoteError> { /// List users attached to a buffer. #[jni(package = "mp.code", class = "Workspace")] -fn list_buffer_users(workspace: &mut Workspace, path: String) -> Result, RemoteError> { +fn list_buffer_users( + workspace: &mut Workspace, + path: String, +) -> Result, RemoteError> { super::tokio().block_on(workspace.list_buffer_users(&path)) } diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index b26d54d..ce7bca3 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -3,60 +3,73 @@ use napi_derive::napi; use crate::api::TextChange; use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::buffer::controller::BufferController; - +use napi::threadsafe_function::{ + ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, +}; +use napi_derive::napi; #[napi] impl BufferController { - - #[napi(js_name = "callback", ts_args_type = "fun: (event: BufferController) => void")] - pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ - let tsfn : ThreadsafeFunction = - fun.create_threadsafe_function(0, - |ctx : ThreadSafeCallContext| { - Ok(vec![ctx.value]) - } - )?; - self.callback(move |controller : BufferController| { - - tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error + /// Register a callback to be invoked every time a new event is available to consume + /// There can only be one callback registered at any given time. + #[napi( + js_name = "callback", + ts_args_type = "fun: (event: BufferController) => void" + )] + pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()> { + let tsfn: ThreadsafeFunction = fun + .create_threadsafe_function( + 0, + |ctx: ThreadSafeCallContext| { + Ok(vec![ctx.value]) + }, + )?; + self.callback(move |controller: BufferController| { + tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); + //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this - }); Ok(()) } + /// Remove registered buffer callback #[napi(js_name = "clear_callback")] pub fn js_clear_callback(&self) { self.clear_callback(); } - + /// Get buffer path #[napi(js_name = "get_path")] pub fn js_path(&self) -> &str { self.path() } + /// Block until next buffer event without returning it #[napi(js_name = "poll")] - pub async fn js_poll(&self) -> napi::Result<()>{ + pub async fn js_poll(&self) -> napi::Result<()> { Ok(self.poll().await?) } + /// Return next buffer event if present #[napi(js_name = "try_recv")] pub async fn js_try_recv(&self) -> napi::Result> { Ok(self.try_recv().await?) } + /// Wait for next buffer event and return it #[napi(js_name = "recv")] pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?) } + /// Send a buffer update to workspace #[napi(js_name = "send")] pub async fn js_send(&self, op: TextChange) -> napi::Result<()> { Ok(self.send(op).await?) } + /// Return buffer whole content #[napi(js_name = "content")] pub async fn js_content(&self) -> napi::Result { Ok(self.content().await?) diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index c1ea64f..8f63862 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -1,9 +1,34 @@ -use napi_derive::napi; use crate::{Client, Workspace}; +use napi_derive::napi; + +#[napi(object, js_name = "User")] +pub struct JsUser { + pub uuid: String, + pub name: String, +} + +impl TryFrom for crate::api::User { + type Error = ::Err; + fn try_from(value: JsUser) -> Result { + Ok(Self { + id: value.uuid.parse()?, + name: value.name, + }) + } +} + +impl From for JsUser { + fn from(value: crate::api::User) -> Self { + Self { + uuid: value.id.to_string(), + name: value.name, + } + } +} #[napi] /// connect to codemp servers and return a client session -pub async fn connect(config: crate::api::Config) -> napi::Result{ +pub async fn connect(config: crate::api::Config) -> napi::Result { Ok(crate::Client::connect(config).await?) } @@ -23,13 +48,21 @@ impl Client { #[napi(js_name = "list_workspaces")] /// list available workspaces - pub async fn js_list_workspaces(&self, owned: bool, invited: bool) -> napi::Result> { + pub async fn js_list_workspaces( + &self, + owned: bool, + invited: bool, + ) -> napi::Result> { Ok(self.list_workspaces(owned, invited).await?) } #[napi(js_name = "invite_to_workspace")] /// invite user to given workspace, if able to - pub async fn js_invite_to_workspace(&self, workspace: String, user: String) -> napi::Result<()> { + pub async fn js_invite_to_workspace( + &self, + workspace: String, + user: String, + ) -> napi::Result<()> { Ok(self.invite_to_workspace(workspace, user).await?) } @@ -51,10 +84,10 @@ impl Client { self.get_workspace(&workspace) } - #[napi(js_name = "user_id")] + #[napi(js_name = "user")] /// return current sessions's user id - pub fn js_user_id(&self) -> String { - self.user().id.to_string() + pub fn js_user(&self) -> JsUser { + self.user().clone().into() } #[napi(js_name = "active_workspaces")] diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index 7fa0a47..565b059 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -3,7 +3,11 @@ use napi_derive::napi; use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode}; use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::cursor::controller::CursorController; - +use napi::threadsafe_function::ErrorStrategy::Fatal; +use napi::threadsafe_function::{ + ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, +}; +use napi_derive::napi; #[napi(object, js_name = "Cursor")] pub struct JsCursor { @@ -19,64 +23,72 @@ pub struct JsCursor { impl From for crate::api::Cursor { fn from(value: JsCursor) -> Self { crate::api::Cursor { - start : (value.start_row, value.start_col), - end: (value.end_row, value.end_col), + start: (value.start_row, value.start_col), + end: (value.end_row, value.end_col), buffer: value.buffer, user: value.user, } } } + impl From for JsCursor { fn from(value: crate::api::Cursor) -> Self { JsCursor { - start_row : value.start.0, - start_col : value.start.1, - end_row : value.end.0, + start_row: value.start.0, + start_col: value.start.1, + end_row: value.end.0, end_col: value.end.1, buffer: value.buffer, - user: value.user.map(|x| x.to_string()) + user: value.user.map(|x| x.to_string()), } - } } - #[napi] impl CursorController { - - #[napi(js_name = "callback", ts_args_type = "fun: (event: CursorController) => void")] - pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ - let tsfn : ThreadsafeFunction = - fun.create_threadsafe_function(0, - |ctx : ThreadSafeCallContext| { - Ok(vec![ctx.value]) - } - )?; - self.callback(move |controller : CursorController| { - - tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error + /// Register a callback to be called on receive. + /// There can only be one callback registered at any given time. + #[napi( + js_name = "callback", + ts_args_type = "fun: (event: CursorController) => void" + )] + pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()> { + let tsfn: ThreadsafeFunction = fun + .create_threadsafe_function( + 0, + |ctx: ThreadSafeCallContext| { + Ok(vec![ctx.value]) + }, + )?; + self.callback(move |controller: CursorController| { + tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); + //check this with tracing also we could use Ok(event) to get the error // If it blocks the main thread too many time we have to change this - }); Ok(()) } + /// Clear the registered callback + #[napi(js_name = "clear_callback")] + pub fn js_clear_callback(&self) { + self.clear_callback(); + } - + /// Send a new cursor event to remote #[napi(js_name = "send")] pub async fn js_send(&self, pos: JsCursor) -> napi::Result<()> { Ok(self.send(crate::api::Cursor::from(pos)).await?) } - - #[napi(js_name= "try_recv")] + /// Get next cursor event if available without blocking + #[napi(js_name = "try_recv")] pub async fn js_try_recv(&self) -> napi::Result> { - Ok(self.try_recv().await? - .map(JsCursor::from)) + Ok(self.try_recv().await?.map(JsCursor::from)) } - #[napi(js_name= "recv")] + /// Block until next + #[napi(js_name = "recv")] pub async fn js_recv(&self) -> napi::Result { Ok(self.recv().await?.into()) } diff --git a/src/ffi/js/ext.rs b/src/ffi/js/ext.rs index 73795ac..145d634 100644 --- a/src/ffi/js/ext.rs +++ b/src/ffi/js/ext.rs @@ -1,12 +1,13 @@ use napi_derive::napi; - +/// Hash function #[napi(js_name = "hash")] pub fn js_hash(data: String) -> i64 { - crate::ext::hash(data) + crate::ext::hash(data) } +/// Get the current version of the client #[napi(js_name = "version")] pub fn js_version() -> String { - crate::version() + crate::version() } diff --git a/src/ffi/js/mod.rs b/src/ffi/js/mod.rs index ad8b242..4b9d3ee 100644 --- a/src/ffi/js/mod.rs +++ b/src/ffi/js/mod.rs @@ -1,9 +1,8 @@ -pub mod client; -pub mod workspace; -pub mod cursor; pub mod buffer; +pub mod client; +pub mod cursor; pub mod ext; - +pub mod workspace; impl From for napi::Error { fn from(value: crate::errors::ConnectionError) -> Self { @@ -33,7 +32,11 @@ impl JsLogger { #[napi(constructor)] pub fn new(debug: Option) -> JsLogger { let (tx, rx) = tokio::sync::mpsc::channel(256); - let level = if debug.unwrap_or(false) { tracing::Level::DEBUG } else {tracing::Level::INFO }; //TODO: study this tracing subscriber and customize it + let level = if debug.unwrap_or(false) { + tracing::Level::DEBUG + } else { + tracing::Level::INFO + }; //TODO: study this tracing subscriber and customize it let format = tracing_subscriber::fmt::format() .with_level(true) .with_target(true) @@ -55,11 +58,7 @@ impl JsLogger { #[napi] pub async fn message(&self) -> Option { - self.0 - .lock() - .await - .recv() - .await + self.0.lock().await.recv().await } } @@ -73,5 +72,7 @@ impl std::io::Write for JsLoggerProducer { Ok(buf.len()) } - fn flush(&mut self) -> std::io::Result<()> { Ok(()) } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } } diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index 775a2e2..a7e3ba7 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -6,6 +6,7 @@ use crate::buffer::controller::BufferController; use crate::cursor::controller::CursorController; use crate::api::controller::AsyncReceiver; + #[napi(object, js_name = "Event")] pub struct JsEvent { pub r#type: String, @@ -15,50 +16,73 @@ pub struct JsEvent { impl From for JsEvent { fn from(value: crate::api::Event) -> Self { match value { - crate::api::Event::FileTreeUpdated(value) => Self { r#type: "filetree".into(), value }, - crate::api::Event::UserJoin(value) => Self { r#type: "join".into(), value }, - crate::api::Event::UserLeave(value) => Self { r#type: "leave".into(), value }, + crate::api::Event::FileTreeUpdated(value) => Self { + r#type: "filetree".into(), + value, + }, + crate::api::Event::UserJoin(value) => Self { + r#type: "join".into(), + value, + }, + crate::api::Event::UserLeave(value) => Self { + r#type: "leave".into(), + value, + }, } } } #[napi] impl Workspace { + /// Get the unique workspace id #[napi(js_name = "id")] pub fn js_id(&self) -> String { self.id() } - + + /// List all available buffers in this workspace #[napi(js_name = "filetree")] pub fn js_filetree(&self, filter: Option<&str>, strict: bool) -> Vec { self.filetree(filter, strict) } + /// List all user names currently in this workspace #[napi(js_name = "user_list")] pub fn js_user_list(&self) -> Vec { self.user_list() } + /// List all currently active buffers + #[napi(js_name = "buffer_list")] + pub fn js_buffer_list(&self) -> Vec { + self.buffer_list() + } + + /// Get workspace's Cursor Controller #[napi(js_name = "cursor")] pub fn js_cursor(&self) -> CursorController { self.cursor() } + /// Get a buffer controller by its name (path) #[napi(js_name = "buffer_by_name")] pub fn js_buffer_by_name(&self, path: String) -> Option { self.buffer_by_name(&path) } + /// Create a new buffer in the current workspace #[napi(js_name = "create")] pub async fn js_create(&self, path: String) -> napi::Result<()> { Ok(self.create(&path).await?) } + /// Attach to a workspace buffer, starting a BufferController #[napi(js_name = "attach")] pub async fn js_attach(&self, path: String) -> napi::Result { Ok(self.attach(&path).await?) } - + + /// Delete a buffer from workspace #[napi(js_name = "delete")] pub async fn js_delete(&self, path: String) -> napi::Result<()> { Ok(self.delete(&path).await?) @@ -81,7 +105,7 @@ impl Workspace { } #[napi(js_name = "clear_callback")] - pub fn js_clear_callbacl(&self) -> napi::Result<()> { + pub fn js_clear_callback(&self) -> napi::Result<()> { self.clear_callback(); Ok(()) } @@ -103,4 +127,34 @@ impl Workspace { Ok(()) } + + /// Detach from an active buffer, stopping its underlying worker + /// this method returns true if no reference or last reference was held, false if there are still + /// dangling references to clear + #[napi(js_name = "detach")] + pub async fn js_detach(&self, path: String) -> bool { + self.detach(&path) + } + + /// Re-fetch remote buffer list + #[napi(js_name = "fetch_buffers")] + pub async fn js_fetch_buffers(&self) -> napi::Result<()> { + Ok(self.fetch_buffers().await?) + } + /// Re-fetch the list of all users in the workspace. + #[napi(js_name = "fetch_users")] + pub async fn js_fetch_users(&self) -> napi::Result<()> { + Ok(self.fetch_users().await?) + } + + /// List users attached to a specific buffer + #[napi(js_name = "list_buffer_users")] + pub async fn js_list_buffer_users(&self, path: String) -> napi::Result> { + Ok(self + .list_buffer_users(&path) + .await? + .into_iter() + .map(JsUser::from) + .collect()) + } } diff --git a/src/ffi/lua/buffer.rs b/src/ffi/lua/buffer.rs index d2733c1..76c89cd 100644 --- a/src/ffi/lua/buffer.rs +++ b/src/ffi/lua/buffer.rs @@ -1,28 +1,41 @@ -use mlua_codemp_patch as mlua; -use mlua::prelude::*; use crate::prelude::*; +use mlua::prelude::*; +use mlua_codemp_patch as mlua; use super::ext::a_sync::a_sync; use super::ext::from_lua_serde; - impl LuaUserData for CodempBufferController { fn add_methods>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("{:?}", this)) + }); - methods.add_method("send", |_, this, (change,): (CodempTextChange,)| - a_sync! { this => this.send(change).await? } + methods.add_method( + "send", + |_, this, (change,): (CodempTextChange,)| a_sync! { this => this.send(change).await? }, ); - methods.add_method("try_recv", |_, this, ()| a_sync! { this => this.try_recv().await? }); + methods.add_method( + "try_recv", + |_, this, ()| a_sync! { this => this.try_recv().await? }, + ); methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? }); methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? }); - methods.add_method("content", |_, this, ()| a_sync! { this => this.content().await? }); + methods.add_method( + "content", + |_, this, ()| a_sync! { this => this.content().await? }, + ); - methods.add_method("clear_callback", |_, this, ()| { this.clear_callback(); Ok(()) }); - methods.add_method("callback", |_, this, (cb,):(LuaFunction,)| { - this.callback(move |controller: CodempBufferController| super::ext::callback().invoke(cb.clone(), controller)); + methods.add_method("clear_callback", |_, this, ()| { + this.clear_callback(); + Ok(()) + }); + methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| { + this.callback(move |controller: CodempBufferController| { + super::ext::callback().invoke(cb.clone(), controller) + }); Ok(()) }); } @@ -32,15 +45,17 @@ from_lua_serde! { CodempTextChange } impl LuaUserData for CodempTextChange { fn add_fields>(fields: &mut F) { fields.add_field_method_get("content", |_, this| Ok(this.content.clone())); - fields.add_field_method_get("start", |_, this| Ok(this.start)); - fields.add_field_method_get("end", |_, this| Ok(this.end)); - fields.add_field_method_get("hash", |_, this| Ok(this.hash)); + fields.add_field_method_get("start", |_, this| Ok(this.start)); + fields.add_field_method_get("end", |_, this| Ok(this.end)); + fields.add_field_method_get("hash", |_, this| Ok(this.hash)); // add a 'finish' accessor too because in Lua 'end' is reserved - fields.add_field_method_get("finish", |_, this| Ok(this.end)); + fields.add_field_method_get("finish", |_, this| Ok(this.end)); } fn add_methods>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); - methods.add_method("apply", |_, this, (txt,):(String,)| Ok(this.apply(&txt))); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("{:?}", this)) + }); + methods.add_method("apply", |_, this, (txt,): (String,)| Ok(this.apply(&txt))); } } diff --git a/src/ffi/lua/client.rs b/src/ffi/lua/client.rs index e5baf77..6198671 100644 --- a/src/ffi/lua/client.rs +++ b/src/ffi/lua/client.rs @@ -1,6 +1,6 @@ -use mlua_codemp_patch as mlua; -use mlua::prelude::*; use crate::prelude::*; +use mlua::prelude::*; +use mlua_codemp_patch as mlua; use super::ext::a_sync::a_sync; use super::ext::from_lua_serde; @@ -13,22 +13,28 @@ impl LuaUserData for CodempClient { } fn add_methods>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("{:?}", this)) + }); - methods.add_method("refresh", |_, this, ()| - a_sync! { this => this.refresh().await? } + methods.add_method( + "refresh", + |_, this, ()| a_sync! { this => this.refresh().await? }, ); - methods.add_method("join_workspace", |_, this, (ws,):(String,)| - a_sync! { this => this.join_workspace(ws).await? } + methods.add_method( + "join_workspace", + |_, this, (ws,): (String,)| a_sync! { this => this.join_workspace(ws).await? }, ); - methods.add_method("create_workspace", |_, this, (ws,):(String,)| - a_sync! { this => this.create_workspace(ws).await? } + methods.add_method( + "create_workspace", + |_, this, (ws,): (String,)| a_sync! { this => this.create_workspace(ws).await? }, ); - methods.add_method("delete_workspace", |_, this, (ws,):(String,)| - a_sync! { this => this.delete_workspace(ws).await? } + methods.add_method( + "delete_workspace", + |_, this, (ws,): (String,)| a_sync! { this => this.delete_workspace(ws).await? }, ); methods.add_method("invite_to_workspace", |_, this, (ws,user):(String,String)| @@ -39,11 +45,13 @@ impl LuaUserData for CodempClient { a_sync! { this => this.list_workspaces(owned.unwrap_or(true), invited.unwrap_or(true)).await? } ); - methods.add_method("leave_workspace", |_, this, (ws,):(String,)| + methods.add_method("leave_workspace", |_, this, (ws,): (String,)| { Ok(this.leave_workspace(&ws)) - ); - - methods.add_method("get_workspace", |_, this, (ws,):(String,)| Ok(this.get_workspace(&ws))); + }); + + methods.add_method("get_workspace", |_, this, (ws,): (String,)| { + Ok(this.get_workspace(&ws)) + }); } } diff --git a/src/ffi/lua/cursor.rs b/src/ffi/lua/cursor.rs index 98eca44..3deea7c 100644 --- a/src/ffi/lua/cursor.rs +++ b/src/ffi/lua/cursor.rs @@ -1,6 +1,6 @@ -use mlua_codemp_patch as mlua; -use mlua::prelude::*; use crate::prelude::*; +use mlua::prelude::*; +use mlua_codemp_patch as mlua; use super::ext::a_sync::a_sync; use super::ext::from_lua_serde; @@ -8,20 +8,29 @@ use super::ext::lua_tuple; impl LuaUserData for CodempCursorController { fn add_methods>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("{:?}", this)) + }); - methods.add_method("send", |_, this, (cursor,):(CodempCursor,)| - a_sync! { this => this.send(cursor).await? } + methods.add_method( + "send", + |_, this, (cursor,): (CodempCursor,)| a_sync! { this => this.send(cursor).await? }, ); - methods.add_method("try_recv", |_, this, ()| - a_sync! { this => this.try_recv().await? } + methods.add_method( + "try_recv", + |_, this, ()| a_sync! { this => this.try_recv().await? }, ); methods.add_method("recv", |_, this, ()| a_sync! { this => this.recv().await? }); methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().await? }); - methods.add_method("clear_callback", |_, this, ()| { this.clear_callback(); Ok(()) }); - methods.add_method("callback", |_, this, (cb,):(LuaFunction,)| { - this.callback(move |controller: CodempCursorController| super::ext::callback().invoke(cb.clone(), controller)); + methods.add_method("clear_callback", |_, this, ()| { + this.clear_callback(); + Ok(()) + }); + methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| { + this.callback(move |controller: CodempCursorController| { + super::ext::callback().invoke(cb.clone(), controller) + }); Ok(()) }); } @@ -30,13 +39,15 @@ impl LuaUserData for CodempCursorController { from_lua_serde! { CodempCursor } impl LuaUserData for CodempCursor { fn add_methods>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("{:?}", this)) + }); } fn add_fields>(fields: &mut F) { fields.add_field_method_get("user", |_, this| Ok(this.user.clone())); fields.add_field_method_get("buffer", |_, this| Ok(this.buffer.clone())); - fields.add_field_method_get("start", |lua, this| lua_tuple(lua, this.start)); + fields.add_field_method_get("start", |lua, this| lua_tuple(lua, this.start)); fields.add_field_method_get("end", |lua, this| lua_tuple(lua, this.end)); // add a 'finish' accessor too because in Lua 'end' is reserved fields.add_field_method_get("finish", |lua, this| lua_tuple(lua, this.end)); diff --git a/src/ffi/lua/ext/a_sync.rs b/src/ffi/lua/ext/a_sync.rs index 54bc20c..e53181d 100644 --- a/src/ffi/lua/ext/a_sync.rs +++ b/src/ffi/lua/ext/a_sync.rs @@ -1,15 +1,15 @@ -use mlua_codemp_patch as mlua; use mlua::prelude::*; +use mlua_codemp_patch as mlua; pub(crate) fn tokio() -> &'static tokio::runtime::Runtime { use std::sync::OnceLock; static RT: OnceLock = OnceLock::new(); - RT.get_or_init(|| + RT.get_or_init(|| { tokio::runtime::Builder::new_current_thread() .enable_all() .build() .expect("could not create tokio runtime") - ) + }) } macro_rules! a_sync { @@ -32,45 +32,53 @@ macro_rules! a_sync { pub(crate) use a_sync; -pub(crate) struct Promise(pub(crate) Option>>); +pub(crate) struct Promise( + pub(crate) Option>>, +); impl LuaUserData for Promise { fn add_fields>(fields: &mut F) { - fields.add_field_method_get("ready", |_, this| + fields.add_field_method_get("ready", |_, this| { Ok(this.0.as_ref().map_or(true, |x| x.is_finished())) - ); + }); } fn add_methods>(methods: &mut M) { // TODO: await MUST NOT be used in callbacks!! methods.add_method_mut("await", |_, this, ()| match this.0.take() { None => Err(LuaError::runtime("Promise already awaited")), - Some(x) => { - tokio() - .block_on(x) - .map_err(LuaError::runtime)? - }, + Some(x) => tokio().block_on(x).map_err(LuaError::runtime)?, }); - methods.add_method_mut("and_then", |_, this, (cb,):(LuaFunction,)| match this.0.take() { + methods.add_method_mut("cancel", |_, this, ()| match this.0.take() { None => Err(LuaError::runtime("Promise already awaited")), Some(x) => { - tokio() - .spawn(async move { + x.abort(); + Ok(()) + }, + }); + methods.add_method_mut("and_then", |_, this, (cb,): (LuaFunction,)| { + match this.0.take() { + None => Err(LuaError::runtime("Promise already awaited")), + Some(x) => { + tokio().spawn(async move { match x.await { - Err(e) => tracing::error!("could not join promise to run callback: {e}"), + Err(e) => { + tracing::error!("could not join promise to run callback: {e}") + } Ok(res) => match res { Err(e) => super::callback().failure(e), Ok(val) => super::callback().invoke(cb, val), }, } }); - Ok(()) - }, + Ok(()) + } + } }); } } -pub(crate) fn setup_driver(_: &Lua, (block,):(Option,)) -> LuaResult> { +pub(crate) fn setup_driver(_: &Lua, (block,): (Option,)) -> LuaResult> { let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let future = async move { tracing::info!(" :: driving runtime..."); @@ -89,25 +97,26 @@ pub(crate) fn setup_driver(_: &Lua, (block,):(Option,)) -> LuaResult, Option>); +pub(crate) struct Driver( + pub(crate) tokio::sync::mpsc::UnboundedSender<()>, + Option>, +); impl LuaUserData for Driver { fn add_methods>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); - methods.add_method_mut("stop", |_, this, ()| { - match this.1.take() { - None => Ok(false), - Some(handle) => { - if this.0.send(()).is_err() { - tracing::warn!("found runtime already stopped while attempting to stop it"); - } - match handle.join() { - Err(e) => Err(LuaError::runtime(format!("runtime thread panicked: {e:?}"))), - Ok(()) => Ok(true), - } - }, + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("{:?}", this)) + }); + methods.add_method_mut("stop", |_, this, ()| match this.1.take() { + None => Ok(false), + Some(handle) => { + if this.0.send(()).is_err() { + tracing::warn!("found runtime already stopped while attempting to stop it"); + } + match handle.join() { + Err(e) => Err(LuaError::runtime(format!("runtime thread panicked: {e:?}"))), + Ok(()) => Ok(true), + } } }); } } - - diff --git a/src/ffi/lua/ext/callback.rs b/src/ffi/lua/ext/callback.rs index db118d7..dd84098 100644 --- a/src/ffi/lua/ext/callback.rs +++ b/src/ffi/lua/ext/callback.rs @@ -1,7 +1,7 @@ -use mlua_codemp_patch as mlua; -use mlua::prelude::*; -use crate::prelude::*; use crate::ext::IgnorableError; +use crate::prelude::*; +use mlua::prelude::*; +use mlua_codemp_patch as mlua; pub(crate) fn callback() -> &'static CallbackChannel { static CHANNEL: std::sync::OnceLock> = std::sync::OnceLock::new(); @@ -10,7 +10,7 @@ pub(crate) fn callback() -> &'static CallbackChannel { pub(crate) struct CallbackChannel { tx: std::sync::Arc>, - rx: std::sync::Mutex> + rx: std::sync::Mutex>, } impl Default for CallbackChannel { @@ -26,12 +26,16 @@ impl Default for CallbackChannel { impl CallbackChannel { pub(crate) fn invoke(&self, cb: LuaFunction, arg: impl Into) { - self.tx.send(LuaCallback::Invoke(cb, arg.into())) + self.tx + .send(LuaCallback::Invoke(cb, arg.into())) .unwrap_or_warn("error scheduling callback") } pub(crate) fn failure(&self, err: impl std::error::Error) { - self.tx.send(LuaCallback::Fail(format!("promise failed with error: {err:?}"))) + self.tx + .send(LuaCallback::Fail(format!( + "promise failed with error: {err:?}" + ))) .unwrap_or_warn("error scheduling callback failure") } @@ -40,12 +44,12 @@ impl CallbackChannel { Err(e) => { tracing::debug!("backing off from callback mutex: {e}"); None - }, + } Ok(mut lock) => match lock.try_recv() { Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => { tracing::error!("callback channel closed"); None - }, + } Err(tokio::sync::mpsc::error::TryRecvError::Empty) => None, Ok(cb) => Some(cb), }, diff --git a/src/ffi/lua/ext/log.rs b/src/ffi/lua/ext/log.rs index 5553660..e22bc64 100644 --- a/src/ffi/lua/ext/log.rs +++ b/src/ffi/lua/ext/log.rs @@ -1,7 +1,7 @@ use std::{io::Write, sync::Mutex}; -use mlua_codemp_patch as mlua; use mlua::prelude::*; +use mlua_codemp_patch as mlua; use tokio::sync::mpsc; #[derive(Debug, Clone)] @@ -12,12 +12,21 @@ impl Write for LuaLoggerProducer { Ok(buf.len()) } - fn flush(&mut self) -> std::io::Result<()> { Ok(()) } + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } } // TODO can we make this less verbose? -pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option)) -> LuaResult { - let level = if debug.unwrap_or_default() { tracing::Level::DEBUG } else {tracing::Level::INFO }; +pub(crate) fn setup_tracing( + _: &Lua, + (printer, debug): (LuaValue, Option), +) -> LuaResult { + let level = if debug.unwrap_or_default() { + tracing::Level::DEBUG + } else { + tracing::Level::INFO + }; let format = tracing_subscriber::fmt::format() .with_level(true) .with_target(true) @@ -36,16 +45,15 @@ pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option)) | LuaValue::Thread(_) | LuaValue::UserData(_) | LuaValue::Error(_) => return Err(LuaError::BindError), // TODO full BadArgument type?? - LuaValue::Nil => { - tracing_subscriber::fmt() - .event_format(format) - .with_max_level(level) - .with_writer(std::sync::Mutex::new(std::io::stderr())) - .try_init() - .is_ok() - }, + LuaValue::Nil => tracing_subscriber::fmt() + .event_format(format) + .with_max_level(level) + .with_writer(std::sync::Mutex::new(std::io::stderr())) + .try_init() + .is_ok(), LuaValue::String(path) => { - let logfile = std::fs::File::create(path.to_string_lossy()).map_err(|e| LuaError::RuntimeError(e.to_string()))?; + let logfile = std::fs::File::create(path.to_string_lossy()) + .map_err(|e| LuaError::RuntimeError(e.to_string()))?; tracing_subscriber::fmt() .event_format(format) .with_max_level(level) @@ -53,7 +61,7 @@ pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option)) .with_ansi(false) .try_init() .is_ok() - }, + } LuaValue::Function(cb) => { let (tx, mut rx) = mpsc::unbounded_channel(); let res = tracing_subscriber::fmt() @@ -71,7 +79,7 @@ pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option)) }); } res - }, + } }; Ok(success) diff --git a/src/ffi/lua/ext/mod.rs b/src/ffi/lua/ext/mod.rs index 081f22a..c30a742 100644 --- a/src/ffi/lua/ext/mod.rs +++ b/src/ffi/lua/ext/mod.rs @@ -2,8 +2,8 @@ pub mod a_sync; pub mod callback; pub mod log; -use mlua_codemp_patch as mlua; use mlua::prelude::*; +use mlua_codemp_patch as mlua; pub(crate) use a_sync::tokio; pub(crate) use callback::callback; diff --git a/src/ffi/lua/mod.rs b/src/ffi/lua/mod.rs index f6d0788..33792d0 100644 --- a/src/ffi/lua/mod.rs +++ b/src/ffi/lua/mod.rs @@ -1,73 +1,99 @@ -mod client; -mod workspace; -mod cursor; mod buffer; +mod client; +mod cursor; mod ext; +mod workspace; -use mlua_codemp_patch as mlua; -use mlua::prelude::*; use crate::prelude::*; +use mlua::prelude::*; +use mlua_codemp_patch as mlua; // define multiple entrypoints, so this library can have multiple names and still work -#[mlua::lua_module(name = "codemp")] fn entry_1(lua: &Lua) -> LuaResult { entrypoint(lua) } -#[mlua::lua_module(name = "libcodemp")] fn entry_2(lua: &Lua) -> LuaResult { entrypoint(lua) } -#[mlua::lua_module(name = "codemp_native")] fn entry_3(lua: &Lua) -> LuaResult { entrypoint(lua) } -#[mlua::lua_module(name = "codemp_lua")] fn entry_4(lua: &Lua) -> LuaResult { entrypoint(lua) } +#[mlua::lua_module(name = "codemp")] +fn entry_1(lua: &Lua) -> LuaResult { + entrypoint(lua) +} +#[mlua::lua_module(name = "libcodemp")] +fn entry_2(lua: &Lua) -> LuaResult { + entrypoint(lua) +} +#[mlua::lua_module(name = "codemp_native")] +fn entry_3(lua: &Lua) -> LuaResult { + entrypoint(lua) +} +#[mlua::lua_module(name = "codemp_lua")] +fn entry_4(lua: &Lua) -> LuaResult { + entrypoint(lua) +} fn entrypoint(lua: &Lua) -> LuaResult { let exports = lua.create_table()?; // entrypoint - exports.set("connect", lua.create_function(|_, (config,):(CodempConfig,)| - ext::a_sync::a_sync! { => CodempClient::connect(config).await? } - )?)?; + exports.set( + "connect", + lua.create_function( + |_, (config,): (CodempConfig,)| ext::a_sync::a_sync! { => CodempClient::connect(config).await? }, + )?, + )?; // utils - exports.set("hash", lua.create_function(|_, (txt,):(String,)| - Ok(crate::ext::hash(txt)) - )?)?; + exports.set( + "hash", + lua.create_function(|_, (txt,): (String,)| Ok(crate::ext::hash(txt)))?, + )?; - exports.set("version", lua.create_function(|_, ()| - Ok(crate::version()) - )?)?; + exports.set( + "version", + lua.create_function(|_, ()| Ok(crate::version()))?, + )?; // runtime - exports.set("setup_driver", lua.create_function(ext::a_sync::setup_driver)?)?; - exports.set("poll_callback", lua.create_function(|lua, ()| { - let mut val = LuaMultiValue::new(); - match ext::callback().recv() { - None => {}, - Some(ext::callback::LuaCallback::Invoke(cb, arg)) => { - val.push_back(LuaValue::Function(cb)); - val.push_back(arg.into_lua(lua)?); + exports.set( + "setup_driver", + lua.create_function(ext::a_sync::setup_driver)?, + )?; + exports.set( + "poll_callback", + lua.create_function(|lua, ()| { + let mut val = LuaMultiValue::new(); + match ext::callback().recv() { + None => {} + Some(ext::callback::LuaCallback::Invoke(cb, arg)) => { + val.push_back(LuaValue::Function(cb)); + val.push_back(arg.into_lua(lua)?); + } + Some(ext::callback::LuaCallback::Fail(msg)) => { + val.push_back(false.into_lua(lua)?); + val.push_back(msg.into_lua(lua)?); + } } - Some(ext::callback::LuaCallback::Fail(msg)) => { - val.push_back(false.into_lua(lua)?); - val.push_back(msg.into_lua(lua)?); - }, - } - Ok(val) - })?)?; + Ok(val) + })?, + )?; // logging - exports.set("setup_tracing", lua.create_function(ext::log::setup_tracing)?)?; + exports.set( + "setup_tracing", + lua.create_function(ext::log::setup_tracing)?, + )?; Ok(exports) } -impl From:: for LuaError { +impl From for LuaError { fn from(value: crate::errors::ConnectionError) -> Self { LuaError::runtime(value.to_string()) } } -impl From:: for LuaError { +impl From for LuaError { fn from(value: crate::errors::RemoteError) -> Self { LuaError::runtime(value.to_string()) } } -impl From:: for LuaError { +impl From for LuaError { fn from(value: crate::errors::ControllerError) -> Self { LuaError::runtime(value.to_string()) } diff --git a/src/ffi/lua/workspace.rs b/src/ffi/lua/workspace.rs index fc87c60..dfa707e 100644 --- a/src/ffi/lua/workspace.rs +++ b/src/ffi/lua/workspace.rs @@ -1,40 +1,52 @@ -use mlua_codemp_patch as mlua; -use mlua::prelude::*; use crate::prelude::*; +use mlua::prelude::*; +use mlua_codemp_patch as mlua; use super::ext::a_sync::a_sync; use super::ext::from_lua_serde; impl LuaUserData for CodempWorkspace { fn add_methods>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); - methods.add_method("create", |_, this, (name,):(String,)| - a_sync! { this => this.create(&name).await? } + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("{:?}", this)) + }); + methods.add_method( + "create", + |_, this, (name,): (String,)| a_sync! { this => this.create(&name).await? }, ); - methods.add_method("attach", |_, this, (name,):(String,)| - a_sync! { this => this.attach(&name).await? } + methods.add_method( + "attach", + |_, this, (name,): (String,)| a_sync! { this => this.attach(&name).await? }, ); - methods.add_method("detach", |_, this, (name,):(String,)| + methods.add_method("detach", |_, this, (name,): (String,)| { Ok(this.detach(&name)) + }); + + methods.add_method( + "delete", + |_, this, (name,): (String,)| a_sync! { this => this.delete(&name).await? }, ); - methods.add_method("delete", |_, this, (name,):(String,)| - a_sync! { this => this.delete(&name).await? } - ); - - methods.add_method("get_buffer", |_, this, (name,):(String,)| Ok(this.buffer_by_name(&name))); + methods.add_method("get_buffer", |_, this, (name,): (String,)| { + Ok(this.buffer_by_name(&name)) + }); methods.add_method("fetch_buffers", |_, this, ()| a_sync! { this => this.fetch_buffers().await? } + ); - methods.add_method("fetch_users", |_, this, ()| - a_sync! { this => this.fetch_users().await? } + methods.add_method( + "fetch_users", + |_, this, ()| a_sync! { this => this.fetch_users().await? }, ); - methods.add_method("filetree", |_, this, (filter, strict,):(Option, Option,)| - Ok(this.filetree(filter.as_deref(), strict.unwrap_or(false))) + methods.add_method( + "filetree", + |_, this, (filter, strict): (Option, Option)| { + Ok(this.filetree(filter.as_deref(), strict.unwrap_or(false))) + }, ); methods.add_method("user_list", |_, this, ()| @@ -62,6 +74,7 @@ impl LuaUserData for CodempWorkspace { this.clear_callback(); Ok(()) }); + } fn add_fields>(fields: &mut F) { @@ -75,7 +88,9 @@ impl LuaUserData for CodempWorkspace { from_lua_serde! { CodempEvent } impl LuaUserData for CodempEvent { fn add_methods>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| { + Ok(format!("{:?}", this)) + }); } fn add_fields>(fields: &mut F) { @@ -86,9 +101,8 @@ impl LuaUserData for CodempEvent { }); fields.add_field_method_get("value", |_, this| match this { CodempEvent::FileTreeUpdated(x) - | CodempEvent::UserJoin(x) - | CodempEvent::UserLeave(x) - => Ok(x.clone()), + | CodempEvent::UserJoin(x) + | CodempEvent::UserLeave(x) => Ok(x.clone()), }); } } diff --git a/src/network.rs b/src/network.rs index 8f8489b..aaa39c1 100644 --- a/src/network.rs +++ b/src/network.rs @@ -1,5 +1,5 @@ use codemp_proto::{ - common::Token, buffer::buffer_client::BufferClient, cursor::cursor_client::CursorClient, + buffer::buffer_client::BufferClient, common::Token, cursor::cursor_client::CursorClient, workspace::workspace_client::WorkspaceClient, }; use tonic::{ @@ -14,10 +14,7 @@ type AuthedService = InterceptedService; #[derive(Debug, Clone)] pub struct SessionInterceptor(pub tokio::sync::watch::Receiver); impl tonic::service::Interceptor for SessionInterceptor { - fn call( - &mut self, - mut request: tonic::Request<()>, - ) -> tonic::Result> { + fn call(&mut self, mut request: tonic::Request<()>) -> tonic::Result> { if let Ok(token) = self.0.borrow().token.parse() { request.metadata_mut().insert("session", token); } diff --git a/src/prelude.rs b/src/prelude.rs index 2e0a640..41f3bec 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -11,10 +11,8 @@ pub use crate::api::{ Event as CodempEvent, Config as CodempConfig, }; - + pub use crate::{ - client::Client as CodempClient, - workspace::Workspace as CodempWorkspace, - cursor::Controller as CodempCursorController, - buffer::Controller as CodempBufferController, + buffer::Controller as CodempBufferController, client::Client as CodempClient, + cursor::Controller as CodempCursorController, workspace::Workspace as CodempWorkspace, }; diff --git a/src/workspace.rs b/src/workspace.rs index 87d2f83..a767c51 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -134,6 +134,11 @@ impl Workspace { Ok(ws) } + /// drop arc, return true if was last + pub(crate) fn consume(self) -> bool { + Arc::into_inner(self.0).is_some() + } + /// Create a new buffer in the current workspace. pub async fn create(&self, path: &str) -> RemoteResult<()> { let mut workspace_client = self.0.services.ws(); @@ -189,9 +194,9 @@ impl Workspace { match self.0.buffers.remove(path) { None => true, // noop: we werent attached in the first place Some((_name, controller)) => match Arc::into_inner(controller.0) { - None => false, // dangling ref! we can't drop this + None => false, // dangling ref! we can't drop this Some(_) => true, // dropping it now - } + }, } } @@ -261,7 +266,6 @@ impl Workspace { })) .await?; - self.0.filetree.remove(path); Ok(()) @@ -308,7 +312,7 @@ impl Workspace { /// A filter may be applied, and it may be strict (equality check) or not (starts_with check). // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 pub fn filetree(&self, filter: Option<&str>, strict: bool) -> Vec { - self.0 + let mut tree = self.0 .filetree .iter() .filter(|f| { @@ -321,7 +325,9 @@ impl Workspace { }) }) .map(|f| f.clone()) - .collect() + .collect::>(); + tree.sort(); + tree } pub(crate) fn run_actor( @@ -333,13 +339,18 @@ impl Workspace { let weak = Arc::downgrade(&self.0); let name = self.id(); tokio::spawn(async move { + tracing::debug!("workspace worker starting"); loop { // TODO can we stop responsively rather than poll for Arc being dropped? - if weak.upgrade().is_none() { break }; + if weak.upgrade().is_none() { + break; + }; let Some(res) = tokio::select!( x = stream.message() => Some(x), _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => None, - ) else { continue }; + ) else { + continue; + }; match res { Err(e) => break tracing::error!("workspace '{}' stream closed: {}", name, e), Ok(None) => break tracing::info!("leaving workspace {}", name), @@ -376,6 +387,7 @@ impl Workspace { } } } + tracing::debug!("workspace worker stopping"); }); } }