From e0cb9fb4648377c07787a01deeffcbb4c6423142 Mon Sep 17 00:00:00 2001 From: frelodev Date: Sat, 10 Feb 2024 19:02:11 +0100 Subject: [PATCH] updated glue to codemp 0.6.0 + refactor --- Cargo.toml | 3 +- src/codemp.ts | 218 ++++++++++++++++++++++++++++ src/rust/buffer.rs | 107 ++++++++++++++ src/rust/client.rs | 45 ++++++ src/rust/cursor.rs | 118 +++++++++++++++ src/rust/lib.rs | 327 ++---------------------------------------- src/rust/op_cache.rs | 70 +++++++++ src/rust/workspace.rs | 56 ++++++++ 8 files changed, 624 insertions(+), 320 deletions(-) create mode 100644 src/codemp.ts create mode 100644 src/rust/buffer.rs create mode 100644 src/rust/client.rs create mode 100644 src/rust/cursor.rs create mode 100644 src/rust/op_cache.rs create mode 100644 src/rust/workspace.rs diff --git a/Cargo.toml b/Cargo.toml index bb4c138..073c368 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,7 @@ crate-type = ["cdylib"] path = "src/rust/lib.rs" [dependencies] -codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag="v0.5.1", features = ["global"] } -#codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", features = ["global"] } +codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag="v0.6.0", features = ["client", "woot"] } tracing = "0.1" tracing-subscriber = "0.3.17" uuid = { version = "1.3.1", features = ["v4"] } diff --git a/src/codemp.ts b/src/codemp.ts new file mode 100644 index 0000000..2d5ce41 --- /dev/null +++ b/src/codemp.ts @@ -0,0 +1,218 @@ +import * as vscode from 'vscode'; +import * as codemp from '../index'; // TODO why won't it work with a custom name??? + + +var CACHE = new codemp.OpCache(); +var BUFFERS : string[][] = []; +let smallNumberDecorationType = vscode.window.createTextEditorDecorationType({}); + +export async function connect() { + let host = await vscode.window.showInputBox({prompt: "server host (default to http://alemi.dev:50052)"}); + if (host === undefined) return // user cancelled with ESC + if (host.length == 0) host = "http://alemi.dev:50052" + await codemp.connect(host); + vscode.window.showInformationMessage(`Connected to codemp @[${host}]`); +} + + +export async function join() { + let workspace = await vscode.window.showInputBox({prompt: "workspace to attach (default to default)"}); + let buffer : string = (await vscode.window.showInputBox({prompt: "buffer name for the file needed to update other clients cursors"}))!; + //let editor = vscode.window.activeTextEditor; + if (workspace === undefined) return // user cancelled with ESC + if (workspace.length == 0) workspace = "default" + + if (buffer === undefined) return // user cancelled with ESC + if (buffer.length == 0) {workspace = "default"; buffer="fucl"; } + + let controller : codemp.JsCursorController = await codemp.join(workspace) + controller.callback(( event:any) => { + let buf : string = event.textEditor.document.uri.toString() + let curPos = vscode.window.activeTextEditor?.selection.active; + let PosNumber : number = curPos?.line as number; + let posizione : vscode.Position = new vscode.Position(0, PosNumber); + let range_start : vscode.Position = new vscode.Position(event.start.row , event.start.col); // -1? + let range_end : vscode.Position = new vscode.Position(event.end.row, event.end.col); // -1? idk if this works it's kinda funny, should test with someone with a working version of codemp + const decorationRange = new vscode.Range(range_start, range_end); + smallNumberDecorationType.dispose(); + smallNumberDecorationType = vscode.window.createTextEditorDecorationType({ + borderWidth: '5px', + borderStyle: 'solid', + overviewRulerColor: 'blue', + overviewRulerLane: vscode.OverviewRulerLane.Right, + light: { + // this color will be used in light color themes + borderColor: 'darkblue' //should create this color based on event.user (uuid) + }, + dark: { + // this color will be used in dark color themes + borderColor: 'lightblue' //should create this color based on event.user (uuid) + } + }); + for (let tuple of BUFFERS) { + if (tuple[0].toString() === buf) { + vscode.window.activeTextEditor?.setDecorations(smallNumberDecorationType, [decorationRange]); + } + } + }); + + + vscode.window.onDidChangeTextEditorSelection((event: vscode.TextEditorSelectionChangeEvent) => { + if (event.kind == vscode.TextEditorSelectionChangeKind.Command) return; // TODO commands might move cursor too + let buf : string = event.textEditor.document.uri.toString() + let selection : vscode.Selection = event.selections[0] // TODO there may be more than one cursor!! + let anchor : [number, number] = [selection.anchor.line, selection.anchor.character]; + let position : [number, number] = [selection.active.line, selection.active.character+1]; + for (let tuple of BUFFERS) { + if (tuple[0].toString() === buf) { + controller.send(tuple[1], anchor, position); + } + } + }); + vscode.window.showInformationMessage(`Connected to workspace @[${workspace}]`); +} + + +export async function createBuffer() { + let workspace="default";//ask which workspace + let bufferName : any = (await vscode.window.showInputBox({prompt: "path of the buffer to create"}))!; + codemp.create(bufferName); + console.log("new buffer created ", bufferName, "\n"); + let editor = vscode.window.activeTextEditor; + + if (editor === undefined) { return } // TODO say something!!!!!! + + /*let range = new vscode.Range( + editor.document.positionAt(0), + editor.document.positionAt(editor.document.getText().length) + )*/ + let buffer : codemp.JsBufferController = await codemp.attach(bufferName); + console.log("buffer"); + console.log(buffer); + //let opSeq = {range.start,editor.document.getText(),range.end} + //buffer.send(range.start,editor.document.getText(),range.end); //test it plz coded this at 10am :( + buffer.send({ + span: { + start: 0, + end: 0 //previous length is 0 + }, + content: editor.document.getText() + }); + console.log("sent all the content", editor.document.getText()); + //Should i disconnect or stay attached to buffer??? +} + + + + + +export async function attach() { + let buffer_name : any = (await vscode.window.showInputBox({prompt: "buffer to attach to"}))!; + let buffer : codemp.JsBufferController = await codemp.attach(buffer_name); + console.log("attached to buffer", buffer_name); + console.log("buffer", buffer); + let editor = vscode.window.activeTextEditor; + + if (editor === undefined) { + let fileUri = buffer_name; + let random = (Math.random() + 1).toString(36).substring(2); + const fileName = ''+ random ; + //const newFileUri = vscode.Uri.file(fileName).with({ scheme: 'untitled', path: fileName }); + + //Create a document not a file so it's temp and it doesn't get saved + const newFileUri = vscode.Uri.file(fileName).with({ scheme: 'untitled', path: "" }); + //vscode.workspace.openTextDocument() + await vscode.workspace.openTextDocument(newFileUri); + vscode.commands.executeCommand('vscode.open', newFileUri); + //vscode.window.showInformationMessage(`Open a file first`); + //return; + } + editor = vscode.window.activeTextEditor!; + //console.log("Buffer = ", buffer, "\n"); + vscode.window.showInformationMessage(`Connected to codemp workspace buffer @[${buffer_name}]`); + + let file_uri : vscode.Uri = editor.document.uri; + BUFFERS.push([file_uri, buffer_name]); + + vscode.workspace.onDidChangeTextDocument((event:vscode.TextDocumentChangeEvent) => { + //console.log(event.reason); + if (event.document.uri != file_uri) return; // ? + for (let change of event.contentChanges) { + if (CACHE.get(buffer_name, change.rangeOffset, change.text, change.rangeOffset + change.rangeLength)) continue; + buffer.send({ + span: { + start: change.rangeOffset, + end: change.rangeOffset+change.rangeLength + }, + content: change.text + }); + } + }); + + //await new Promise((resolve) => setTimeout(resolve, 200)); // tonioware + //console.log("test"); + + buffer.callback((event: any) => { + CACHE.put(buffer_name, event.span.start, event.content, event.span.end); + + if (editor === undefined) { return } // TODO say something!!!!!! + let range = new vscode.Range( + editor.document.positionAt(event.span.start), + editor.document.positionAt(event.span.end) + ) + editor.edit(editBuilder => { + editBuilder + .replace(range, event.content) + }) + }); +} + +export async function disconnectBuffer() { + let buffer : string = (await vscode.window.showInputBox({prompt: "buffer name for the file to disconnect from"}))!; + codemp.disconnectBuffer(buffer); + vscode.window.showInformationMessage(`Disconnected from codemp workspace buffer @[${buffer}]`); +} + +export async function sync() { + let editor = vscode.window.activeTextEditor; + if (editor === undefined) { return } + for (let tuple of BUFFERS) { + console.log(tuple[0].toString()); + //console.log(tuple[1]); + console.log("\n"); + console.log(editor?.document.uri.toString()); + //console.log(BUFFERS[0]); + if (tuple[0].toString() === editor?.document.uri.toString()) { + + let buffer = await codemp.getBuffer(tuple[1]); + if (buffer==null) { + vscode.window.showErrorMessage("This buffer does not exist anymore"); + return; + } + let content = buffer.content(); + let range = new vscode.Range( + editor.document.positionAt(0), + editor.document.positionAt(editor.document.getText().length) + ); + + CACHE.put(tuple[1],0,content,editor.document.getText().length); + editor.edit(editBuilder => editBuilder.replace(range, content)); + return; + } + else{ + vscode.window.showErrorMessage("This buffer is not managed by codemp"); + } + } +} + + + + + + +// This method is called when your extension is deactivated +export function deactivate() { +//Maybe i should disconnect from every workspace and buffer ??? // TODO +} + + diff --git a/src/rust/buffer.rs b/src/rust/buffer.rs new file mode 100644 index 0000000..b449410 --- /dev/null +++ b/src/rust/buffer.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; +use napi::threadsafe_function::{ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode}; +use napi_derive::napi; +use codemp::api::Controller; +use crate::JsCodempError; + +/// 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: codemp::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 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 { + + + #[napi(ts_args_type = "fun: (event: JsTextChange) => void")] + pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ + let tsfn : ThreadsafeFunction = + fun.create_threadsafe_function(0, + |ctx : ThreadSafeCallContext| { + Ok(vec![JsTextChange::from(ctx.value)]) + } + )?; + let _controller = self.0.clone(); + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + 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 + }, + Err(codemp::Error::Deadlocked) => continue, + Err(e) => break tracing::warn!("error receiving: {}", e), + } + } + }); + Ok(()) + } + + + #[napi] + pub fn content(&self) -> napi::Result { + Ok(self.0.content()) + } + + + + + #[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 = codemp::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)?) + } +} \ No newline at end of file diff --git a/src/rust/client.rs b/src/rust/client.rs new file mode 100644 index 0000000..5688c40 --- /dev/null +++ b/src/rust/client.rs @@ -0,0 +1,45 @@ +use napi_derive::napi; +use crate::JsCodempError; +use crate::workspace::JsWorkspace; + +#[napi] +/// main codemp client session +pub struct JsCodempClient(tokio::sync::RwLock); + +#[napi] +/// connect to codemp servers and return a client session +pub async fn connect(addr: Option) -> napi::Result{ + let client = codemp::Client::new(addr.as_deref().unwrap_or("http://codemp.alemi.dev:50053")) + .await + .map_err(JsCodempError)?; + + Ok(JsCodempClient(tokio::sync::RwLock::new(client))) +} + +#[napi] +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)?; + 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)?)) + } + + #[napi] + /// get workspace with given id, if it exists + pub async fn get_workspace(&self, workspace: String) -> napi::Result> { + Ok(self.0.read().await.get_workspace(&workspace).map(|w| JsWorkspace::from(w))) + } + + #[napi] + /// return current sessions's user id + pub async fn user_id(&self) -> napi::Result { + Ok(self.0.read().await.user_id().to_string()) + } +} \ No newline at end of file diff --git a/src/rust/cursor.rs b/src/rust/cursor.rs new file mode 100644 index 0000000..76eb1e2 --- /dev/null +++ b/src/rust/cursor.rs @@ -0,0 +1,118 @@ +use std::sync::Arc; +use napi_derive::napi; +use uuid::Uuid; +use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode, ErrorStrategy}; +use codemp::api::Controller; +use crate::JsCodempError; + +#[napi] +pub struct JsCursorController(Arc); + +impl From::> for JsCursorController { + fn from(value: Arc) -> Self { + JsCursorController(value) + } +} + +#[napi] +impl JsCursorController { + + + /*#[napi] + pub fn call_threadsafe_recv(callback: JsFunction) -> Result<()>{ + let tsfn: ThreadsafeFunction = + callback.create_threadsafe_function(0, |ctx| Ok(vec![ctx.value + 1]))?; + }*/ + + #[napi(ts_args_type = "fun: (event: JsCursorEvent) => void")] + pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ + let tsfn : ThreadsafeFunction = + fun.create_threadsafe_function(0, + |ctx : ThreadSafeCallContext| { + Ok(vec![JsCursorEvent::from(ctx.value)]) + } + )?; + let _controller = self.0.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 + }, + Err(codemp::Error::Deadlocked) => continue, + Err(e) => break tracing::warn!("error receiving: {}", e), + } + } + }); + Ok(()) + } + + + // let controller = codemp.join('default').await + // // TODO register cursor callback, when cursormoved call { controller.send(event) } + // controller.callback( (ev) => { + // editor.change(event.tex) + // }); + + + + + + + // #[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, 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)?) + } +} + + + +#[derive(Debug)] +#[napi(object)] +pub struct JsCursorEvent { + pub user: String, + pub buffer: String, + pub start: JsRowCol, + pub end: JsRowCol, +} + +impl From:: for JsCursorEvent { + fn from(value: codemp::proto::cursor::CursorEvent) -> Self { + let pos = value.position; + let start = pos.start; + let end = pos.end; + JsCursorEvent { + user: Uuid::from(value.user).to_string(), + buffer: pos.buffer.into(), + start: JsRowCol { row: start.row, col: start.col }, + end: JsRowCol { row: end.row, col: end.col }, + } + } +} + +#[derive(Debug)] +#[napi(object)] +pub struct JsRowCol { + pub row: i32, + pub col: i32 +} + +impl From:: for JsRowCol { + fn from(value: codemp::proto::cursor::RowCol) -> Self { + JsRowCol { row: value.row, col: value.col } + } +} \ No newline at end of file diff --git a/src/rust/lib.rs b/src/rust/lib.rs index ec7c7f9..8a8f675 100644 --- a/src/rust/lib.rs +++ b/src/rust/lib.rs @@ -1,324 +1,15 @@ #![deny(clippy::all)] -use std::{sync::Arc, collections::HashSet}; -use codemp::{ - prelude::*, - proto::{RowCol, CursorEvent}, -}; -use napi_derive::napi; -use napi::{Status, threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunctionCallMode, ErrorStrategy::Fatal, ThreadsafeFunction}}; -use napi::tokio; + +pub mod client; +pub mod workspace; +pub mod cursor; +pub mod buffer; + #[derive(Debug)] -struct JsCodempError(CodempError); - -pub type OpTuple = (String, u32, String, u32); - -#[napi] -pub struct OpCache { - store: HashSet, -} - -#[napi] -impl OpCache { - #[napi(constructor)] - pub fn new() -> Self { - OpCache { - store: HashSet::new(), - } - } - - #[napi] - pub fn put(&mut self, buf: String, start: u32, text: String, end: u32) -> bool { - let op = (buf, start, text, end); - let res = self.store.contains(&op); - self.store.insert(op); - res - } - - #[napi] - pub fn get(&mut self, buf: String, start: u32, text: String, end: u32) -> bool { - let op = (buf, start, text, end); - if self.store.contains(&op) { - self.store.remove(&op); - true - } else { - false - } - } -} - - +struct JsCodempError(codemp::Error); impl From:: for napi::Error { fn from(value: JsCodempError) -> Self { - napi::Error::new(Status::GenericFailure, &format!("CodempError: {:?}", value)) + napi::Error::new(napi::Status::GenericFailure, &format!("CodempError: {:?}", value)) } -} - - -#[napi] -pub async fn connect(addr: String) -> napi::Result<()> { - let f = std::fs::File::create("/home/***REMOVED***/projects/codemp/mine/codempvscode/***REMOVED***.txt").unwrap(); - tracing_subscriber::fmt() - .with_ansi(false) - .with_max_level(tracing::Level::INFO) - .with_writer(std::sync::Mutex::new(f)) - .init(); - CODEMP_INSTANCE.connect(&addr).await - .map_err(|e| JsCodempError(e).into()) -} - -#[napi] -pub async fn get_buffer(path: String) -> Option { - let x = CODEMP_INSTANCE.get_buffer(&path) - .await - .ok()?; - Some(JsBufferController(x)) -} - -#[napi] -pub async fn leave_workspace() -> Result<(), napi::Error> { - CODEMP_INSTANCE.leave_workspace().await.map_err(|e| napi::Error::from(JsCodempError(e))) -} - -#[napi] -pub async fn disconnect_buffer(path: String) -> Result { - CODEMP_INSTANCE.disconnect_buffer(&path).await.map_err(|e| napi::Error::from(JsCodempError(e))) -} - - - - -/// CURSOR - -#[napi] -pub async fn join(session: String) -> napi::Result { - let controller = CODEMP_INSTANCE.join(&session).await - .map_err(|e| napi::Error::from(JsCodempError(e)))?; - Ok(controller.into()) -} - -#[napi] -pub struct JsCursorController(Arc); - -impl From::> for JsCursorController { - fn from(value: Arc) -> Self { - JsCursorController(value) - } -} - -#[napi] -impl JsCursorController { - - - /*#[napi] - pub fn call_threadsafe_recv(callback: JsFunction) -> Result<()>{ - let tsfn: ThreadsafeFunction = - callback.create_threadsafe_function(0, |ctx| Ok(vec![ctx.value + 1]))?; - }*/ - - #[napi(ts_args_type = "fun: (event: JsCursorEvent) => void")] - pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ - let tsfn : ThreadsafeFunction = - fun.create_threadsafe_function(0, - |ctx : ThreadSafeCallContext| { - Ok(vec![JsCursorEvent::from(ctx.value)]) - } - )?; - let _controller = self.0.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 - }, - Err(CodempError::Deadlocked) => continue, - Err(e) => break tracing::warn!("error receiving: {}", e), - } - } - }); - Ok(()) - } - - - // let controller = codemp.join('default').await - // // TODO register cursor callback, when cursormoved call { controller.send(event) } - // controller.callback( (ev) => { - // editor.change(event.tex) - // }); - - - - - - - // #[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, buffer: String, start: (i32, i32), end: (i32, i32)) -> napi::Result<()> { - let pos = CodempCursorPosition { buffer, start: Some(RowCol::from(start)), end: Some(RowCol::from(end)) }; - self.0.send(pos) - .map_err(|e| napi::Error::from(JsCodempError(e))) - } -} - -#[derive(Debug)] -#[napi(object)] -pub struct JsCursorEvent { - pub user: String, - pub buffer: String, - pub start: JsRowCol, - pub end: JsRowCol, -} - -impl From:: for JsCursorEvent { - fn from(value: CursorEvent) -> Self { - let pos = value.position.unwrap_or_default(); - let start = pos.start.unwrap_or_default(); - let end = pos.end.unwrap_or_default(); - JsCursorEvent { - user: value.user, - buffer: pos.buffer, - start: JsRowCol { row: start.row, col: start.col }, - end: JsRowCol { row: end.row, col: end.col }, - } - } -} - -#[derive(Debug)] -#[napi(object)] -pub struct JsRowCol { - pub row: i32, - pub col: i32 -} - -impl From:: for JsRowCol { - fn from(value: RowCol) -> Self { - JsRowCol { row: value.row, col: value.col } - } -} - - - -/// 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: CodempTextChange) -> 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 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 { - - - #[napi(ts_args_type = "fun: (event: JsTextChange) => void")] - pub fn callback(&self, fun: napi::JsFunction) -> napi::Result<()>{ - let tsfn : ThreadsafeFunction = - fun.create_threadsafe_function(0, - |ctx : ThreadSafeCallContext| { - Ok(vec![JsTextChange::from(ctx.value)]) - } - )?; - let _controller = self.0.clone(); - tokio::spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(200)).await; - 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 - }, - Err(CodempError::Deadlocked) => continue, - Err(e) => break tracing::warn!("error receiving: {}", e), - } - } - }); - Ok(()) - } - - - #[napi] - pub fn content(&self) -> napi::Result { - Ok(self.0.content()) - } - - - - - #[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 = CodempTextChange { - span: op.span.start as usize .. op.span.end as usize, - content: op.content, - }; - self.0.send(new_text_change) - .map_err(|e| napi::Error::from(JsCodempError(e))) - } -} - -#[napi] -pub async fn create(path: String, content: Option) -> napi::Result<()> { - CODEMP_INSTANCE.create(&path, content.as_deref()).await - .map_err(|e| napi::Error::from(JsCodempError(e))) -} - -#[napi] -pub async fn attach(path: String) -> napi::Result { - Ok( - CODEMP_INSTANCE.attach(&path).await - .map_err(|e| napi::Error::from(JsCodempError(e)))? - .into() - ) -} +} \ No newline at end of file diff --git a/src/rust/op_cache.rs b/src/rust/op_cache.rs new file mode 100644 index 0000000..1c0cc9a --- /dev/null +++ b/src/rust/op_cache.rs @@ -0,0 +1,70 @@ +use std::collections::HashSet; + +pub type OpTuple = (String, u32, String, u32); + +#[napi] +pub struct OpCache { + store: HashSet, +} + +#[napi] +impl OpCache { + #[napi(constructor)] + pub fn new() -> Self { + OpCache { + store: HashSet::new(), + } + } + + #[napi] + pub fn put(&mut self, buf: String, start: u32, text: String, end: u32) -> bool { + let op = (buf, start, text, end); + let res = self.store.contains(&op); + self.store.insert(op); + res + } + + #[napi] + pub fn get(&mut self, buf: String, start: u32, text: String, end: u32) -> bool { + let op = (buf, start, text, end); + if self.store.contains(&op) { + self.store.remove(&op); + true + } else { + false + } + } +} + + + + + +#[cfg(test)] +mod test { + #[test] + fn op_cache_put_returns_whether_it_already_contained_the_key() { + let mut op = super::OpCache::new(); + assert!(!op.put("default".into(), 0, "hello world".into(), 0)); // false: did not already contain it + assert!(op.put("default".into(), 0, "hello world".into(), 0)); // true: already contained it + } + #[test] + fn op_cache_contains_only_after_put() { + let mut op = super::OpCache::new(); + assert!(!op.get("default".into(), 0, "hello world".into(), 0)); + op.put("default".into(), 0, "hello world".into(), 0); + assert!(op.get("default".into(), 0, "hello world".into(), 0)); + } + + #[test] + fn op_cache_different_keys(){ + let mut op = super::OpCache::new(); + assert!(!op.get("default".into(), 0, "hello world".into(), 0)); + op.put("default".into(), 0, "hello world".into(), 0); + assert!(op.get("default".into(), 0, "hello world".into(), 0)); + assert!(!op.get("workspace".into(), 0, "hi".into(), 0)); + op.put("workspace".into(), 0, "hi".into(), 0); + assert!(op.get("workspace".into(), 0, "hi".into(), 0)); + } + +} \ No newline at end of file diff --git a/src/rust/workspace.rs b/src/rust/workspace.rs new file mode 100644 index 0000000..3b0b257 --- /dev/null +++ b/src/rust/workspace.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use napi_derive::napi; + +use crate::{JsCodempError, buffer::JsBufferController, cursor::JsCursorController}; + + +#[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) -> napi::Result { + Ok(self.0.id()) + } + + #[napi] + pub fn filetree(&self) -> napi::Result> { + Ok(self.0.filetree()) + } + + #[napi] + pub fn cursor(&self) -> napi::Result { + Ok(JsCursorController::from(self.0.cursor())) + } + + #[napi] + pub fn buffer_by_name(&self, path: String) -> napi::Result> { + Ok(self.0.buffer_by_name(&path).map(|b| JsBufferController::from(b))) + } + + #[napi] + pub async fn create(&self, path: String) -> napi::Result<()> { + Ok(self.0.create(&path).await.map_err(JsCodempError)?) + } + + #[napi] + pub async fn attach(&self, path: String) -> napi::Result { + Ok(JsBufferController::from(self.0.attach(&path).await.map_err(JsCodempError)?)) + } + + /*#[napi] + pub async fn delete(&self, path: String) -> napi::Result<>{ + self.0.delete(&path) + }*/ + +} \ No newline at end of file