diff --git a/Cargo.toml b/Cargo.toml index 5e5ac0c..131420b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,8 +42,8 @@ derive_more = { version = "0.99.17", optional = true } # glue (js) rmpv = { version = "1", optional = true } -napi = { version = "2", features = ["full"], optional = true } -napi-derive = { version="2", optional = true} +napi = { version = "2.16", features = ["full"], optional = true } +napi-derive = { version="2.16", optional = true} futures = { version = "0.3.28", optional = true } # glue (python) @@ -60,7 +60,7 @@ napi-build = { version = "2", optional = true } pyo3-build-config = { version = "0.19.2", optional = true } [features] -default = [] +default = ["js"] lua = ["mlua", "derive_more", "lazy_static", "tracing-subscriber"] java = ["lazy_static", "jni", "tracing-subscriber"] java-artifact = ["java"] # also builds the jar diff --git a/src/api/change.rs b/src/api/change.rs index 19d792a..a5eee55 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -30,11 +30,15 @@ pub struct Op(pub(crate) woot::crdt::Op); /// to delete a the fourth character we should send a /// `TextChange { span: 3..4, content: "".into() }` /// + #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "python", pyo3::pyclass)] +#[cfg_attr(feature = "js", napi_derive::napi(object))] pub struct TextChange { - /// range of text change, as char indexes in buffer previous state - pub span: std::ops::Range, + /// range start of text change, as char indexes in buffer previous state + pub start: u32, + /// range end of text change, as char indexes in buffer previous state + pub end: u32, /// new content of text inside span pub content: String, } @@ -65,11 +69,16 @@ impl TextChange { let end_after = after.len() - end; TextChange { - span: start..end_before, + start: start as u32, + end: end_before as u32, content: after[start..end_after].to_string(), } } + pub fn span(&self) -> std::ops::Range { + self.start as usize .. self.end as usize + } + /// consume the [TextChange], transforming it into a Vec of [Op] pub fn transform(self, woot: &Woot) -> WootResult> { let mut out = Vec::new(); @@ -77,19 +86,19 @@ impl TextChange { return Ok(out); } // no-op let view = woot.view(); - let Some(span) = view.get(self.span.clone()) else { + let Some(span) = view.get(self.span()) else { return Err(crate::woot::WootError::OutOfBounds); }; let diff = similar::TextDiff::from_chars(span, &self.content); for (i, diff) in diff.iter_all_changes().enumerate() { match diff.tag() { similar::ChangeTag::Equal => {} - similar::ChangeTag::Delete => match woot.delete_one(self.span.start + i) { + similar::ChangeTag::Delete => match woot.delete_one(self.span().start + i) { Err(e) => tracing::error!("could not create deletion: {}", e), Ok(op) => out.push(Op(op)), }, similar::ChangeTag::Insert => { - match woot.insert(self.span.start + i, diff.value()) { + match woot.insert(self.span().start + i, diff.value()) { Ok(ops) => { for op in ops { out.push(Op(op)) @@ -105,7 +114,7 @@ impl TextChange { /// returns true if this TextChange deletes existing text pub fn is_deletion(&self) -> bool { - !self.span.is_empty() + !self.span().is_empty() } /// returns true if this TextChange adds new text @@ -120,9 +129,9 @@ impl TextChange { /// applies this text change to given text, returning a new string pub fn apply(&self, txt: &str) -> String { - let pre_index = std::cmp::min(self.span.start, txt.len()); + let pre_index = std::cmp::min(self.span().start, txt.len()); let pre = txt.get(..pre_index).unwrap_or("").to_string(); - let post = txt.get(self.span.end..).unwrap_or("").to_string(); + let post = txt.get(self.span().end..).unwrap_or("").to_string(); format!("{}{}{}", pre, self.content, post) } @@ -144,7 +153,7 @@ mod tests { "sphinx of black quartz, judge my vow", "sphinx of quartz, judge my vow", ); - assert_eq!(change.span, 10..16); + assert_eq!(change.span(), 10..16); assert_eq!(change.content, ""); } @@ -154,7 +163,7 @@ mod tests { "sphinx of quartz, judge my vow", "sphinx of black quartz, judge my vow", ); - assert_eq!(change.span, 10..10); + assert_eq!(change.span(), 10..10); assert_eq!(change.content, "black "); } @@ -164,14 +173,14 @@ mod tests { "sphinx of black quartz, judge my vow", "sphinx who watches the desert, judge my vow", ); - assert_eq!(change.span, 7..22); + assert_eq!(change.span(), 7..22); assert_eq!(change.content, "who watches the desert"); } #[test] fn textchange_apply_works_for_insertions() { let change = super::TextChange { - span: 5..5, + start: 5, end: 5, content: " cruel".to_string(), }; let result = change.apply("hello world!"); @@ -181,7 +190,7 @@ mod tests { #[test] fn textchange_apply_works_for_deletions() { let change = super::TextChange { - span: 5..11, + start: 5, end: 11, content: "".to_string(), }; let result = change.apply("hello cruel world!"); @@ -191,7 +200,7 @@ mod tests { #[test] fn textchange_apply_works_for_replacements() { let change = super::TextChange { - span: 5..11, + start: 5, end: 11, content: " not very pleasant".to_string(), }; let result = change.apply("hello cruel world!"); @@ -201,7 +210,7 @@ mod tests { #[test] fn textchange_apply_never_panics() { let change = super::TextChange { - span: 100..110, + start: 100, end: 110, content: "a very long string \n which totally matters".to_string(), }; let result = change.apply("a short text"); @@ -220,7 +229,7 @@ mod tests { #[test] fn empty_textchange_doesnt_alter_buffer() { let change = super::TextChange { - span: 42..42, + start: 42, end: 42, content: "".to_string(), }; let result = change.apply("some important text"); diff --git a/src/buffer/controller.rs b/src/buffer/controller.rs index 21dca71..92a71c9 100644 --- a/src/buffer/controller.rs +++ b/src/buffer/controller.rs @@ -22,6 +22,7 @@ use crate::api::TextChange; /// upon dropping this handle will stop the associated worker #[derive(Debug, Clone)] #[cfg_attr(feature = "python", pyo3::pyclass)] +#[cfg_attr(feature = "js", napi_derive::napi)] pub struct BufferController(Arc); #[derive(Debug)] diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index 3f579d1..dc9e460 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -30,6 +30,7 @@ use codemp_proto::cursor::{CursorEvent, CursorPosition}; /// upon dropping this handle will stop the associated worker #[derive(Debug, Clone)] #[cfg_attr(feature = "python", pyo3::pyclass)] +#[cfg_attr(feature = "js", napi_derive::napi)] pub struct CursorController(Arc); #[derive(Debug)] diff --git a/src/ffi/js/buffer.rs b/src/ffi/js/buffer.rs index 342b9cb..c92da6f 100644 --- a/src/ffi/js/buffer.rs +++ b/src/ffi/js/buffer.rs @@ -1,56 +1,25 @@ use std::sync::Arc; use napi::threadsafe_function::{ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}; use napi_derive::napi; -use crate::api::Controller; +use crate::api::TextChange; use crate::ffi::js::JsCodempError; +use crate::api::Controller; +use crate::prelude::*; -/// BUFFER -#[napi(object)] -pub struct JsTextChange { - pub span: JsRange, - pub content: String, -} -#[napi(object)] -pub struct JsRange{ - pub start: i32, - pub end: i32, -} -impl From:: for JsTextChange { - fn from(value: crate::api::TextChange) -> Self { - JsTextChange { - // TODO how is x.. represented ? span.end can never be None - span: JsRange { start: value.span.start as i32, end: value.span.end as i32 }, - content: value.content, +impl From for napi::Error { + fn from(value: crate::Error) -> Self { + let msg = format!("{value}"); + match value { + crate::Error::Deadlocked => napi::Error::new(napi::Status::WouldDeadlock, msg), + _ => napi::Error::new(napi::Status::GenericFailure, msg), } } } - -impl From::> for JsBufferController { - fn from(value: Arc) -> Self { - JsBufferController(value) - } -} - - #[napi] -pub struct JsBufferController(Arc); - - -/*#[napi] -pub fn delta(string : String, start: i64, txt: String, end: i64 ) -> Option { - Some(JsCodempOperationSeq(string.diff(start as usize, &txt, end as usize)?)) -}*/ - - - - - - -#[napi] -impl JsBufferController { +impl CodempBufferController { #[napi(ts_args_type = "fun: (event: JsTextChange) => void")] @@ -58,10 +27,10 @@ impl JsBufferController { let tsfn : ThreadsafeFunction = fun.create_threadsafe_function(0, |ctx : ThreadSafeCallContext| { - Ok(vec![JsTextChange::from(ctx.value)]) + Ok(vec![ctx.value]) } )?; - let _controller = self.0.clone(); + let _controller = self.clone(); tokio::spawn(async move { //tokio::time::sleep(std::time::Duration::from_secs(1)).await; loop { @@ -79,32 +48,13 @@ impl JsBufferController { } - #[napi] - pub fn content(&self) -> String { - self.0.content() + #[napi(js_name = "recv")] + pub async fn jsrecv(&self) -> napi::Result { + Ok(self.recv().await?.into()) } #[napi] - pub fn get_name(&self) -> String { - self.0.name().to_string() - } - - #[napi] - pub async fn recv(&self) -> napi::Result { - Ok( - self.0.recv().await - .map_err(|e| napi::Error::from(JsCodempError(e)))? - .into() - ) - } - - #[napi] - pub fn send(&self, op: JsTextChange) -> napi::Result<()> { - // TODO might be nice to take ownership of the opseq - let new_text_change = crate::api::TextChange { - span: op.span.start as usize .. op.span.end as usize, - content: op.content, - }; - Ok(self.0.send(new_text_change).map_err(JsCodempError)?) + pub fn send(&self, op: TextChange) -> napi::Result<()> { + Ok(self.send(op)?) } } \ No newline at end of file diff --git a/src/ffi/js/client.rs b/src/ffi/js/client.rs index 5527f74..de8446c 100644 --- a/src/ffi/js/client.rs +++ b/src/ffi/js/client.rs @@ -1,6 +1,5 @@ use napi_derive::napi; use crate::ffi::js::JsCodempError; -use crate::ffi::js::workspace::JsWorkspace; #[napi] /// main codemp client session @@ -10,8 +9,7 @@ pub struct JsCodempClient(tokio::sync::RwLock); /// connect to codemp servers and return a client session pub async fn connect(addr: Option) -> napi::Result{ let client = crate::Client::new(addr.as_deref().unwrap_or("http://codemp.alemi.dev:50053")) - .await - .map_err(JsCodempError)?; + .await?; Ok(JsCodempClient(tokio::sync::RwLock::new(client))) } @@ -21,14 +19,14 @@ impl JsCodempClient { #[napi] /// login against AuthService with provided credentials, optionally requesting access to a workspace pub async fn login(&self, username: String, password: String, workspace_id: Option) -> napi::Result<()> { - self.0.read().await.login(username, password, workspace_id).await.map_err(JsCodempError)?; + self.0.read().await.login(username, password, workspace_id).await?; Ok(()) } #[napi] /// join workspace with given id (will start its cursor controller) pub async fn join_workspace(&self, workspace: String) -> napi::Result { - Ok(JsWorkspace::from(self.0.write().await.join_workspace(&workspace).await.map_err(JsCodempError)?)) + Ok(JsWorkspace::from(self.0.write().await.join_workspace(&workspace).await?)) } #[napi] diff --git a/src/ffi/js/cursor.rs b/src/ffi/js/cursor.rs index 8c994c8..3c2c556 100644 --- a/src/ffi/js/cursor.rs +++ b/src/ffi/js/cursor.rs @@ -3,34 +3,64 @@ use napi_derive::napi; use uuid::Uuid; use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode, ErrorStrategy}; use crate::api::Controller; -use crate::ffi::js::JsCodempError; +use crate::prelude::*; -#[napi] -pub struct JsCursorController(Arc); -impl From::> for JsCursorController { - fn from(value: Arc) -> Self { - JsCursorController(value) + + +#[napi(js_name = "Cursor")] +pub struct JsCursor { + /// range of text change, as char indexes in buffer previous state + pub start_row: i32, + pub start_col: i32, + pub end_row: i32, + pub end_col: i32, + pub buffer: String, + pub user: Option, +} + +impl From for CodempCursor { + fn from(value: JsCursor) -> Self { + CodempCursor { + start : (value.start_row, value.start_col), + end: (value.end_row, value.end_col), + buffer: value.buffer, + user: value.user.map(|x| uuid::Uuid::parse_str(&x).expect("invalid uuid")), + } + } +} +impl From for JsCursor { + fn from(value: CodempCursor) -> Self { + JsCursor { + start_row : value.start.0, + start_col : value.start.1, + end_row : value.end.0, + end_col: value.end.1, + buffer: value.buffer, + user: value.user.map(|x| x.to_string()) + } + } } + #[napi] -impl JsCursorController { +impl CodempCursorController { #[napi(ts_args_type = "fun: (event: JsCursorEvent) => void")] pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ - let tsfn : ThreadsafeFunction = + let tsfn : ThreadsafeFunction = fun.create_threadsafe_function(0, - |ctx : ThreadSafeCallContext| { - Ok(vec![JsCursorEvent::from(ctx.value)]) + |ctx : ThreadSafeCallContext| { + Ok(vec![ctx.value]) } )?; - let _controller = self.0.clone(); + let _controller = self.clone(); tokio::spawn(async move { loop { match _controller.recv().await { Ok(event) => { - tsfn.call(event.clone(), ThreadsafeFunctionCallMode::NonBlocking); //check this shit with tracing also we could use Ok(event) to get the error + tsfn.call(event.into(), ThreadsafeFunctionCallMode::NonBlocking); //check this shit with tracing also we could use Ok(event) to get the error }, Err(crate::Error::Deadlocked) => continue, Err(e) => break tracing::warn!("error receiving: {}", e), @@ -41,13 +71,8 @@ impl JsCursorController { } #[napi] - pub fn send(&self, buffer: String, start: (i32, i32), end: (i32, i32)) -> napi::Result<()> { - let pos = codemp_proto::cursor::CursorPosition { - buffer: buffer.into(), - start: codemp_proto::cursor::RowCol::from(start), - end: codemp_proto::cursor::RowCol::from(end), - }; - Ok(self.0.send(pos).map_err(JsCodempError)?) + pub fn send(&self, pos: &CodempCursorController) -> napi::Result<()> { + Ok(self.send(pos)?) } } diff --git a/src/ffi/js/workspace.rs b/src/ffi/js/workspace.rs index ce6d924..b7571f7 100644 --- a/src/ffi/js/workspace.rs +++ b/src/ffi/js/workspace.rs @@ -1,51 +1,39 @@ use std::sync::Arc; use napi_derive::napi; - -use crate::ffi::js::{JsCodempError, buffer::JsBufferController, cursor::JsCursorController}; +use crate::prelude::*; #[napi] -/// a reference to a codemp workspace -pub struct JsWorkspace(Arc); - -impl From> for JsWorkspace { - fn from(value: Arc) -> Self { - JsWorkspace(value) - } -} - -#[napi] -impl JsWorkspace { - - #[napi] - pub fn id(&self) -> String { - self.0.id() +impl CodempWorkspace { + #[napi(js_name = "id")] + pub fn js_id(&self) -> String { + self.id() } - #[napi] - pub fn filetree(&self) -> Vec { - self.0.filetree() + #[napi(js_name = "filetree")] + pub fn js_filetree(&self) -> Vec { + self.filetree() } - #[napi] - pub fn cursor(&self) -> JsCursorController { - JsCursorController::from(self.0.cursor()) + #[napi(js_name = "cursor")] + pub fn js_cursor(&self) -> CodempCursorController { + self.cursor() } - #[napi] - pub fn buffer_by_name(&self, path: String) -> Option { - self.0.buffer_by_name(&path).map(|b| JsBufferController::from(b)) + #[napi(js_name = "buffer_by_name")] + pub fn js_buffer_by_name(&self, path: String) -> Option { + self.buffer_by_name(&path) } - #[napi] - pub async fn create(&self, path: String) -> napi::Result<()> { - Ok(self.0.create(&path).await.map_err(JsCodempError)?) + #[napi(js_name = "create")] + pub async fn js_create(&self, path: String) -> napi::Result<()> { + Ok(self.create(&path).await?) } - #[napi] - pub async fn attach(&self, path: String) -> napi::Result { - Ok(JsBufferController::from(self.0.attach(&path).await.map_err(JsCodempError)?)) + #[napi(js_name = "attach")] + pub async fn js_attach(&self, path: String) -> napi::Result { + Ok(self.attach(&path).await?) } /*#[napi] diff --git a/src/workspace.rs b/src/workspace.rs index 9eb65ae..3684ded 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -21,6 +21,9 @@ use tokio::sync::mpsc; use tonic::Streaming; use uuid::Uuid; +#[cfg(feature = "js")] +use napi_derive::napi; + //TODO may contain more info in the future #[derive(Debug, Clone)] pub struct UserInfo { @@ -29,6 +32,7 @@ pub struct UserInfo { #[derive(Debug, Clone)] #[cfg_attr(feature = "python", pyo3::pyclass)] +#[cfg_attr(feature = "js", napi)] pub struct Workspace(Arc); #[derive(Debug)] @@ -227,27 +231,32 @@ impl Workspace { } /// get the id of the workspace + // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 pub fn id(&self) -> String { self.0.id.clone() } /// return a reference to current cursor controller, if currently in a workspace + // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 pub fn cursor(&self) -> cursor::Controller { self.0.cursor.clone() } /// get a new reference to a buffer controller, if any is active to given path + // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 pub fn buffer_by_name(&self, path: &str) -> Option { self.0.buffers.get(path).map(|x| x.clone()) } /// get a list of all the currently attached to buffers + // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 pub fn buffer_list(&self) -> Vec { self.0.buffers.iter().map(|elem| elem.key().clone()).collect() } /// get the currently cached "filetree" + // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 pub fn filetree(&self) -> Vec { self.0.filetree.iter().map(|f| f.clone()).collect() } -} +} \ No newline at end of file