mirror of
https://github.com/hexedtech/codemp-vscode.git
synced 2024-12-23 05:54:51 +01:00
updated glue to codemp 0.6.0 + refactor
This commit is contained in:
parent
0a1c88d09c
commit
e0cb9fb464
8 changed files with 624 additions and 320 deletions
|
@ -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
218
src/codemp.ts
Normal 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
107
src/rust/buffer.rs
Normal 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
45
src/rust/client.rs
Normal 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
118
src/rust/cursor.rs
Normal 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 }
|
||||
}
|
||||
}
|
327
src/rust/lib.rs
327
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<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
70
src/rust/op_cache.rs
Normal 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
56
src/rust/workspace.rs
Normal 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)
|
||||
}*/
|
||||
|
||||
}
|
Loading…
Reference in a new issue