updated glue to codemp 0.6.0 + refactor

This commit is contained in:
frelodev 2024-02-10 19:02:11 +01:00
parent 0a1c88d09c
commit e0cb9fb464
8 changed files with 624 additions and 320 deletions

View file

@ -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"] }

218
src/codemp.ts Normal file
View file

@ -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
}

107
src/rust/buffer.rs Normal file
View file

@ -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::<codemp::api::TextChange> 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::<Arc<codemp::buffer::Controller>> for JsBufferController {
fn from(value: Arc<codemp::buffer::Controller>) -> Self {
JsBufferController(value)
}
}
#[napi]
pub struct JsBufferController(Arc<codemp::buffer::Controller>);
/*#[napi]
pub fn delta(string : String, start: i64, txt: String, end: i64 ) -> Option<JsCodempOperationSeq> {
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<codemp::api::TextChange, Fatal> =
fun.create_threadsafe_function(0,
|ctx : ThreadSafeCallContext<codemp::api::TextChange>| {
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<String> {
Ok(self.0.content())
}
#[napi]
pub async fn recv(&self) -> napi::Result<JsTextChange> {
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)?)
}
}

45
src/rust/client.rs Normal file
View file

@ -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<codemp::Client>);
#[napi]
/// connect to codemp servers and return a client session
pub async fn connect(addr: Option<String>) -> napi::Result<JsCodempClient>{
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<String>) -> 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<JsWorkspace> {
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<Option<JsWorkspace>> {
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<String> {
Ok(self.0.read().await.user_id().to_string())
}
}

118
src/rust/cursor.rs Normal file
View file

@ -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<codemp::cursor::Controller>);
impl From::<Arc<codemp::cursor::Controller>> for JsCursorController {
fn from(value: Arc<codemp::cursor::Controller>) -> Self {
JsCursorController(value)
}
}
#[napi]
impl JsCursorController {
/*#[napi]
pub fn call_threadsafe_recv(callback: JsFunction) -> Result<()>{
let tsfn: ThreadsafeFunction<u32, ErrorStrategy::CalleeHandled> =
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<codemp::proto::cursor::CursorEvent, ErrorStrategy::Fatal> =
fun.create_threadsafe_function(0,
|ctx : ThreadSafeCallContext<codemp::proto::cursor::CursorEvent>| {
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<JsCursorEvent> {
// 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::<codemp::proto::cursor::CursorEvent> 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::<codemp::proto::cursor::RowCol> for JsRowCol {
fn from(value: codemp::proto::cursor::RowCol) -> Self {
JsRowCol { row: value.row, col: value.col }
}
}

View file

@ -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<OpTuple>,
}
#[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::<JsCodempError> 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<JsBufferController> {
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<bool, napi::Error> {
CODEMP_INSTANCE.disconnect_buffer(&path).await.map_err(|e| napi::Error::from(JsCodempError(e)))
}
/// CURSOR
#[napi]
pub async fn join(session: String) -> napi::Result<JsCursorController> {
let controller = CODEMP_INSTANCE.join(&session).await
.map_err(|e| napi::Error::from(JsCodempError(e)))?;
Ok(controller.into())
}
#[napi]
pub struct JsCursorController(Arc<CodempCursorController>);
impl From::<Arc<CodempCursorController>> for JsCursorController {
fn from(value: Arc<CodempCursorController>) -> Self {
JsCursorController(value)
}
}
#[napi]
impl JsCursorController {
/*#[napi]
pub fn call_threadsafe_recv(callback: JsFunction) -> Result<()>{
let tsfn: ThreadsafeFunction<u32, ErrorStrategy::CalleeHandled> =
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<CodempCursorEvent, Fatal> =
fun.create_threadsafe_function(0,
|ctx : ThreadSafeCallContext<CodempCursorEvent>| {
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<JsCursorEvent> {
// 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::<CursorEvent> 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::<RowCol> 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::<CodempTextChange> 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::<Arc<CodempBufferController>> for JsBufferController {
fn from(value: Arc<CodempBufferController>) -> Self {
JsBufferController(value)
}
}
#[napi]
pub struct JsBufferController(Arc<CodempBufferController>);
/*#[napi]
pub fn delta(string : String, start: i64, txt: String, end: i64 ) -> Option<JsCodempOperationSeq> {
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<CodempTextChange, Fatal> =
fun.create_threadsafe_function(0,
|ctx : ThreadSafeCallContext<CodempTextChange>| {
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<String> {
Ok(self.0.content())
}
#[napi]
pub async fn recv(&self) -> napi::Result<JsTextChange> {
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<String>) -> 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<JsBufferController> {
Ok(
CODEMP_INSTANCE.attach(&path).await
.map_err(|e| napi::Error::from(JsCodempError(e)))?
.into()
)
}
}

70
src/rust/op_cache.rs Normal file
View file

@ -0,0 +1,70 @@
use std::collections::HashSet;
pub type OpTuple = (String, u32, String, u32);
#[napi]
pub struct OpCache {
store: HashSet<OpTuple>,
}
#[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));
}
}

56
src/rust/workspace.rs Normal file
View file

@ -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<codemp::Workspace>);
impl From<Arc<codemp::Workspace>> for JsWorkspace {
fn from(value: Arc<codemp::Workspace>) -> Self {
JsWorkspace(value)
}
}
#[napi]
impl JsWorkspace {
#[napi]
pub fn id(&self) -> napi::Result<String> {
Ok(self.0.id())
}
#[napi]
pub fn filetree(&self) -> napi::Result<Vec<String>> {
Ok(self.0.filetree())
}
#[napi]
pub fn cursor(&self) -> napi::Result<JsCursorController> {
Ok(JsCursorController::from(self.0.cursor()))
}
#[napi]
pub fn buffer_by_name(&self, path: String) -> napi::Result<Option<JsBufferController>> {
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<JsBufferController> {
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)
}*/
}