From 9b281b1b4c565410b4231b440c510502870b27a7 Mon Sep 17 00:00:00 2001 From: frelodev Date: Thu, 14 Sep 2023 23:22:19 +0200 Subject: [PATCH] merged in a single folder, rust bindings napi + vscode extension --- src/extension.ts | 104 +++++++++++++ src/rust/lib.rs | 242 +++++++++++++++++++++++++++++++ src/test/runTest.ts | 23 +++ src/test/suite/extension.test.ts | 15 ++ src/test/suite/index.ts | 40 +++++ 5 files changed, 424 insertions(+) create mode 100644 src/extension.ts create mode 100644 src/rust/lib.rs create mode 100644 src/test/runTest.ts create mode 100644 src/test/suite/extension.test.ts create mode 100644 src/test/suite/index.ts diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..c6107b3 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,104 @@ +/* + + +vscode ++ src + + glue.rs + + extension.ts ++ Cargo.toml ++ package.json + + +*/ + + + +// The module 'vscode' contains the VS Code extensibility API +// Import the module and reference it with the alias vscode in your code below +import * as vscode from 'vscode'; +//import * as codempp from '/home/***REMOVED***/projects/codemp/mine/codempvscode/codemp.node'; +const codemp = require("/home/***REMOVED***/projects/codemp/mine/vscode/target/debug/libcodemp_vscode.node"); +// import * as codemp from "/home/***REMOVED***/projects/codemp/mine/vscode/target/debug/libcodemp_vscode.node"; + +// This method is called when your extension is activated +// Your extension is activated the very first time the command is executed +export function activate(context: vscode.ExtensionContext) { + + // Use the console to output diagnostic information (console.log) and errors (console.error) + // This line of code will only be executed once when your extension is activated + console.log('Congratulations, your extension "codempvscode" is now active!'); + + // The command has been defined in the package.json file + // Now provide the implementation of the command with registerCommand + // The commandId parameter must match the command field in package.json + let disposable = vscode.commands.registerCommand('codempvscode.helloWorld', () => { + // The code you place here will be executed every time your command is executed + // Display a message box to the user + vscode.window.showInformationMessage(process.cwd()); + }); + let connectCommand = vscode.commands.registerCommand('codempvscode.connect', connect); + let joinCommand = vscode.commands.registerCommand('codempvscode.join', join); + context.subscriptions.push(connectCommand); + context.subscriptions.push(joinCommand); + context.subscriptions.push(disposable); + +} + + +async function connect() { + let host = await vscode.window.showInputBox({prompt: "server host (default to http://alemi.dev:50051)"}) + if (host === undefined) return // user cancelled with ESC + if (host.length == 0) host = "http://alemi.dev:50051" + await codemp.connect(host); + vscode.window.showInformationMessage(`Connected to codemp ***REMOVED*** @[${host}]`); +} + + +async function join() { + let workspace = await vscode.window.showInputBox({prompt: "workspace to attach (default to default)"}) + let buffer = await vscode.window.showInputBox({prompt: "buffer name for the file needed to update other clients cursors"}) + 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 = "test" + + + + let controller = await codemp.join(workspace) + controller.callback((event:any) => { + console.log(event); + }); + + + vscode.window.onDidChangeTextEditorSelection((event: vscode.TextEditorSelectionChangeEvent)=>{ + if(event.kind==1 || event.kind ==2){ + let buf = event.textEditor.document.uri.toString() + let selection = event.selections[0] // TODO there may be more than one cursor!! + //let anchor = [selection.anchor.line+1, selection.anchor.character] + //let position = [selection.active.line+1, selection.active.character+1] + let anchor = [selection.anchor.line, selection.anchor.character] + let position = [selection.active.line, selection.active.character+1] + console.log("Buffer from selection" + buffer+"\n"); + console.log("selection " + selection+"\n"); + console.log("Anchor selection" + anchor+"\n"); + console.log("position selection" + position+"\n"); + controller.send(buffer, anchor, position); + } + }); + vscode.window.showInformationMessage(`Connected to workspace @[${workspace}]`); +} + + + +/*async function attach() { + let workspace = await vscode.window.showInputBox({prompt: "workspace to attach (default to default)"}) + if (workspace === undefined) return // user cancelled with ESC + if (workspace.length == 0) workspace = "default" + await codemp.attach(workspace); + vscode.window.showInformationMessage(`Connected to codemp ***REMOVED*** @[${workspace}]`); +}*/ + + +// This method is called when your extension is deactivated +export function deactivate() {} diff --git a/src/rust/lib.rs b/src/rust/lib.rs new file mode 100644 index 0000000..fbfefae --- /dev/null +++ b/src/rust/lib.rs @@ -0,0 +1,242 @@ +#![deny(clippy::all)] + +use std::sync::Arc; +use futures::prelude::*; +use codemp::{ + prelude::*, + proto::{RowCol, CursorEvent}, + buffer::factory::OperationFactory, ot::OperationSeq +}; +use napi_derive::napi; +use napi::{CallContext, Status, threadsafe_function::{ThreadSafeCallContext, ThreadsafeFunctionCallMode, ErrorStrategy::{CalleeHandled, Fatal}, ThreadsafeFunction}}; +use napi::tokio::{self, fs}; + +#[derive(Debug)] +struct JsCodempError(CodempError); + + + + + + + + +impl From:: for napi::Error { + fn from(value: JsCodempError) -> Self { + napi::Error::new(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/vscode/log.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()) +} + + + +/// 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<()>{ //TODO it sucks but v0.5 will improve it!!! + 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 { + let event = _controller.recv().await.expect("could not receive cursor event!"); + tracing::info!("printing '{:?}' event", event); // works? + tsfn.call(event.clone(), ThreadsafeFunctionCallMode::NonBlocking); //check this shit with tracing also we could use Ok(event) to get the error + tracing::info!("printing '{:?}' event after tsfn", event); // works? + } + }); + 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: Option, +} + +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: Some(value.span.end as i32) }, + content: value.content, + } + } +} + +impl From:: for JsCodempOperationSeq{ + fn from(value: OperationSeq) -> Self { + JsCodempOperationSeq(value) + } +} + + +impl From::> for JsBufferController { + fn from(value: Arc) -> Self { + JsBufferController(value) + } +} + + +#[napi] +pub struct JsBufferController(Arc); + +#[napi(js_name = "CodempOperationSeq")] +pub struct JsCodempOperationSeq(CodempOperationSeq); + + + + +#[napi] +impl JsBufferController { + + #[napi] + pub fn delta(&self, start: i64, txt: String, end: i64) -> Option { + self.0.delta(start as usize, &txt, end as usize).map(|x| x.into()) + } + + #[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: &JsCodempOperationSeq) -> napi::Result<()> { + // TODO might be nice to take ownership of the opseq + self.0.send(op.0.clone()) + .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() + ) +} diff --git a/src/test/runTest.ts b/src/test/runTest.ts new file mode 100644 index 0000000..93a4441 --- /dev/null +++ b/src/test/runTest.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; + +import { runTests } from '@vscode/test-electron'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to test runner + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error('Failed to run tests', err); + process.exit(1); + } +} + +main(); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts new file mode 100644 index 0000000..4ca0ab4 --- /dev/null +++ b/src/test/suite/extension.test.ts @@ -0,0 +1,15 @@ +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +// import * as myExtension from '../../extension'; + +suite('Extension Test Suite', () => { + vscode.window.showInformationMessage('Start all tests.'); + + test('Sample test', () => { + assert.strictEqual(-1, [1, 2, 3].indexOf(5)); + assert.strictEqual(-1, [1, 2, 3].indexOf(0)); + }); +}); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts new file mode 100644 index 0000000..bcf9173 --- /dev/null +++ b/src/test/suite/index.ts @@ -0,0 +1,40 @@ +import * as path from 'path'; +import * as Mocha from 'mocha'; +import * as glob from 'glob'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true + }); + + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise((c, e) => { + const testFiles = new glob.Glob("**/**.test.js", { cwd: testsRoot }); + const testFileStream = testFiles.stream(); + + testFileStream.on("data", (file) => { + mocha.addFile(path.resolve(testsRoot, file)); + }); + testFileStream.on("error", (err) => { + e(err); + }); + testFileStream.on("end", () => { + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err); + } + }); + }); +}