Merge branch 'dev' into feat/workspace-receiver

This commit is contained in:
əlemi 2024-10-03 03:52:42 +02:00 committed by GitHub
commit 535de6c2fc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 790 additions and 378 deletions

View file

@ -11,7 +11,7 @@ authors = [
] ]
license = "GPL-3.0-only" license = "GPL-3.0-only"
edition = "2021" edition = "2021"
version = "0.7.2" version = "0.7.3"
exclude = ["dist/*"] exclude = ["dist/*"]
[lib] [lib]

View file

@ -5,7 +5,7 @@
[![Crates.io Version](https://img.shields.io/crates/v/codemp)](https://crates.io/crates/codemp) [![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) [![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 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. > `codemp` is a **collaborative** text editing solution to work remotely.

View file

@ -20,7 +20,7 @@ fn main() {
{ {
if let Ok("macos") = std::env::var("CARGO_CFG_TARGET_OS").as_deref() { 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=-undefined");
println!("cargo:rustc-cdylib-link-arg=dynamic_lookup"); println!("cargo:rustc-cdylib-link-arg=dynamic_lookup");
} }
} }
} }

View file

@ -5,7 +5,7 @@ plugins {
} }
group = 'mp.code' group = 'mp.code'
version = '0.7.2' version = '0.7.3'
java { java {
sourceCompatibility = targetCompatibility = JavaVersion.VERSION_11 sourceCompatibility = targetCompatibility = JavaVersion.VERSION_11

View file

@ -1,6 +1,6 @@
{ {
"name": "@codemp/native", "name": "@codemp/native",
"version": "0.7.2", "version": "0.7.3",
"description": "code multiplexer -- javascript bindings", "description": "code multiplexer -- javascript bindings",
"keywords": [ "keywords": [
"codemp", "codemp",
@ -35,8 +35,8 @@
} }
}, },
"optionalDependencies": { "optionalDependencies": {
"@codemp/native-win32-x64-msvc": "0.7.2", "@codemp/native-win32-x64-msvc": "0.7.3",
"@codemp/native-darwin-arm64": "0.7.2", "@codemp/native-darwin-arm64": "0.7.3",
"@codemp/native-linux-x64-gnu": "0.7.2" "@codemp/native-linux-x64-gnu": "0.7.3"
} }
} }

View file

@ -18,6 +18,9 @@ local NilPromise = {}
--- block until promise is ready --- block until promise is ready
function NilPromise:await() end function NilPromise:await() end
--- cancel promise execution
function NilPromise:cancel() end
---@param cb fun() callback to invoke ---@param cb fun() callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function NilPromise:and_then(cb) end function NilPromise:and_then(cb) end
@ -30,6 +33,9 @@ local StringPromise = {}
--- @return string --- @return string
function StringPromise:await() end function StringPromise:await() end
--- cancel promise execution
function StringPromise:cancel() end
---@param cb fun(x: string) callback to invoke ---@param cb fun(x: string) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function StringPromise:and_then(cb) end function StringPromise:and_then(cb) end
@ -40,6 +46,8 @@ local StringArrayPromise = {}
--- block until promise is ready and return value --- block until promise is ready and return value
--- @return string[] --- @return string[]
function StringArrayPromise:await() end function StringArrayPromise:await() end
--- cancel promise execution
function StringArrayPromise:cancel() end
---@param cb fun(x: string[]) callback to invoke ---@param cb fun(x: string[]) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function StringArrayPromise:and_then(cb) end function StringArrayPromise:and_then(cb) end
@ -50,6 +58,8 @@ local ClientPromise = {}
--- block until promise is ready and return value --- block until promise is ready and return value
--- @return Client --- @return Client
function ClientPromise:await() end function ClientPromise:await() end
--- cancel promise execution
function ClientPromise:cancel() end
---@param cb fun(x: Client) callback to invoke ---@param cb fun(x: Client) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function ClientPromise:and_then(cb) end function ClientPromise:and_then(cb) end
@ -60,6 +70,8 @@ local WorkspacePromise = {}
--- block until promise is ready and return value --- block until promise is ready and return value
--- @return Workspace --- @return Workspace
function WorkspacePromise:await() end function WorkspacePromise:await() end
--- cancel promise execution
function WorkspacePromise:cancel() end
---@param cb fun(x: Workspace) callback to invoke ---@param cb fun(x: Workspace) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function WorkspacePromise:and_then(cb) end function WorkspacePromise:and_then(cb) end
@ -70,6 +82,8 @@ local WorkspaceEventPromise = {}
--- block until promise is ready and return value --- block until promise is ready and return value
--- @return WorkspaceEvent --- @return WorkspaceEvent
function WorkspaceEventPromise:await() end function WorkspaceEventPromise:await() end
--- cancel promise execution
function WorkspaceEventPromise:cancel() end
---@param cb fun(x: WorkspaceEvent) callback to invoke ---@param cb fun(x: WorkspaceEvent) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function WorkspaceEventPromise:and_then(cb) end function WorkspaceEventPromise:and_then(cb) end
@ -90,6 +104,8 @@ local BufferControllerPromise = {}
--- block until promise is ready and return value --- block until promise is ready and return value
--- @return BufferController --- @return BufferController
function BufferControllerPromise:await() end function BufferControllerPromise:await() end
--- cancel promise execution
function BufferControllerPromise:cancel() end
---@param cb fun(x: BufferController) callback to invoke ---@param cb fun(x: BufferController) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function BufferControllerPromise:and_then(cb) end function BufferControllerPromise:and_then(cb) end
@ -100,6 +116,8 @@ local CursorPromise = {}
--- block until promise is ready and return value --- block until promise is ready and return value
--- @return Cursor --- @return Cursor
function CursorPromise:await() end function CursorPromise:await() end
--- cancel promise execution
function CursorPromise:cancel() end
---@param cb fun(x: Cursor) callback to invoke ---@param cb fun(x: Cursor) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function CursorPromise:and_then(cb) end function CursorPromise:and_then(cb) end
@ -110,6 +128,8 @@ local MaybeCursorPromise = {}
--- block until promise is ready and return value --- block until promise is ready and return value
--- @return Cursor | nil --- @return Cursor | nil
function MaybeCursorPromise:await() end function MaybeCursorPromise:await() end
--- cancel promise execution
function MaybeCursorPromise:cancel() end
---@param cb fun(x: Cursor | nil) callback to invoke ---@param cb fun(x: Cursor | nil) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function MaybeCursorPromise:and_then(cb) end function MaybeCursorPromise:and_then(cb) end
@ -120,6 +140,8 @@ local TextChangePromise = {}
--- block until promise is ready and return value --- block until promise is ready and return value
--- @return TextChange --- @return TextChange
function TextChangePromise:await() end function TextChangePromise:await() end
--- cancel promise execution
function TextChangePromise:cancel() end
---@param cb fun(x: TextChange) callback to invoke ---@param cb fun(x: TextChange) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function TextChangePromise:and_then(cb) end function TextChangePromise:and_then(cb) end
@ -130,6 +152,8 @@ local MaybeTextChangePromise = {}
--- block until promise is ready and return value --- block until promise is ready and return value
--- @return TextChange | nil --- @return TextChange | nil
function MaybeTextChangePromise:await() end function MaybeTextChangePromise:await() end
--- cancel promise execution
function MaybeTextChangePromise:cancel() end
---@param cb fun(x: TextChange | nil) callback to invoke ---@param cb fun(x: TextChange | nil) callback to invoke
---invoke callback asynchronously as soon as promise is ready ---invoke callback asynchronously as soon as promise is ready
function MaybeTextChangePromise:and_then(cb) end function MaybeTextChangePromise:and_then(cb) end

View file

@ -1,9 +1,9 @@
package = "codemp" package = "codemp"
version = "0.7.2-1" version = "0.7.3-1"
source = { source = {
url = "git+https://github.com/hexedtech/codemp", url = "git+https://github.com/hexedtech/codemp",
tag = "v0.7.2", tag = "v0.7.3",
} }
dependencies = { dependencies = {

View file

@ -1,6 +1,6 @@
[project] [project]
name = "codemp" name = "codemp"
version = "0.7.2" version = "0.7.3"
description = "code multiplexer" description = "code multiplexer"
requires-python = ">=3.8" requires-python = ">=3.8"
license = "GPL-3.0-only" license = "GPL-3.0-only"

View file

@ -75,7 +75,6 @@ pub trait AsyncReceiver<T : Sized + Send + Sync> : Sized + Send + Sync {
async fn try_recv(&self) -> ControllerResult<Option<T>>; async fn try_recv(&self) -> ControllerResult<Option<T>>;
} }
/// Type wrapper for Boxed dynamic callback. /// Type wrapper for Boxed dynamic callback.
pub struct ControllerCallback<T>(pub Box<dyn Sync + Send + Fn(T)>); pub struct ControllerCallback<T>(pub Box<dyn Sync + Send + Fn(T)>);

View file

@ -19,9 +19,9 @@ pub mod event;
/// data structure for remote users /// data structure for remote users
pub mod user; pub mod user;
pub use controller::Controller;
pub use change::TextChange; pub use change::TextChange;
pub use config::Config; pub use config::Config;
pub use controller::Controller;
pub use cursor::Cursor; pub use cursor::Cursor;
pub use event::Event; pub use event::Event;
pub use user::User; pub use user::User;

View file

@ -34,7 +34,12 @@ struct BufferWorker {
} }
impl BufferController { impl BufferController {
pub(crate) fn spawn(user_id: Uuid, path: &str, tx: mpsc::Sender<Operation>, rx: Streaming<BufferEvent>) -> Self { pub(crate) fn spawn(
user_id: Uuid,
path: &str,
tx: mpsc::Sender<Operation>,
rx: Streaming<BufferEvent>,
) -> Self {
let init = diamond_types::LocalVersion::default(); let init = diamond_types::LocalVersion::default();
let (latest_version_tx, latest_version_rx) = watch::channel(init.clone()); let (latest_version_tx, latest_version_rx) = watch::channel(init.clone());
@ -75,17 +80,21 @@ impl BufferController {
timer: Timer::new(10), // TODO configurable! timer: Timer::new(10), // TODO configurable!
}; };
tokio::spawn(async move { tokio::spawn(async move { BufferController::work(worker, tx, rx).await });
BufferController::work(worker, tx, rx).await
});
BufferController(controller) BufferController(controller)
} }
async fn work(mut worker: BufferWorker, tx: mpsc::Sender<Operation>, mut rx: Streaming<BufferEvent>) { async fn work(
mut worker: BufferWorker,
tx: mpsc::Sender<Operation>,
mut rx: Streaming<BufferEvent>,
) {
tracing::debug!("controller worker started"); tracing::debug!("controller worker started");
loop { loop {
if worker.controller.upgrade().is_none() { break }; if worker.controller.upgrade().is_none() {
break;
};
// block until one of these is ready // block until one of these is ready
tokio::select! { tokio::select! {

View file

@ -182,7 +182,10 @@ impl Client {
/// Leave the [`Workspace`] with the given name. /// Leave the [`Workspace`] with the given name.
pub fn leave_workspace(&self, id: &str) -> bool { 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. /// Gets a [`Workspace`] handle by name.

View file

@ -4,8 +4,11 @@ use tokio::sync::{mpsc, oneshot, watch};
use tonic::Streaming; use tonic::Streaming;
use uuid::Uuid; use uuid::Uuid;
use crate::{api::{controller::ControllerCallback, Cursor, User}, ext::IgnorableError}; use crate::{
use codemp_proto::cursor::{CursorPosition, CursorEvent}; api::{controller::ControllerCallback, Cursor, User},
ext::IgnorableError,
};
use codemp_proto::cursor::{CursorEvent, CursorPosition};
use super::controller::{CursorController, CursorControllerInner}; use super::controller::{CursorController, CursorControllerInner};
@ -21,7 +24,11 @@ struct CursorWorker {
} }
impl CursorController { impl CursorController {
pub(crate) fn spawn(user_map: Arc<dashmap::DashMap<Uuid, User>>, tx: mpsc::Sender<CursorPosition>, rx: Streaming<CursorEvent>) -> Self { pub(crate) fn spawn(
user_map: Arc<dashmap::DashMap<Uuid, User>>,
tx: mpsc::Sender<CursorPosition>,
rx: Streaming<CursorEvent>,
) -> Self {
// TODO we should tweak the channel buffer size to better propagate backpressure // TODO we should tweak the channel buffer size to better propagate backpressure
let (op_tx, op_rx) = mpsc::channel(64); let (op_tx, op_rx) = mpsc::channel(64);
let (stream_tx, stream_rx) = mpsc::channel(1); let (stream_tx, stream_rx) = mpsc::channel(1);
@ -52,11 +59,17 @@ impl CursorController {
CursorController(controller) CursorController(controller)
} }
async fn work(mut worker: CursorWorker, tx: mpsc::Sender<CursorPosition>, mut rx: Streaming<CursorEvent>) { async fn work(
mut worker: CursorWorker,
tx: mpsc::Sender<CursorPosition>,
mut rx: Streaming<CursorEvent>,
) {
loop { loop {
tracing::debug!("cursor worker polling"); tracing::debug!("cursor worker polling");
if worker.controller.upgrade().is_none() { break }; // clean exit: all controllers dropped if worker.controller.upgrade().is_none() {
tokio::select!{ break;
}; // clean exit: all controllers dropped
tokio::select! {
biased; biased;
// new poller // new poller

View file

@ -61,4 +61,3 @@ impl From<tokio::sync::oneshot::error::RecvError> for ControllerError {
/// Wraps [std::result::Result] with a [ControllerError]. /// Wraps [std::result::Result] with a [ControllerError].
pub type ControllerResult<T> = std::result::Result<T, ControllerError>; pub type ControllerResult<T> = std::result::Result<T, ControllerError>;

View file

@ -16,7 +16,7 @@ use tokio::sync::mpsc;
pub async fn select_buffer( pub async fn select_buffer(
buffers: &[crate::buffer::Controller], buffers: &[crate::buffer::Controller],
timeout: Option<std::time::Duration>, timeout: Option<std::time::Duration>,
runtime: &tokio::runtime::Runtime runtime: &tokio::runtime::Runtime,
) -> ControllerResult<Option<crate::buffer::Controller>> { ) -> ControllerResult<Option<crate::buffer::Controller>> {
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = mpsc::unbounded_channel();
let mut tasks = Vec::new(); let mut tasks = Vec::new();
@ -46,7 +46,7 @@ pub async fn select_buffer(
t.abort(); t.abort();
} }
return Ok(x); return Ok(x);
}, }
} }
} }
} }
@ -104,11 +104,13 @@ pub trait IgnorableError {
} }
impl<T, E> IgnorableError for std::result::Result<T, E> impl<T, E> IgnorableError for std::result::Result<T, E>
where E : std::fmt::Debug { where
E: std::fmt::Debug,
{
/// Logs the error as a warning and returns a unit. /// Logs the error as a warning and returns a unit.
fn unwrap_or_warn(self, msg: &str) { fn unwrap_or_warn(self, msg: &str) {
match self { match self {
Ok(_) => {}, Ok(_) => {}
Err(e) => tracing::warn!("{}: {:?}", msg, e), Err(e) => tracing::warn!("{}: {:?}", msg, e),
} }
} }

View file

@ -1,7 +1,10 @@
use jni::{objects::JObject, JNIEnv}; use jni::{objects::JObject, JNIEnv};
use jni_toolbox::jni; 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; use super::null_check;
@ -19,7 +22,9 @@ fn get_content(controller: &mut crate::buffer::Controller) -> Result<String, Con
/// Try to fetch a [TextChange], or return null if there's nothing. /// Try to fetch a [TextChange], or return null if there's nothing.
#[jni(package = "mp.code", class = "BufferController")] #[jni(package = "mp.code", class = "BufferController")]
fn try_recv(controller: &mut crate::buffer::Controller) -> Result<Option<TextChange>, ControllerError> { fn try_recv(
controller: &mut crate::buffer::Controller,
) -> Result<Option<TextChange>, ControllerError> {
super::tokio().block_on(controller.try_recv()) super::tokio().block_on(controller.try_recv())
} }
@ -31,23 +36,34 @@ fn recv(controller: &mut crate::buffer::Controller) -> Result<TextChange, Contro
/// Send a [TextChange] to the server. /// Send a [TextChange] to the server.
#[jni(package = "mp.code", class = "BufferController")] #[jni(package = "mp.code", class = "BufferController")]
fn send(controller: &mut crate::buffer::Controller, change: TextChange) -> Result<(), ControllerError> { fn send(
controller: &mut crate::buffer::Controller,
change: TextChange,
) -> Result<(), ControllerError> {
super::tokio().block_on(controller.send(change)) super::tokio().block_on(controller.send(change))
} }
/// Register a callback for buffer changes. /// Register a callback for buffer changes.
#[jni(package = "mp.code", class = "BufferController")] #[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, {}); null_check!(env, cb, {});
let Ok(cb_ref) = env.new_global_ref(cb) else { let Ok(cb_ref) = env.new_global_ref(cb) else {
env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!") env.throw_new(
.expect("Failed to throw exception!"); "mp/code/exceptions/JNIException",
"Failed to pin callback reference!",
)
.expect("Failed to throw exception!");
return; return;
}; };
controller.callback(move |controller: crate::buffer::Controller| { controller.callback(move |controller: crate::buffer::Controller| {
let jvm = super::jvm(); 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"); .expect("failed attaching to main JVM thread");
if let Err(e) = env.with_local_frame(5, |env| { if let Err(e) = env.with_local_frame(5, |env| {
use jni_toolbox::IntoJavaObject; use jni_toolbox::IntoJavaObject;
@ -56,7 +72,7 @@ fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::buffer::Co
&cb_ref, &cb_ref,
"accept", "accept",
"(Ljava/lang/Object;)V", "(Ljava/lang/Object;)V",
&[jni::objects::JValueGen::Object(&jcontroller)] &[jni::objects::JValueGen::Object(&jcontroller)],
) { ) {
tracing::error!("error invoking callback: {e:?}"); tracing::error!("error invoking callback: {e:?}");
}; };

View file

@ -1,5 +1,10 @@
use crate::{
api::Config,
client::Client,
errors::{ConnectionError, RemoteError},
Workspace,
};
use jni_toolbox::jni; 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. /// Connect using the given credentials to the default server, and return a [Client] to interact with it.
#[jni(package = "mp.code", class = "Client", ptr)] #[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. /// Invite another user to an owned workspace.
#[jni(package = "mp.code", class = "Client")] #[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)) super::tokio().block_on(client.invite_to_workspace(workspace, user))
} }
/// List available workspaces. /// List available workspaces.
#[jni(package = "mp.code", class = "Client")] #[jni(package = "mp.code", class = "Client")]
fn list_workspaces(client: &mut Client, owned: bool, invited: bool) -> Result<Vec<String>, RemoteError> { fn list_workspaces(
client: &mut Client,
owned: bool,
invited: bool,
) -> Result<Vec<String>, RemoteError> {
super::tokio().block_on(client.list_workspaces(owned, invited)) super::tokio().block_on(client.list_workspaces(owned, invited))
} }
@ -52,7 +65,7 @@ fn active_workspaces(client: &mut Client) -> Vec<String> {
/// Leave a [Workspace] and return whether or not the client was in such workspace. /// Leave a [Workspace] and return whether or not the client was in such workspace.
#[jni(package = "mp.code", class = "Client")] #[jni(package = "mp.code", class = "Client")]
fn leave_workspace(client: &mut Client, workspace: String) -> bool { 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. /// Get a [Workspace] by name and returns a pointer to it.

View file

@ -1,6 +1,13 @@
use crate::{
api::{Controller, Cursor},
errors::ControllerError,
};
use jni::{objects::JObject, JNIEnv}; use jni::{objects::JObject, JNIEnv};
use jni_toolbox::jni; 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; use super::null_check;
@ -24,17 +31,25 @@ fn send(controller: &mut crate::cursor::Controller, cursor: Cursor) -> Result<()
/// Register a callback for cursor changes. /// Register a callback for cursor changes.
#[jni(package = "mp.code", class = "CursorController")] #[jni(package = "mp.code", class = "CursorController")]
fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::cursor::Controller, cb: JObject<'local>) { fn callback<'local>(
env: &mut JNIEnv<'local>,
controller: &mut crate::cursor::Controller,
cb: JObject<'local>,
) {
null_check!(env, cb, {}); null_check!(env, cb, {});
let Ok(cb_ref) = env.new_global_ref(cb) else { let Ok(cb_ref) = env.new_global_ref(cb) else {
env.throw_new("mp/code/exceptions/JNIException", "Failed to pin callback reference!") env.throw_new(
.expect("Failed to throw exception!"); "mp/code/exceptions/JNIException",
"Failed to pin callback reference!",
)
.expect("Failed to throw exception!");
return; return;
}; };
controller.callback(move |controller: crate::cursor::Controller| { controller.callback(move |controller: crate::cursor::Controller| {
let jvm = super::jvm(); 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"); .expect("failed attaching to main JVM thread");
if let Err(e) = env.with_local_frame(5, |env| { if let Err(e) = env.with_local_frame(5, |env| {
use jni_toolbox::IntoJavaObject; use jni_toolbox::IntoJavaObject;
@ -43,7 +58,7 @@ fn callback<'local>(env: &mut JNIEnv<'local>, controller: &mut crate::cursor::Co
&cb_ref, &cb_ref,
"accept", "accept",
"(Ljava/lang/Object;)V", "(Ljava/lang/Object;)V",
&[jni::objects::JValueGen::Object(&jcontroller)] &[jni::objects::JValueGen::Object(&jcontroller)],
) { ) {
tracing::error!("error invoking callback: {e:?}"); tracing::error!("error invoking callback: {e:?}");
}; };

View file

@ -1,19 +1,19 @@
pub mod client;
pub mod workspace;
pub mod cursor;
pub mod buffer; pub mod buffer;
pub mod client;
pub mod cursor;
pub mod ext; pub mod ext;
pub mod workspace;
/// Gets or creates the relevant [tokio::runtime::Runtime]. /// Gets or creates the relevant [tokio::runtime::Runtime].
fn tokio() -> &'static tokio::runtime::Runtime { fn tokio() -> &'static tokio::runtime::Runtime {
use std::sync::OnceLock; use std::sync::OnceLock;
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new(); static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
RT.get_or_init(|| RT.get_or_init(|| {
tokio::runtime::Builder::new_current_thread() tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build()
.expect("could not create tokio runtime") .expect("could not create tokio runtime")
) })
} }
/// A static reference to [jni::JavaVM] that is set on JNI load. /// A static reference to [jni::JavaVM] that is set on JNI load.
@ -27,10 +27,7 @@ pub(crate) fn jvm() -> std::sync::Arc<jni::JavaVM> {
/// Called upon initialisation of the JVM. /// Called upon initialisation of the JVM.
#[allow(non_snake_case)] #[allow(non_snake_case)]
#[no_mangle] #[no_mangle]
pub extern "system" fn JNI_OnLoad( pub extern "system" fn JNI_OnLoad(vm: jni::JavaVM, _: *mut std::ffi::c_void) -> jni::sys::jint {
vm: jni::JavaVM,
_: *mut std::ffi::c_void
) -> jni::sys::jint {
unsafe { JVM = Some(std::sync::Arc::new(vm)) }; unsafe { JVM = Some(std::sync::Arc::new(vm)) };
jni::sys::JNI_VERSION_1_1 jni::sys::JNI_VERSION_1_1
} }
@ -48,7 +45,11 @@ pub(crate) fn setup_logger(debug: bool, path: Option<String>) {
.with_source_location(false) .with_source_location(false)
.compact(); .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() let builder = tracing_subscriber::fmt()
.event_format(format) .event_format(format)
@ -58,13 +59,16 @@ pub(crate) fn setup_logger(debug: bool, path: Option<String>) {
let logfile = std::fs::File::create(path).expect("failed creating logfile"); let logfile = std::fs::File::create(path).expect("failed creating logfile");
builder.with_writer(std::sync::Mutex::new(logfile)).init(); builder.with_writer(std::sync::Mutex::new(logfile)).init();
} else { } 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 /// 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. /// 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) => { ($env: ident, $var: ident, $return: expr) => {
if $var.is_null() { if $var.is_null() {
let mut message = stringify!($var).to_string(); let mut message = stringify!($var).to_string();
@ -80,9 +84,14 @@ pub(crate) use null_check;
impl jni_toolbox::JniToolboxError for crate::errors::ConnectionError { impl jni_toolbox::JniToolboxError for crate::errors::ConnectionError {
fn jclass(&self) -> String { fn jclass(&self) -> String {
match self { match self {
crate::errors::ConnectionError::Transport(_) => "mp/code/exceptions/ConnectionTransportException", crate::errors::ConnectionError::Transport(_) => {
crate::errors::ConnectionError::Remote(_) => "mp/code/exceptions/ConnectionRemoteException" "mp/code/exceptions/ConnectionTransportException"
}.to_string() }
crate::errors::ConnectionError::Remote(_) => {
"mp/code/exceptions/ConnectionRemoteException"
}
}
.to_string()
} }
} }
@ -95,9 +104,14 @@ impl jni_toolbox::JniToolboxError for crate::errors::RemoteError {
impl jni_toolbox::JniToolboxError for crate::errors::ControllerError { impl jni_toolbox::JniToolboxError for crate::errors::ControllerError {
fn jclass(&self) -> String { fn jclass(&self) -> String {
match self { match self {
crate::errors::ControllerError::Stopped => "mp/code/exceptions/ControllerStoppedException", crate::errors::ControllerError::Stopped => {
crate::errors::ControllerError::Unfulfilled => "mp/code/exceptions/ControllerUnfulfilledException", "mp/code/exceptions/ControllerStoppedException"
}.to_string() }
crate::errors::ControllerError::Unfulfilled => {
"mp/code/exceptions/ControllerUnfulfilledException"
}
}
.to_string()
} }
} }
@ -106,12 +120,17 @@ macro_rules! into_java_ptr_class {
($type: ty, $jclass: literal) => { ($type: ty, $jclass: literal) => {
impl<'j> jni_toolbox::IntoJavaObject<'j> for $type { impl<'j> jni_toolbox::IntoJavaObject<'j> for $type {
const CLASS: &'static str = $jclass; const CLASS: &'static str = $jclass;
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> { fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let class = env.find_class(Self::CLASS)?; let class = env.find_class(Self::CLASS)?;
env.new_object( env.new_object(
class, class,
"(J)V", "(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 { impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::User {
const CLASS: &'static str = "mp/code/data/User"; const CLASS: &'static str = "mp/code/data/User";
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> { fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let id_field = self.id.into_java_object(env)?; let id_field = self.id.into_java_object(env)?;
let name_field = env.new_string(self.name)?; let name_field = env.new_string(self.name)?;
let class = env.find_class(Self::CLASS)?; 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", "(Ljava/util/UUID;Ljava/lang/String;)V",
&[ &[
jni::objects::JValueGen::Object(&id_field), 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 { impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event {
const CLASS: &'static str = "mp/code/Workspace$Event"; const CLASS: &'static str = "mp/code/Workspace$Event";
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> { fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let (ordinal, arg) = match self { let (ordinal, arg) = match self {
crate::api::Event::UserJoin(arg) => (0, env.new_string(arg)?), crate::api::Event::UserJoin(arg) => (0, env.new_string(arg)?),
crate::api::Event::UserLeave(arg) => (1, env.new_string(arg)?), crate::api::Event::UserLeave(arg) => (1, env.new_string(arg)?),
@ -150,12 +175,10 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event {
}; };
let type_class = env.find_class("mp/code/Workspace$Event$Type")?; let type_class = env.find_class("mp/code/Workspace$Event$Type")?;
let variants: jni::objects::JObjectArray = env.call_method( let variants: jni::objects::JObjectArray = env
type_class, .call_method(type_class, "getEnumConstants", "()[Ljava/lang/Object;", &[])?
"getEnumConstants", .l()?
"()[Ljava/lang/Object;", .into();
&[]
)?.l()?.into();
let event_type = env.get_object_array_element(variants, ordinal)?; let event_type = env.get_object_array_element(variants, ordinal)?;
let event_class = env.find_class(Self::CLASS)?; let event_class = env.find_class(Self::CLASS)?;
@ -164,23 +187,32 @@ impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Event {
"(Lmp/code/Workspace$Event$Type;Ljava/lang/String;)V", "(Lmp/code/Workspace$Event$Type;Ljava/lang/String;)V",
&[ &[
jni::objects::JValueGen::Object(&event_type), 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 { impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::TextChange {
const CLASS: &'static str = "mp/code/data/TextChange"; const CLASS: &'static str = "mp/code/data/TextChange";
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> { fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let content = env.new_string(self.content)?; let content = env.new_string(self.content)?;
let hash_class = env.find_class("java/util/OptionalLong")?; let hash_class = env.find_class("java/util/OptionalLong")?;
let hash = if let Some(h) = self.hash { 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 { } else {
env.call_static_method(hash_class, "empty", "()Ljava/util/OptionalLong;", &[]) env.call_static_method(hash_class, "empty", "()Ljava/util/OptionalLong;", &[])
}?.l()?; }?
.l()?;
let class = env.find_class(Self::CLASS)?; let class = env.find_class(Self::CLASS)?;
env.new_object( 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.start.into()),
jni::objects::JValueGen::Long(self.end.into()), jni::objects::JValueGen::Long(self.end.into()),
jni::objects::JValueGen::Object(&content), 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 { impl<'j> jni_toolbox::IntoJavaObject<'j> for crate::api::Cursor {
const CLASS: &'static str = "mp/code/data/Cursor"; const CLASS: &'static str = "mp/code/data/Cursor";
fn into_java_object(self, env: &mut jni::JNIEnv<'j>) -> Result<jni::objects::JObject<'j>, jni::errors::Error> { fn into_java_object(
self,
env: &mut jni::JNIEnv<'j>,
) -> Result<jni::objects::JObject<'j>, jni::errors::Error> {
let class = env.find_class("mp/code/data/Cursor")?; let class = env.find_class("mp/code/data/Cursor")?;
let buffer = env.new_string(&self.buffer)?; let buffer = env.new_string(&self.buffer)?;
let user = if let Some(user) = self.user { 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.0),
jni::objects::JValueGen::Int(self.end.1), jni::objects::JValueGen::Int(self.end.1),
jni::objects::JValueGen::Object(&buffer), 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) => { ($type: ty) => {
impl<'j> jni_toolbox::FromJava<'j> for &mut $type { impl<'j> jni_toolbox::FromJava<'j> for &mut $type {
type From = jni::sys::jobject; type From = jni::sys::jobject;
fn from_java(_env: &mut jni::JNIEnv<'j>, value: Self::From) -> Result<Self, jni::errors::Error> { fn from_java(
_env: &mut jni::JNIEnv<'j>,
value: Self::From,
) -> Result<Self, jni::errors::Error> {
Ok(unsafe { Box::leak(Box::from_raw(value as *mut $type)) }) 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 { impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config {
type From = jni::objects::JObject<'j>; type From = jni::objects::JObject<'j>;
fn from_java(env: &mut jni::JNIEnv<'j>, config: Self::From) -> Result<Self, jni::errors::Error> { fn from_java(
env: &mut jni::JNIEnv<'j>,
config: Self::From,
) -> Result<Self, jni::errors::Error> {
let username = { 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() { if jfield.is_null() {
return Err(jni::errors::Error::NullPtr("Username can never be 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 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() { if jfield.is_null() {
return Err(jni::errors::Error::NullPtr("Password can never be 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 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()? { 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()) Some(unsafe { env.get_string_unchecked(&field.into()) }?.into())
} else { } else {
None None
@ -268,7 +317,9 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config {
}; };
let port = { 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()? { if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? {
let ivalue = env.call_method(&jfield, "getAsInt", "()I", &[])?.i()?; let ivalue = env.call_method(&jfield, "getAsInt", "()I", &[])?.i()?;
Some(ivalue.clamp(0, 65535) as u16) Some(ivalue.clamp(0, 65535) as u16)
@ -278,35 +329,55 @@ impl<'j> jni_toolbox::FromJava<'j> for crate::api::Config {
}; };
let tls = { 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()? { if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? {
let field = env.call_method(&jfield, "get", "()Ljava/lang/Object;", &[])?.l()?; let field = env
let bool_true = env.get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;")?.l()?; .call_method(&jfield, "get", "()Ljava/lang/Object;", &[])?
Some(env.call_method( .l()?;
field, let bool_true = env
"equals", .get_static_field("java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;")?
"(Ljava/lang/Object;)Z", .l()?;
&[jni::objects::JValueGen::Object(&bool_true)] Some(
)?.z()?) // what a joke env.call_method(
field,
"equals",
"(Ljava/lang/Object;)Z",
&[jni::objects::JValueGen::Object(&bool_true)],
)?
.z()?,
) // what a joke
} else { } else {
None 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 { impl<'j> jni_toolbox::FromJava<'j> for crate::api::Cursor {
type From = jni::objects::JObject<'j>; type From = jni::objects::JObject<'j>;
fn from_java(env: &mut jni::JNIEnv<'j>, cursor: Self::From) -> Result<Self, jni::errors::Error> { fn from_java(
env: &mut jni::JNIEnv<'j>,
cursor: Self::From,
) -> Result<Self, jni::errors::Error> {
let start_row = env.get_field(&cursor, "startRow", "I")?.i()?; let start_row = env.get_field(&cursor, "startRow", "I")?.i()?;
let start_col = env.get_field(&cursor, "startCol", "I")?.i()?; let start_col = env.get_field(&cursor, "startCol", "I")?.i()?;
let end_row = env.get_field(&cursor, "endRow", "I")?.i()?; let end_row = env.get_field(&cursor, "endRow", "I")?.i()?;
let end_col = env.get_field(&cursor, "endCol", "I")?.i()?; let end_col = env.get_field(&cursor, "endCol", "I")?.i()?;
let buffer = { 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() { if jfield.is_null() {
return Err(jni::errors::Error::NullPtr("Buffer can never be null!")); return Err(jni::errors::Error::NullPtr("Buffer can never be 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 { impl<'j> jni_toolbox::FromJava<'j> for crate::api::TextChange {
type From = jni::objects::JObject<'j>; type From = jni::objects::JObject<'j>;
fn from_java(env: &mut jni::JNIEnv<'j>, change: Self::From) -> Result<Self, jni::errors::Error> { fn from_java(
let start = env.get_field(&change, "start", "J")?.j()?.clamp(0, u32::MAX.into()) as u32; env: &mut jni::JNIEnv<'j>,
let end = env.get_field(&change, "end", "J")?.j()?.clamp(0, u32::MAX.into()) as u32; change: Self::From,
) -> Result<Self, jni::errors::Error> {
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 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() { if jfield.is_null() {
return Err(jni::errors::Error::NullPtr("Content can never be 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 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()? { if env.call_method(&jfield, "isPresent", "()Z", &[])?.z()? {
Some(env.call_method(&jfield, "getAsLong", "()J", &[])?.j()?) Some(env.call_method(&jfield, "getAsLong", "()J", &[])?.j()?)
} else { } else {
None None
} }
}; };
Ok(Self { start, end, content, hash }) Ok(Self {
start,
end,
content,
hash,
})
} }
} }

View file

@ -1,6 +1,9 @@
use jni::{objects::JObject, JNIEnv}; use crate::{
api::controller::AsyncReceiver,
errors::{ConnectionError, ControllerError, RemoteError},
Workspace,
};
use jni_toolbox::jni; use jni_toolbox::jni;
use crate::{api::controller::AsyncReceiver, errors::{ConnectionError, ControllerError, RemoteError}, ffi::java::null_check, Workspace};
/// Get the workspace id. /// Get the workspace id.
#[jni(package = "mp.code", class = "Workspace")] #[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]. /// Attach to a buffer and return a pointer to its [crate::buffer::Controller].
#[jni(package = "mp.code", class = "Workspace")] #[jni(package = "mp.code", class = "Workspace")]
fn attach_to_buffer(workspace: &mut Workspace, path: String) -> Result<crate::buffer::Controller, ConnectionError> { fn attach_to_buffer(
workspace: &mut Workspace,
path: String,
) -> Result<crate::buffer::Controller, ConnectionError> {
super::tokio().block_on(workspace.attach(&path)) 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. /// List users attached to a buffer.
#[jni(package = "mp.code", class = "Workspace")] #[jni(package = "mp.code", class = "Workspace")]
fn list_buffer_users(workspace: &mut Workspace, path: String) -> Result<Vec<crate::api::User>, RemoteError> { fn list_buffer_users(
workspace: &mut Workspace,
path: String,
) -> Result<Vec<crate::api::User>, RemoteError> {
super::tokio().block_on(workspace.list_buffer_users(&path)) super::tokio().block_on(workspace.list_buffer_users(&path))
} }

View file

@ -3,60 +3,73 @@ use napi_derive::napi;
use crate::api::TextChange; use crate::api::TextChange;
use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::api::controller::{AsyncReceiver, AsyncSender};
use crate::buffer::controller::BufferController; use crate::buffer::controller::BufferController;
use napi::threadsafe_function::{
ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
use napi_derive::napi;
#[napi] #[napi]
impl BufferController { impl BufferController {
/// Register a callback to be invoked every time a new event is available to consume
#[napi(js_name = "callback", ts_args_type = "fun: (event: BufferController) => void")] /// There can only be one callback registered at any given time.
pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ #[napi(
let tsfn : ThreadsafeFunction<crate::buffer::controller::BufferController, Fatal> = js_name = "callback",
fun.create_threadsafe_function(0, ts_args_type = "fun: (event: BufferController) => void"
|ctx : ThreadSafeCallContext<crate::buffer::controller::BufferController>| { )]
Ok(vec![ctx.value]) pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()> {
} let tsfn: ThreadsafeFunction<crate::buffer::controller::BufferController, Fatal> = fun
)?; .create_threadsafe_function(
self.callback(move |controller : BufferController| { 0,
|ctx: ThreadSafeCallContext<crate::buffer::controller::BufferController>| {
tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error 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 // If it blocks the main thread too many time we have to change this
}); });
Ok(()) Ok(())
} }
/// Remove registered buffer callback
#[napi(js_name = "clear_callback")] #[napi(js_name = "clear_callback")]
pub fn js_clear_callback(&self) { pub fn js_clear_callback(&self) {
self.clear_callback(); self.clear_callback();
} }
/// Get buffer path
#[napi(js_name = "get_path")] #[napi(js_name = "get_path")]
pub fn js_path(&self) -> &str { pub fn js_path(&self) -> &str {
self.path() self.path()
} }
/// Block until next buffer event without returning it
#[napi(js_name = "poll")] #[napi(js_name = "poll")]
pub async fn js_poll(&self) -> napi::Result<()>{ pub async fn js_poll(&self) -> napi::Result<()> {
Ok(self.poll().await?) Ok(self.poll().await?)
} }
/// Return next buffer event if present
#[napi(js_name = "try_recv")] #[napi(js_name = "try_recv")]
pub async fn js_try_recv(&self) -> napi::Result<Option<TextChange>> { pub async fn js_try_recv(&self) -> napi::Result<Option<TextChange>> {
Ok(self.try_recv().await?) Ok(self.try_recv().await?)
} }
/// Wait for next buffer event and return it
#[napi(js_name = "recv")] #[napi(js_name = "recv")]
pub async fn js_recv(&self) -> napi::Result<TextChange> { pub async fn js_recv(&self) -> napi::Result<TextChange> {
Ok(self.recv().await?) Ok(self.recv().await?)
} }
/// Send a buffer update to workspace
#[napi(js_name = "send")] #[napi(js_name = "send")]
pub async fn js_send(&self, op: TextChange) -> napi::Result<()> { pub async fn js_send(&self, op: TextChange) -> napi::Result<()> {
Ok(self.send(op).await?) Ok(self.send(op).await?)
} }
/// Return buffer whole content
#[napi(js_name = "content")] #[napi(js_name = "content")]
pub async fn js_content(&self) -> napi::Result<String> { pub async fn js_content(&self) -> napi::Result<String> {
Ok(self.content().await?) Ok(self.content().await?)

View file

@ -1,9 +1,34 @@
use napi_derive::napi;
use crate::{Client, Workspace}; use crate::{Client, Workspace};
use napi_derive::napi;
#[napi(object, js_name = "User")]
pub struct JsUser {
pub uuid: String,
pub name: String,
}
impl TryFrom<JsUser> for crate::api::User {
type Error = <uuid::Uuid as std::str::FromStr>::Err;
fn try_from(value: JsUser) -> Result<Self, Self::Error> {
Ok(Self {
id: value.uuid.parse()?,
name: value.name,
})
}
}
impl From<crate::api::User> for JsUser {
fn from(value: crate::api::User) -> Self {
Self {
uuid: value.id.to_string(),
name: value.name,
}
}
}
#[napi] #[napi]
/// connect to codemp servers and return a client session /// connect to codemp servers and return a client session
pub async fn connect(config: crate::api::Config) -> napi::Result<crate::Client>{ pub async fn connect(config: crate::api::Config) -> napi::Result<crate::Client> {
Ok(crate::Client::connect(config).await?) Ok(crate::Client::connect(config).await?)
} }
@ -23,13 +48,21 @@ impl Client {
#[napi(js_name = "list_workspaces")] #[napi(js_name = "list_workspaces")]
/// list available workspaces /// list available workspaces
pub async fn js_list_workspaces(&self, owned: bool, invited: bool) -> napi::Result<Vec<String>> { pub async fn js_list_workspaces(
&self,
owned: bool,
invited: bool,
) -> napi::Result<Vec<String>> {
Ok(self.list_workspaces(owned, invited).await?) Ok(self.list_workspaces(owned, invited).await?)
} }
#[napi(js_name = "invite_to_workspace")] #[napi(js_name = "invite_to_workspace")]
/// invite user to given workspace, if able to /// 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?) Ok(self.invite_to_workspace(workspace, user).await?)
} }
@ -51,10 +84,10 @@ impl Client {
self.get_workspace(&workspace) self.get_workspace(&workspace)
} }
#[napi(js_name = "user_id")] #[napi(js_name = "user")]
/// return current sessions's user id /// return current sessions's user id
pub fn js_user_id(&self) -> String { pub fn js_user(&self) -> JsUser {
self.user().id.to_string() self.user().clone().into()
} }
#[napi(js_name = "active_workspaces")] #[napi(js_name = "active_workspaces")]

View file

@ -3,7 +3,11 @@ use napi_derive::napi;
use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode}; use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode};
use crate::api::controller::{AsyncReceiver, AsyncSender}; use crate::api::controller::{AsyncReceiver, AsyncSender};
use crate::cursor::controller::CursorController; 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")] #[napi(object, js_name = "Cursor")]
pub struct JsCursor { pub struct JsCursor {
@ -19,64 +23,72 @@ pub struct JsCursor {
impl From<JsCursor> for crate::api::Cursor { impl From<JsCursor> for crate::api::Cursor {
fn from(value: JsCursor) -> Self { fn from(value: JsCursor) -> Self {
crate::api::Cursor { crate::api::Cursor {
start : (value.start_row, value.start_col), start: (value.start_row, value.start_col),
end: (value.end_row, value.end_col), end: (value.end_row, value.end_col),
buffer: value.buffer, buffer: value.buffer,
user: value.user, user: value.user,
} }
} }
} }
impl From<crate::api::Cursor> for JsCursor { impl From<crate::api::Cursor> for JsCursor {
fn from(value: crate::api::Cursor) -> Self { fn from(value: crate::api::Cursor) -> Self {
JsCursor { JsCursor {
start_row : value.start.0, start_row: value.start.0,
start_col : value.start.1, start_col: value.start.1,
end_row : value.end.0, end_row: value.end.0,
end_col: value.end.1, end_col: value.end.1,
buffer: value.buffer, buffer: value.buffer,
user: value.user.map(|x| x.to_string()) user: value.user.map(|x| x.to_string()),
} }
} }
} }
#[napi] #[napi]
impl CursorController { impl CursorController {
/// Register a callback to be called on receive.
#[napi(js_name = "callback", ts_args_type = "fun: (event: CursorController) => void")] /// There can only be one callback registered at any given time.
pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ #[napi(
let tsfn : ThreadsafeFunction<crate::cursor::controller::CursorController, Fatal> = js_name = "callback",
fun.create_threadsafe_function(0, ts_args_type = "fun: (event: CursorController) => void"
|ctx : ThreadSafeCallContext<crate::cursor::controller::CursorController>| { )]
Ok(vec![ctx.value]) pub fn js_callback(&self, fun: napi::JsFunction) -> napi::Result<()> {
} let tsfn: ThreadsafeFunction<crate::cursor::controller::CursorController, Fatal> = fun
)?; .create_threadsafe_function(
self.callback(move |controller : CursorController| { 0,
|ctx: ThreadSafeCallContext<crate::cursor::controller::CursorController>| {
tsfn.call(controller.clone(), ThreadsafeFunctionCallMode::Blocking); //check this with tracing also we could use Ok(event) to get the error 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 // If it blocks the main thread too many time we have to change this
}); });
Ok(()) 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")] #[napi(js_name = "send")]
pub async fn js_send(&self, pos: JsCursor) -> napi::Result<()> { pub async fn js_send(&self, pos: JsCursor) -> napi::Result<()> {
Ok(self.send(crate::api::Cursor::from(pos)).await?) Ok(self.send(crate::api::Cursor::from(pos)).await?)
} }
/// Get next cursor event if available without blocking
#[napi(js_name= "try_recv")] #[napi(js_name = "try_recv")]
pub async fn js_try_recv(&self) -> napi::Result<Option<JsCursor>> { pub async fn js_try_recv(&self) -> napi::Result<Option<JsCursor>> {
Ok(self.try_recv().await? Ok(self.try_recv().await?.map(JsCursor::from))
.map(JsCursor::from))
} }
#[napi(js_name= "recv")] /// Block until next
#[napi(js_name = "recv")]
pub async fn js_recv(&self) -> napi::Result<JsCursor> { pub async fn js_recv(&self) -> napi::Result<JsCursor> {
Ok(self.recv().await?.into()) Ok(self.recv().await?.into())
} }

View file

@ -1,12 +1,13 @@
use napi_derive::napi; use napi_derive::napi;
/// Hash function
#[napi(js_name = "hash")] #[napi(js_name = "hash")]
pub fn js_hash(data: String) -> i64 { 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")] #[napi(js_name = "version")]
pub fn js_version() -> String { pub fn js_version() -> String {
crate::version() crate::version()
} }

View file

@ -1,9 +1,8 @@
pub mod client;
pub mod workspace;
pub mod cursor;
pub mod buffer; pub mod buffer;
pub mod client;
pub mod cursor;
pub mod ext; pub mod ext;
pub mod workspace;
impl From<crate::errors::ConnectionError> for napi::Error { impl From<crate::errors::ConnectionError> for napi::Error {
fn from(value: crate::errors::ConnectionError) -> Self { fn from(value: crate::errors::ConnectionError) -> Self {
@ -33,7 +32,11 @@ impl JsLogger {
#[napi(constructor)] #[napi(constructor)]
pub fn new(debug: Option<bool>) -> JsLogger { pub fn new(debug: Option<bool>) -> JsLogger {
let (tx, rx) = tokio::sync::mpsc::channel(256); 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() let format = tracing_subscriber::fmt::format()
.with_level(true) .with_level(true)
.with_target(true) .with_target(true)
@ -55,11 +58,7 @@ impl JsLogger {
#[napi] #[napi]
pub async fn message(&self) -> Option<String> { pub async fn message(&self) -> Option<String> {
self.0 self.0.lock().await.recv().await
.lock()
.await
.recv()
.await
} }
} }
@ -73,5 +72,7 @@ impl std::io::Write for JsLoggerProducer {
Ok(buf.len()) Ok(buf.len())
} }
fn flush(&mut self) -> std::io::Result<()> { Ok(()) } fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
} }

View file

@ -6,6 +6,7 @@ use crate::buffer::controller::BufferController;
use crate::cursor::controller::CursorController; use crate::cursor::controller::CursorController;
use crate::api::controller::AsyncReceiver; use crate::api::controller::AsyncReceiver;
#[napi(object, js_name = "Event")] #[napi(object, js_name = "Event")]
pub struct JsEvent { pub struct JsEvent {
pub r#type: String, pub r#type: String,
@ -15,50 +16,73 @@ pub struct JsEvent {
impl From<crate::api::Event> for JsEvent { impl From<crate::api::Event> for JsEvent {
fn from(value: crate::api::Event) -> Self { fn from(value: crate::api::Event) -> Self {
match value { match value {
crate::api::Event::FileTreeUpdated(value) => Self { r#type: "filetree".into(), value }, crate::api::Event::FileTreeUpdated(value) => Self {
crate::api::Event::UserJoin(value) => Self { r#type: "join".into(), value }, r#type: "filetree".into(),
crate::api::Event::UserLeave(value) => Self { r#type: "leave".into(), value }, value,
},
crate::api::Event::UserJoin(value) => Self {
r#type: "join".into(),
value,
},
crate::api::Event::UserLeave(value) => Self {
r#type: "leave".into(),
value,
},
} }
} }
} }
#[napi] #[napi]
impl Workspace { impl Workspace {
/// Get the unique workspace id
#[napi(js_name = "id")] #[napi(js_name = "id")]
pub fn js_id(&self) -> String { pub fn js_id(&self) -> String {
self.id() self.id()
} }
/// List all available buffers in this workspace
#[napi(js_name = "filetree")] #[napi(js_name = "filetree")]
pub fn js_filetree(&self, filter: Option<&str>, strict: bool) -> Vec<String> { pub fn js_filetree(&self, filter: Option<&str>, strict: bool) -> Vec<String> {
self.filetree(filter, strict) self.filetree(filter, strict)
} }
/// List all user names currently in this workspace
#[napi(js_name = "user_list")] #[napi(js_name = "user_list")]
pub fn js_user_list(&self) -> Vec<String> { pub fn js_user_list(&self) -> Vec<String> {
self.user_list() self.user_list()
} }
/// List all currently active buffers
#[napi(js_name = "buffer_list")]
pub fn js_buffer_list(&self) -> Vec<String> {
self.buffer_list()
}
/// Get workspace's Cursor Controller
#[napi(js_name = "cursor")] #[napi(js_name = "cursor")]
pub fn js_cursor(&self) -> CursorController { pub fn js_cursor(&self) -> CursorController {
self.cursor() self.cursor()
} }
/// Get a buffer controller by its name (path)
#[napi(js_name = "buffer_by_name")] #[napi(js_name = "buffer_by_name")]
pub fn js_buffer_by_name(&self, path: String) -> Option<BufferController> { pub fn js_buffer_by_name(&self, path: String) -> Option<BufferController> {
self.buffer_by_name(&path) self.buffer_by_name(&path)
} }
/// Create a new buffer in the current workspace
#[napi(js_name = "create")] #[napi(js_name = "create")]
pub async fn js_create(&self, path: String) -> napi::Result<()> { pub async fn js_create(&self, path: String) -> napi::Result<()> {
Ok(self.create(&path).await?) Ok(self.create(&path).await?)
} }
/// Attach to a workspace buffer, starting a BufferController
#[napi(js_name = "attach")] #[napi(js_name = "attach")]
pub async fn js_attach(&self, path: String) -> napi::Result<BufferController> { pub async fn js_attach(&self, path: String) -> napi::Result<BufferController> {
Ok(self.attach(&path).await?) Ok(self.attach(&path).await?)
} }
/// Delete a buffer from workspace
#[napi(js_name = "delete")] #[napi(js_name = "delete")]
pub async fn js_delete(&self, path: String) -> napi::Result<()> { pub async fn js_delete(&self, path: String) -> napi::Result<()> {
Ok(self.delete(&path).await?) Ok(self.delete(&path).await?)
@ -81,7 +105,7 @@ impl Workspace {
} }
#[napi(js_name = "clear_callback")] #[napi(js_name = "clear_callback")]
pub fn js_clear_callbacl(&self) -> napi::Result<()> { pub fn js_clear_callback(&self) -> napi::Result<()> {
self.clear_callback(); self.clear_callback();
Ok(()) Ok(())
} }
@ -103,4 +127,34 @@ impl Workspace {
Ok(()) 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<Vec<JsUser>> {
Ok(self
.list_buffer_users(&path)
.await?
.into_iter()
.map(JsUser::from)
.collect())
}
} }

View file

@ -1,28 +1,41 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*; use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use super::ext::a_sync::a_sync; use super::ext::a_sync::a_sync;
use super::ext::from_lua_serde; use super::ext::from_lua_serde;
impl LuaUserData for CodempBufferController { impl LuaUserData for CodempBufferController {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(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,)| methods.add_method(
a_sync! { this => this.send(change).await? } "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("recv", |_, this, ()| a_sync! { this => this.recv().await? });
methods.add_method("poll", |_, this, ()| a_sync! { this => this.poll().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("clear_callback", |_, this, ()| {
methods.add_method("callback", |_, this, (cb,):(LuaFunction,)| { this.clear_callback();
this.callback(move |controller: CodempBufferController| super::ext::callback().invoke(cb.clone(), controller)); Ok(())
});
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
this.callback(move |controller: CodempBufferController| {
super::ext::callback().invoke(cb.clone(), controller)
});
Ok(()) Ok(())
}); });
} }
@ -32,15 +45,17 @@ from_lua_serde! { CodempTextChange }
impl LuaUserData for CodempTextChange { impl LuaUserData for CodempTextChange {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) { fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("content", |_, this| Ok(this.content.clone())); 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("start", |_, this| Ok(this.start));
fields.add_field_method_get("end", |_, this| Ok(this.end)); 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("hash", |_, this| Ok(this.hash));
// add a 'finish' accessor too because in Lua 'end' is reserved // 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<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
methods.add_method("apply", |_, this, (txt,):(String,)| Ok(this.apply(&txt))); Ok(format!("{:?}", this))
});
methods.add_method("apply", |_, this, (txt,): (String,)| Ok(this.apply(&txt)));
} }
} }

View file

@ -1,6 +1,6 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*; use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use super::ext::a_sync::a_sync; use super::ext::a_sync::a_sync;
use super::ext::from_lua_serde; use super::ext::from_lua_serde;
@ -13,22 +13,28 @@ impl LuaUserData for CodempClient {
} }
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(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, ()| methods.add_method(
a_sync! { this => this.refresh().await? } "refresh",
|_, this, ()| a_sync! { this => this.refresh().await? },
); );
methods.add_method("join_workspace", |_, this, (ws,):(String,)| methods.add_method(
a_sync! { this => this.join_workspace(ws).await? } "join_workspace",
|_, this, (ws,): (String,)| a_sync! { this => this.join_workspace(ws).await? },
); );
methods.add_method("create_workspace", |_, this, (ws,):(String,)| methods.add_method(
a_sync! { this => this.create_workspace(ws).await? } "create_workspace",
|_, this, (ws,): (String,)| a_sync! { this => this.create_workspace(ws).await? },
); );
methods.add_method("delete_workspace", |_, this, (ws,):(String,)| methods.add_method(
a_sync! { this => this.delete_workspace(ws).await? } "delete_workspace",
|_, this, (ws,): (String,)| a_sync! { this => this.delete_workspace(ws).await? },
); );
methods.add_method("invite_to_workspace", |_, this, (ws,user):(String,String)| 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? } 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)) 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))
});
} }
} }

View file

@ -1,6 +1,6 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*; use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use super::ext::a_sync::a_sync; use super::ext::a_sync::a_sync;
use super::ext::from_lua_serde; use super::ext::from_lua_serde;
@ -8,20 +8,29 @@ use super::ext::lua_tuple;
impl LuaUserData for CodempCursorController { impl LuaUserData for CodempCursorController {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(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,)| methods.add_method(
a_sync! { this => this.send(cursor).await? } "send",
|_, this, (cursor,): (CodempCursor,)| a_sync! { this => this.send(cursor).await? },
); );
methods.add_method("try_recv", |_, this, ()| methods.add_method(
a_sync! { this => this.try_recv().await? } "try_recv",
|_, this, ()| a_sync! { this => this.try_recv().await? },
); );
methods.add_method("recv", |_, this, ()| a_sync! { this => this.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("poll", |_, this, ()| a_sync! { this => this.poll().await? });
methods.add_method("clear_callback", |_, this, ()| { this.clear_callback(); Ok(()) }); methods.add_method("clear_callback", |_, this, ()| {
methods.add_method("callback", |_, this, (cb,):(LuaFunction,)| { this.clear_callback();
this.callback(move |controller: CodempCursorController| super::ext::callback().invoke(cb.clone(), controller)); Ok(())
});
methods.add_method("callback", |_, this, (cb,): (LuaFunction,)| {
this.callback(move |controller: CodempCursorController| {
super::ext::callback().invoke(cb.clone(), controller)
});
Ok(()) Ok(())
}); });
} }
@ -30,13 +39,15 @@ impl LuaUserData for CodempCursorController {
from_lua_serde! { CodempCursor } from_lua_serde! { CodempCursor }
impl LuaUserData for CodempCursor { impl LuaUserData for CodempCursor {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(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<F: LuaUserDataFields<Self>>(fields: &mut F) { fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
fields.add_field_method_get("user", |_, this| Ok(this.user.clone())); 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("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)); fields.add_field_method_get("end", |lua, this| lua_tuple(lua, this.end));
// add a 'finish' accessor too because in Lua 'end' is reserved // add a 'finish' accessor too because in Lua 'end' is reserved
fields.add_field_method_get("finish", |lua, this| lua_tuple(lua, this.end)); fields.add_field_method_get("finish", |lua, this| lua_tuple(lua, this.end));

View file

@ -1,15 +1,15 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*; use mlua::prelude::*;
use mlua_codemp_patch as mlua;
pub(crate) fn tokio() -> &'static tokio::runtime::Runtime { pub(crate) fn tokio() -> &'static tokio::runtime::Runtime {
use std::sync::OnceLock; use std::sync::OnceLock;
static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new(); static RT: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
RT.get_or_init(|| RT.get_or_init(|| {
tokio::runtime::Builder::new_current_thread() tokio::runtime::Builder::new_current_thread()
.enable_all() .enable_all()
.build() .build()
.expect("could not create tokio runtime") .expect("could not create tokio runtime")
) })
} }
macro_rules! a_sync { macro_rules! a_sync {
@ -32,45 +32,53 @@ macro_rules! a_sync {
pub(crate) use a_sync; pub(crate) use a_sync;
pub(crate) struct Promise(pub(crate) Option<tokio::task::JoinHandle<LuaResult<super::callback::CallbackArg>>>); pub(crate) struct Promise(
pub(crate) Option<tokio::task::JoinHandle<LuaResult<super::callback::CallbackArg>>>,
);
impl LuaUserData for Promise { impl LuaUserData for Promise {
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) { fn add_fields<F: LuaUserDataFields<Self>>(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())) Ok(this.0.as_ref().map_or(true, |x| x.is_finished()))
); });
} }
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
// TODO: await MUST NOT be used in callbacks!! // TODO: await MUST NOT be used in callbacks!!
methods.add_method_mut("await", |_, this, ()| match this.0.take() { methods.add_method_mut("await", |_, this, ()| match this.0.take() {
None => Err(LuaError::runtime("Promise already awaited")), None => Err(LuaError::runtime("Promise already awaited")),
Some(x) => { Some(x) => tokio().block_on(x).map_err(LuaError::runtime)?,
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")), None => Err(LuaError::runtime("Promise already awaited")),
Some(x) => { Some(x) => {
tokio() x.abort();
.spawn(async move { 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 { 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 { Ok(res) => match res {
Err(e) => super::callback().failure(e), Err(e) => super::callback().failure(e),
Ok(val) => super::callback().invoke(cb, val), Ok(val) => super::callback().invoke(cb, val),
}, },
} }
}); });
Ok(()) Ok(())
}, }
}
}); });
} }
} }
pub(crate) fn setup_driver(_: &Lua, (block,):(Option<bool>,)) -> LuaResult<Option<Driver>> { pub(crate) fn setup_driver(_: &Lua, (block,): (Option<bool>,)) -> LuaResult<Option<Driver>> {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let future = async move { let future = async move {
tracing::info!(" :: driving runtime..."); tracing::info!(" :: driving runtime...");
@ -89,25 +97,26 @@ pub(crate) fn setup_driver(_: &Lua, (block,):(Option<bool>,)) -> LuaResult<Optio
} }
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Driver(pub(crate) tokio::sync::mpsc::UnboundedSender<()>, Option<std::thread::JoinHandle<()>>); pub(crate) struct Driver(
pub(crate) tokio::sync::mpsc::UnboundedSender<()>,
Option<std::thread::JoinHandle<()>>,
);
impl LuaUserData for Driver { impl LuaUserData for Driver {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
methods.add_method_mut("stop", |_, this, ()| { Ok(format!("{:?}", this))
match this.1.take() { });
None => Ok(false), methods.add_method_mut("stop", |_, this, ()| match this.1.take() {
Some(handle) => { None => Ok(false),
if this.0.send(()).is_err() { Some(handle) => {
tracing::warn!("found runtime already stopped while attempting to stop it"); 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:?}"))), match handle.join() {
Ok(()) => Ok(true), Err(e) => Err(LuaError::runtime(format!("runtime thread panicked: {e:?}"))),
} Ok(()) => Ok(true),
}, }
} }
}); });
} }
} }

View file

@ -1,7 +1,7 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*;
use crate::ext::IgnorableError; use crate::ext::IgnorableError;
use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
pub(crate) fn callback() -> &'static CallbackChannel<LuaCallback> { pub(crate) fn callback() -> &'static CallbackChannel<LuaCallback> {
static CHANNEL: std::sync::OnceLock<CallbackChannel<LuaCallback>> = std::sync::OnceLock::new(); static CHANNEL: std::sync::OnceLock<CallbackChannel<LuaCallback>> = std::sync::OnceLock::new();
@ -10,7 +10,7 @@ pub(crate) fn callback() -> &'static CallbackChannel<LuaCallback> {
pub(crate) struct CallbackChannel<T> { pub(crate) struct CallbackChannel<T> {
tx: std::sync::Arc<tokio::sync::mpsc::UnboundedSender<T>>, tx: std::sync::Arc<tokio::sync::mpsc::UnboundedSender<T>>,
rx: std::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<T>> rx: std::sync::Mutex<tokio::sync::mpsc::UnboundedReceiver<T>>,
} }
impl Default for CallbackChannel<LuaCallback> { impl Default for CallbackChannel<LuaCallback> {
@ -26,12 +26,16 @@ impl Default for CallbackChannel<LuaCallback> {
impl CallbackChannel<LuaCallback> { impl CallbackChannel<LuaCallback> {
pub(crate) fn invoke(&self, cb: LuaFunction, arg: impl Into<CallbackArg>) { pub(crate) fn invoke(&self, cb: LuaFunction, arg: impl Into<CallbackArg>) {
self.tx.send(LuaCallback::Invoke(cb, arg.into())) self.tx
.send(LuaCallback::Invoke(cb, arg.into()))
.unwrap_or_warn("error scheduling callback") .unwrap_or_warn("error scheduling callback")
} }
pub(crate) fn failure(&self, err: impl std::error::Error) { 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") .unwrap_or_warn("error scheduling callback failure")
} }
@ -40,12 +44,12 @@ impl CallbackChannel<LuaCallback> {
Err(e) => { Err(e) => {
tracing::debug!("backing off from callback mutex: {e}"); tracing::debug!("backing off from callback mutex: {e}");
None None
}, }
Ok(mut lock) => match lock.try_recv() { Ok(mut lock) => match lock.try_recv() {
Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => { Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => {
tracing::error!("callback channel closed"); tracing::error!("callback channel closed");
None None
}, }
Err(tokio::sync::mpsc::error::TryRecvError::Empty) => None, Err(tokio::sync::mpsc::error::TryRecvError::Empty) => None,
Ok(cb) => Some(cb), Ok(cb) => Some(cb),
}, },

View file

@ -1,7 +1,7 @@
use std::{io::Write, sync::Mutex}; use std::{io::Write, sync::Mutex};
use mlua_codemp_patch as mlua;
use mlua::prelude::*; use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use tokio::sync::mpsc; use tokio::sync::mpsc;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -12,12 +12,21 @@ impl Write for LuaLoggerProducer {
Ok(buf.len()) 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? // TODO can we make this less verbose?
pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option<bool>)) -> LuaResult<bool> { pub(crate) fn setup_tracing(
let level = if debug.unwrap_or_default() { tracing::Level::DEBUG } else {tracing::Level::INFO }; _: &Lua,
(printer, debug): (LuaValue, Option<bool>),
) -> LuaResult<bool> {
let level = if debug.unwrap_or_default() {
tracing::Level::DEBUG
} else {
tracing::Level::INFO
};
let format = tracing_subscriber::fmt::format() let format = tracing_subscriber::fmt::format()
.with_level(true) .with_level(true)
.with_target(true) .with_target(true)
@ -36,16 +45,15 @@ pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option<bool>))
| LuaValue::Thread(_) | LuaValue::Thread(_)
| LuaValue::UserData(_) | LuaValue::UserData(_)
| LuaValue::Error(_) => return Err(LuaError::BindError), // TODO full BadArgument type?? | LuaValue::Error(_) => return Err(LuaError::BindError), // TODO full BadArgument type??
LuaValue::Nil => { LuaValue::Nil => tracing_subscriber::fmt()
tracing_subscriber::fmt() .event_format(format)
.event_format(format) .with_max_level(level)
.with_max_level(level) .with_writer(std::sync::Mutex::new(std::io::stderr()))
.with_writer(std::sync::Mutex::new(std::io::stderr())) .try_init()
.try_init() .is_ok(),
.is_ok()
},
LuaValue::String(path) => { 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() tracing_subscriber::fmt()
.event_format(format) .event_format(format)
.with_max_level(level) .with_max_level(level)
@ -53,7 +61,7 @@ pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option<bool>))
.with_ansi(false) .with_ansi(false)
.try_init() .try_init()
.is_ok() .is_ok()
}, }
LuaValue::Function(cb) => { LuaValue::Function(cb) => {
let (tx, mut rx) = mpsc::unbounded_channel(); let (tx, mut rx) = mpsc::unbounded_channel();
let res = tracing_subscriber::fmt() let res = tracing_subscriber::fmt()
@ -71,7 +79,7 @@ pub(crate) fn setup_tracing(_: &Lua, (printer, debug): (LuaValue, Option<bool>))
}); });
} }
res res
}, }
}; };
Ok(success) Ok(success)

View file

@ -2,8 +2,8 @@ pub mod a_sync;
pub mod callback; pub mod callback;
pub mod log; pub mod log;
use mlua_codemp_patch as mlua;
use mlua::prelude::*; use mlua::prelude::*;
use mlua_codemp_patch as mlua;
pub(crate) use a_sync::tokio; pub(crate) use a_sync::tokio;
pub(crate) use callback::callback; pub(crate) use callback::callback;

View file

@ -1,73 +1,99 @@
mod client;
mod workspace;
mod cursor;
mod buffer; mod buffer;
mod client;
mod cursor;
mod ext; mod ext;
mod workspace;
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::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 // define multiple entrypoints, so this library can have multiple names and still work
#[mlua::lua_module(name = "codemp")] fn entry_1(lua: &Lua) -> LuaResult<LuaTable> { entrypoint(lua) } #[mlua::lua_module(name = "codemp")]
#[mlua::lua_module(name = "libcodemp")] fn entry_2(lua: &Lua) -> LuaResult<LuaTable> { entrypoint(lua) } fn entry_1(lua: &Lua) -> LuaResult<LuaTable> {
#[mlua::lua_module(name = "codemp_native")] fn entry_3(lua: &Lua) -> LuaResult<LuaTable> { entrypoint(lua) } entrypoint(lua)
#[mlua::lua_module(name = "codemp_lua")] fn entry_4(lua: &Lua) -> LuaResult<LuaTable> { entrypoint(lua) } }
#[mlua::lua_module(name = "libcodemp")]
fn entry_2(lua: &Lua) -> LuaResult<LuaTable> {
entrypoint(lua)
}
#[mlua::lua_module(name = "codemp_native")]
fn entry_3(lua: &Lua) -> LuaResult<LuaTable> {
entrypoint(lua)
}
#[mlua::lua_module(name = "codemp_lua")]
fn entry_4(lua: &Lua) -> LuaResult<LuaTable> {
entrypoint(lua)
}
fn entrypoint(lua: &Lua) -> LuaResult<LuaTable> { fn entrypoint(lua: &Lua) -> LuaResult<LuaTable> {
let exports = lua.create_table()?; let exports = lua.create_table()?;
// entrypoint // entrypoint
exports.set("connect", lua.create_function(|_, (config,):(CodempConfig,)| exports.set(
ext::a_sync::a_sync! { => CodempClient::connect(config).await? } "connect",
)?)?; lua.create_function(
|_, (config,): (CodempConfig,)| ext::a_sync::a_sync! { => CodempClient::connect(config).await? },
)?,
)?;
// utils // utils
exports.set("hash", lua.create_function(|_, (txt,):(String,)| exports.set(
Ok(crate::ext::hash(txt)) "hash",
)?)?; lua.create_function(|_, (txt,): (String,)| Ok(crate::ext::hash(txt)))?,
)?;
exports.set("version", lua.create_function(|_, ()| exports.set(
Ok(crate::version()) "version",
)?)?; lua.create_function(|_, ()| Ok(crate::version()))?,
)?;
// runtime // runtime
exports.set("setup_driver", lua.create_function(ext::a_sync::setup_driver)?)?; exports.set(
exports.set("poll_callback", lua.create_function(|lua, ()| { "setup_driver",
let mut val = LuaMultiValue::new(); lua.create_function(ext::a_sync::setup_driver)?,
match ext::callback().recv() { )?;
None => {}, exports.set(
Some(ext::callback::LuaCallback::Invoke(cb, arg)) => { "poll_callback",
val.push_back(LuaValue::Function(cb)); lua.create_function(|lua, ()| {
val.push_back(arg.into_lua(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)) => { Ok(val)
val.push_back(false.into_lua(lua)?); })?,
val.push_back(msg.into_lua(lua)?); )?;
},
}
Ok(val)
})?)?;
// logging // 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) Ok(exports)
} }
impl From::<crate::errors::ConnectionError> for LuaError { impl From<crate::errors::ConnectionError> for LuaError {
fn from(value: crate::errors::ConnectionError) -> Self { fn from(value: crate::errors::ConnectionError) -> Self {
LuaError::runtime(value.to_string()) LuaError::runtime(value.to_string())
} }
} }
impl From::<crate::errors::RemoteError> for LuaError { impl From<crate::errors::RemoteError> for LuaError {
fn from(value: crate::errors::RemoteError) -> Self { fn from(value: crate::errors::RemoteError) -> Self {
LuaError::runtime(value.to_string()) LuaError::runtime(value.to_string())
} }
} }
impl From::<crate::errors::ControllerError> for LuaError { impl From<crate::errors::ControllerError> for LuaError {
fn from(value: crate::errors::ControllerError) -> Self { fn from(value: crate::errors::ControllerError) -> Self {
LuaError::runtime(value.to_string()) LuaError::runtime(value.to_string())
} }

View file

@ -1,40 +1,52 @@
use mlua_codemp_patch as mlua;
use mlua::prelude::*;
use crate::prelude::*; use crate::prelude::*;
use mlua::prelude::*;
use mlua_codemp_patch as mlua;
use super::ext::a_sync::a_sync; use super::ext::a_sync::a_sync;
use super::ext::from_lua_serde; use super::ext::from_lua_serde;
impl LuaUserData for CodempWorkspace { impl LuaUserData for CodempWorkspace {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| {
methods.add_method("create", |_, this, (name,):(String,)| Ok(format!("{:?}", this))
a_sync! { this => this.create(&name).await? } });
methods.add_method(
"create",
|_, this, (name,): (String,)| a_sync! { this => this.create(&name).await? },
); );
methods.add_method("attach", |_, this, (name,):(String,)| methods.add_method(
a_sync! { this => this.attach(&name).await? } "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)) 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,)| methods.add_method("get_buffer", |_, this, (name,): (String,)| {
a_sync! { this => this.delete(&name).await? } 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, ()| methods.add_method("fetch_buffers", |_, this, ()|
a_sync! { this => this.fetch_buffers().await? } a_sync! { this => this.fetch_buffers().await? }
); );
methods.add_method("fetch_users", |_, this, ()| methods.add_method(
a_sync! { this => this.fetch_users().await? } "fetch_users",
|_, this, ()| a_sync! { this => this.fetch_users().await? },
); );
methods.add_method("filetree", |_, this, (filter, strict,):(Option<String>, Option<bool>,)| methods.add_method(
Ok(this.filetree(filter.as_deref(), strict.unwrap_or(false))) "filetree",
|_, this, (filter, strict): (Option<String>, Option<bool>)| {
Ok(this.filetree(filter.as_deref(), strict.unwrap_or(false)))
},
); );
methods.add_method("user_list", |_, this, ()| methods.add_method("user_list", |_, this, ()|
@ -62,6 +74,7 @@ impl LuaUserData for CodempWorkspace {
this.clear_callback(); this.clear_callback();
Ok(()) Ok(())
}); });
} }
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) { fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
@ -75,7 +88,9 @@ impl LuaUserData for CodempWorkspace {
from_lua_serde! { CodempEvent } from_lua_serde! { CodempEvent }
impl LuaUserData for CodempEvent { impl LuaUserData for CodempEvent {
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) { fn add_methods<M: LuaUserDataMethods<Self>>(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<F: LuaUserDataFields<Self>>(fields: &mut F) { fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
@ -86,9 +101,8 @@ impl LuaUserData for CodempEvent {
}); });
fields.add_field_method_get("value", |_, this| match this { fields.add_field_method_get("value", |_, this| match this {
CodempEvent::FileTreeUpdated(x) CodempEvent::FileTreeUpdated(x)
| CodempEvent::UserJoin(x) | CodempEvent::UserJoin(x)
| CodempEvent::UserLeave(x) | CodempEvent::UserLeave(x) => Ok(x.clone()),
=> Ok(x.clone()),
}); });
} }
} }

View file

@ -1,5 +1,5 @@
use codemp_proto::{ 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, workspace::workspace_client::WorkspaceClient,
}; };
use tonic::{ use tonic::{
@ -14,10 +14,7 @@ type AuthedService = InterceptedService<Channel, WorkspaceInterceptor>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SessionInterceptor(pub tokio::sync::watch::Receiver<codemp_proto::common::Token>); pub struct SessionInterceptor(pub tokio::sync::watch::Receiver<codemp_proto::common::Token>);
impl tonic::service::Interceptor for SessionInterceptor { impl tonic::service::Interceptor for SessionInterceptor {
fn call( fn call(&mut self, mut request: tonic::Request<()>) -> tonic::Result<tonic::Request<()>> {
&mut self,
mut request: tonic::Request<()>,
) -> tonic::Result<tonic::Request<()>> {
if let Ok(token) = self.0.borrow().token.parse() { if let Ok(token) = self.0.borrow().token.parse() {
request.metadata_mut().insert("session", token); request.metadata_mut().insert("session", token);
} }

View file

@ -13,8 +13,6 @@ pub use crate::api::{
}; };
pub use crate::{ pub use crate::{
client::Client as CodempClient, buffer::Controller as CodempBufferController, client::Client as CodempClient,
workspace::Workspace as CodempWorkspace, cursor::Controller as CodempCursorController, workspace::Workspace as CodempWorkspace,
cursor::Controller as CodempCursorController,
buffer::Controller as CodempBufferController,
}; };

View file

@ -134,6 +134,11 @@ impl Workspace {
Ok(ws) 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. /// Create a new buffer in the current workspace.
pub async fn create(&self, path: &str) -> RemoteResult<()> { pub async fn create(&self, path: &str) -> RemoteResult<()> {
let mut workspace_client = self.0.services.ws(); let mut workspace_client = self.0.services.ws();
@ -189,9 +194,9 @@ impl Workspace {
match self.0.buffers.remove(path) { match self.0.buffers.remove(path) {
None => true, // noop: we werent attached in the first place None => true, // noop: we werent attached in the first place
Some((_name, controller)) => match Arc::into_inner(controller.0) { 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 Some(_) => true, // dropping it now
} },
} }
} }
@ -261,7 +266,6 @@ impl Workspace {
})) }))
.await?; .await?;
self.0.filetree.remove(path); self.0.filetree.remove(path);
Ok(()) Ok(())
@ -308,7 +312,7 @@ impl Workspace {
/// A filter may be applied, and it may be strict (equality check) or not (starts_with check). /// 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 // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120
pub fn filetree(&self, filter: Option<&str>, strict: bool) -> Vec<String> { pub fn filetree(&self, filter: Option<&str>, strict: bool) -> Vec<String> {
self.0 let mut tree = self.0
.filetree .filetree
.iter() .iter()
.filter(|f| { .filter(|f| {
@ -321,7 +325,9 @@ impl Workspace {
}) })
}) })
.map(|f| f.clone()) .map(|f| f.clone())
.collect() .collect::<Vec<String>>();
tree.sort();
tree
} }
pub(crate) fn run_actor( pub(crate) fn run_actor(
@ -333,13 +339,18 @@ impl Workspace {
let weak = Arc::downgrade(&self.0); let weak = Arc::downgrade(&self.0);
let name = self.id(); let name = self.id();
tokio::spawn(async move { tokio::spawn(async move {
tracing::debug!("workspace worker starting");
loop { loop {
// TODO can we stop responsively rather than poll for Arc being dropped? // 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!( let Some(res) = tokio::select!(
x = stream.message() => Some(x), x = stream.message() => Some(x),
_ = tokio::time::sleep(std::time::Duration::from_secs(5)) => None, _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => None,
) else { continue }; ) else {
continue;
};
match res { match res {
Err(e) => break tracing::error!("workspace '{}' stream closed: {}", name, e), Err(e) => break tracing::error!("workspace '{}' stream closed: {}", name, e),
Ok(None) => break tracing::info!("leaving workspace {}", name), Ok(None) => break tracing::info!("leaving workspace {}", name),
@ -376,6 +387,7 @@ impl Workspace {
} }
} }
} }
tracing::debug!("workspace worker stopping");
}); });
} }
} }