From 7f9422103a135031907ab18bab03e306695f5623 Mon Sep 17 00:00:00 2001 From: frelodev Date: Sat, 19 Aug 2023 18:43:22 +0200 Subject: [PATCH] feat: switched from neon to napi Co-authored-by: alemi --- .cargo/config | 4 + .gitignore | 2 + Cargo.toml | 17 ++- build.rs | 5 + package.json | 114 ++++++++++++---- src/lib.rs | 367 +++++++++++++++++++------------------------------- 6 files changed, 247 insertions(+), 262 deletions(-) create mode 100644 build.rs diff --git a/.cargo/config b/.cargo/config index c91c3f3..e09164b 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,6 @@ [net] git-fetch-with-cli = true + +[target.aarch64-unknown-linux-musl] +linker = "aarch64-linux-musl-gcc" +rustflags = ["-C", "target-feature=-crt-static"] diff --git a/.gitignore b/.gitignore index 6ab98ef..ad591a0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ Cargo.lock /client/vscode/*.vsix /client/vscode/codemp.node +node_modules + diff --git a/Cargo.toml b/Cargo.toml index d65068e..4104672 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,20 +3,27 @@ name = "codemp-vscode" version = "0.0.1" description = "VSCode extension for CodeMP" edition = "2021" -exclude = ["index.node"] [lib] crate-type = ["cdylib"] [dependencies] -codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.3" } +# codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.3" } +codemp = { path = "../../lib", features = ["global"]} tracing = "0.1" tracing-subscriber = "0.3" uuid = { version = "1.3.1", features = ["v4"] } -once_cell = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" rmpv = "1" -clap = { version = "4.2.1", features = ["derive"] } async-trait = "0.1.68" -neon = { version = "0.10.1", default-features = false, features = ["channel-api", "napi-6", "promise-api"] } +napi = { version = "2", features = ["full"] } +napi-derive = "2" +futures = "0.3.28" +tokio = {version = "1.32.0", features = ["full"] } + +[build-dependencies] +napi-build = "2" + +[profile.release] +lto = true diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..f8bfd67 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +extern crate napi_build; + +fn main() { + napi_build::setup(); +} diff --git a/package.json b/package.json index 86b36a1..da7e2b2 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,96 @@ { - "name": "codemp-vscode", - "version": "0.0.1", - "description": "VSCode extension for CodeMP", - "main": "./out/extension.js", + "name": "@codemp/vscode", + "version": "0.2.0", + "description": "codemp bindings for vscode plugin", + "main": "index.js", + "files": [ + "index.js" + ], + "napi": { + "name": "codemp-vscode", + "triples": { + "defaults": true, + "additional": [ + "x86_64-unknown-linux-musl", + "aarch64-unknown-linux-gnu", + "i686-pc-windows-msvc", + "armv7-unknown-linux-gnueabihf", + "aarch64-apple-darwin", + "aarch64-linux-android", + "x86_64-unknown-freebsd", + "aarch64-unknown-linux-musl", + "aarch64-pc-windows-msvc", + "armv7-linux-androideabi" + ] + } + }, "engines": { - "vscode": "^1.32.0" + "node": ">= 10" }, "scripts": { - "build": "cargo-cp-artifact --artifact cdylib codemp-vscode codemp.node -- cargo build --release --message-format=json-render-diagnostics", - "install": "npm run build", - "test": "cargo test" + "artifacts": "napi artifacts", + "bench": "node -r @swc-node/register benchmark/bench.ts", + "build": "napi build --platform --release --pipe \"prettier -w\"", + "build:debug": "napi build --platform --pipe \"prettier -w\"", + "format": "run-p format:prettier format:rs format:toml", + "format:prettier": "prettier . -w", + "format:toml": "taplo format", + "format:rs": "cargo fmt", + "lint": "eslint . -c ./.eslintrc.yml", + "prepublishOnly": "napi prepublish -t npm", + "test": "ava", + "version": "napi version" }, "devDependencies": { - "cargo-cp-artifact": "^0.1" + "@napi-rs/cli": "^2.14.6", + "@swc-node/register": "^1.5.5", + "@swc/core": "^1.3.32", + "@taplo/cli": "^0.5.2", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "ava": "^5.1.1", + "benny": "^3.7.1", + "chalk": "^5.2.0", + "eslint": "^8.33.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^5.0.0", + "husky": "^8.0.3", + "lint-staged": "^14.0.0", + "npm-run-all": "^4.1.5", + "prettier": "^3.0.0", + "typescript": "^5.0.0" }, - "contributes": { - "commands": [ - { - "command": "codemp.connect", - "title": "Connect to CodeMP" - }, - { - "command": "codemp.join", - "title": "Join remote session" - }, - { - "command": "codemp.share", - "title": "Share local session" - } + "lint-staged": { + "*.@(js|ts|tsx)": [ + "eslint -c .eslintrc.yml --fix" + ], + "*.@(js|ts|tsx|yml|yaml|md|json)": [ + "prettier --write" + ], + "*.toml": [ + "taplo format" ] }, - "activationEvents": [ - "onCommand:codemp.connect", - "onCommand:codemp.join", - "onCommand:codemp.share" - ] + "ava": { + "require": [ + "@swc-node/register" + ], + "extensions": [ + "ts" + ], + "timeout": "2m", + "workerThreads": false, + "environmentVariables": { + "TS_NODE_PROJECT": "./tsconfig.json" + } + }, + "prettier": { + "printWidth": 120, + "semi": false, + "trailingComma": "all", + "singleQuote": true, + "arrowParens": "always" + }, + "packageManager": "yarn@3.6.2" } diff --git a/src/lib.rs b/src/lib.rs index f8289c4..70de490 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,268 +1,177 @@ +#![deny(clippy::all)] + use std::sync::Arc; - -use neon::prelude::*; -use once_cell::sync::OnceCell; +use futures::prelude::*; +use napi::bindgen_prelude::*; use codemp::{ - controller::cursor::{CursorSubscriber, CursorControllerHandle}, - controller::buffer::{OperationControllerHandle, OperationControllerSubscriber}, - client::CodempClient, - factory::OperationFactory, - proto::buffer_client::BufferClient, + prelude::*, + proto::{RowCol, CursorEvent}, + buffer::factory::OperationFactory, ot::OperationSeq }; -use codemp::tokio::{runtime::Runtime, sync::Mutex}; +use napi_derive::napi; +use napi::tokio::{self, fs}; -fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> { - static RUNTIME: OnceCell = OnceCell::new(); +#[derive(Debug)] +struct JsCodempError(CodempError); - RUNTIME.get_or_try_init(|| { - Runtime::new() - .or_else(|err| cx.throw_error(err.to_string())) - }) +impl From:: for napi::Error { + fn from(value: JsCodempError) -> Self { + napi::Error::new(Status::GenericFailure, &format!("CodempError: {:?}", value)) + } } -fn tuple<'a, C: Context<'a>>(cx: &mut C, a: i32, b: i32) -> NeonResult> { - let obj = cx.empty_array(); - let a_val = cx.number(a); - obj.set(cx, 0, a_val)?; - let b_val = cx.number(b); - obj.set(cx, 1, b_val)?; - Ok(obj) +#[napi] +pub fn connect(addr: String) -> napi::Result<()> { + CODEMP_INSTANCE.connect(&addr) + .map_err(|e| JsCodempError(e).into()) } -fn unpack_tuple<'a, C: Context<'a>>(cx: &mut C, arr: Handle<'a, JsArray>) -> NeonResult<(i32, i32)> { - Ok(( - arr.get::(cx, 0)?.value(cx) as i32, - arr.get::(cx, 1)?.value(cx) as i32, - )) + + +/// CURSOR + +#[napi] +pub fn join(session: String) -> napi::Result { + let controller = CODEMP_INSTANCE.join(&session) + .map_err(|e| napi::Error::from(JsCodempError(e)))?; + Ok(controller.into()) } -struct ClientHandle(Arc>); -impl Finalize for ClientHandle {} +#[napi] +pub struct JsCursorController(Arc); -fn connect(mut cx: FunctionContext) -> JsResult { - let host = cx.argument::(0).ok().map(|x| x.value(&mut cx)); +impl From::> for JsCursorController { + fn from(value: Arc) -> Self { + JsCursorController(value) + } +} - let (deferred, promise) = cx.promise(); - let channel = cx.channel(); +#[napi] +impl JsCursorController { - runtime(&mut cx)?.spawn(async move { - match BufferClient::connect(host.unwrap_or("".into())).await { - Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::>(format!("{}", e))), - Ok(c) => deferred.settle_with(&channel, |mut cx| { - let obj = cx.empty_object(); - let boxed_value = cx.boxed(ClientHandle(Arc::new(Mutex::new(c.into())))); - obj.set(&mut cx, "boxed", boxed_value)?; - let method_create = JsFunction::new(&mut cx, create_client)?; - obj.set(&mut cx, "create", method_create)?; - let method_listen = JsFunction::new(&mut cx, listen_client)?; - obj.set(&mut cx, "listen", method_listen)?; - let method_attach = JsFunction::new(&mut cx, attach_client)?; - obj.set(&mut cx, "attach", method_attach)?; - Ok(obj) - }), + #[napi] + pub async fn recv(&self) -> napi::Result { + Ok( + self.0.recv().await + .map_err(|e| napi::Error::from(JsCodempError(e)))? + .into() + ) + } + + #[napi] + pub async 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).await + .map_err(|e| napi::Error::from(JsCodempError(e))) + } +} + +#[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 }, } - }); - - Ok(promise) + } } -fn create_client(mut cx: FunctionContext) -> JsResult { - let path = cx.argument::(0)?.value(&mut cx); - let content = cx.argument::(1).ok().map(|x| x.value(&mut cx)); - let this = cx.this(); - let boxed : Handle> = this.get(&mut cx, "boxed")?; +#[napi(object)] +pub struct JsRowCol { + pub row: i32, + pub col: i32 +} - let rc = boxed.0.clone(); - let (deferred, promise) = cx.promise(); - let channel = cx.channel(); +impl From:: for JsRowCol { + fn from(value: RowCol) -> Self { + JsRowCol { row: value.row, col: value.col } + } +} - runtime(&mut cx)?.spawn(async move { - match rc.lock().await.create(path, content).await { - Ok(accepted) => deferred.settle_with(&channel, move |mut cx| Ok(cx.boolean(accepted))), - Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::>(e.to_string())), + + +/// 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, } - }); - - Ok(promise) + } } -fn listen_client(mut cx: FunctionContext) -> JsResult { - let this = cx.this(); - let boxed : Handle> = this.get(&mut cx, "boxed")?; - - let rc = boxed.0.clone(); - let (deferred, promise) = cx.promise(); - let channel = cx.channel(); - - runtime(&mut cx)?.spawn(async move { - match rc.lock().await.listen().await { - Ok(controller) => { - deferred.settle_with(&channel, move |mut cx| { - let obj = cx.empty_object(); - let boxed_value = cx.boxed(CursorEventsHandle(controller)); - obj.set(&mut cx, "boxed", boxed_value)?; - let callback_method = JsFunction::new(&mut cx, callback_cursor)?; - obj.set(&mut cx, "callback", callback_method)?; - let send_method = JsFunction::new(&mut cx, send_cursor)?; - obj.set(&mut cx, "send", send_method)?; - Ok(obj) - }) - }, - Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::>(e.to_string())), - } - }); - - Ok(promise) -} - -fn attach_client(mut cx: FunctionContext) -> JsResult { - let this = cx.this(); - let boxed : Handle> = this.get(&mut cx, "boxed")?; - let path = cx.argument::(0)?.value(&mut cx); - - let rc = boxed.0.clone(); - let (deferred, promise) = cx.promise(); - let channel = cx.channel(); - - runtime(&mut cx)?.spawn(async move { - match rc.lock().await.attach(path).await { - Ok(controller) => { - deferred.settle_with(&channel, move |mut cx| { - let obj = cx.empty_object(); - let boxed_value = cx.boxed(OperationControllerJs(controller)); - obj.set(&mut cx, "boxed", boxed_value)?; - let apply_method = JsFunction::new(&mut cx, apply_operation)?; - obj.set(&mut cx, "apply", apply_method)?; - let content_method = JsFunction::new(&mut cx, content_operation)?; - obj.set(&mut cx, "content", content_method)?; - let callback_method = JsFunction::new(&mut cx, callback_operation)?; - obj.set(&mut cx, "callback", callback_method)?; - Ok(obj) - }) - }, - Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::>(e.to_string())), - } - }); - - Ok(promise) +impl From::> for JsBufferController { + fn from(value: Arc) -> Self { + JsBufferController(value) + } } -struct OperationControllerJs(OperationControllerHandle); -impl Finalize for OperationControllerJs {} +#[napi] +pub struct JsBufferController(Arc); -fn apply_operation(mut cx: FunctionContext) -> JsResult { - let this = cx.this(); - let boxed : Handle> = this.get(&mut cx, "boxed")?; - let skip = cx.argument::(0)?.value(&mut cx).round() as usize; - let text = cx.argument::(1)?.value(&mut cx); - let tail = cx.argument::(2)?.value(&mut cx).round() as usize; - let rc = boxed.0.clone(); - let (deferred, promise) = cx.promise(); - let channel = cx.channel(); +#[napi] +impl JsBufferController { + + #[napi] + pub async fn recv(&self) -> napi::Result { + Ok( + self.0.recv().await + .map_err(|e| napi::Error::from(JsCodempError(e)))? + .into() + ) + } + + //#[napi] + pub async fn send(&self, op: OperationSeq) -> napi::Result<()> { + self.0.send(op).await + .map_err(|e| napi::Error::from(JsCodempError(e))) + } - runtime(&mut cx)?.spawn(async move { - if let Some(op) = rc.delta(skip, text.as_str(), tail) { - rc.apply(op).await; - deferred.settle_with(&channel, move |mut cx| Ok(cx.boolean(true))); - } else { - deferred.settle_with(&channel, move |mut cx| Ok(cx.undefined())); - } - }); - Ok(promise) } -fn content_operation(mut cx: FunctionContext) -> JsResult { - let this = cx.this(); - let boxed : Handle> = this.get(&mut cx, "boxed")?; - Ok(cx.string(boxed.0.content())) +#[napi] +pub fn create(path: String, content: Option) -> napi::Result<()> { + CODEMP_INSTANCE.create(&path, content.as_deref()) + .map_err(|e| napi::Error::from(JsCodempError(e))) } -fn callback_operation(mut cx: FunctionContext) -> JsResult { - let this = cx.this(); - let boxed : Handle> = this.get(&mut cx, "boxed")?; - let callback = Arc::new(cx.argument::(0)?.root(&mut cx)); - - let mut rc = boxed.0.clone(); - let channel = cx.channel(); - - // TODO when garbage collecting OperationController stop this worker - runtime(&mut cx)?.spawn(async move { - while let Some(edit) = rc.poll().await { - let cb = callback.clone(); - channel.send(move |mut cx| { - cb.to_inner(&mut cx) - .call_with(&cx) - .arg(cx.number(edit.span.start as i32)) - .arg(cx.number(edit.span.end as i32)) - .arg(cx.string(edit.content)) - .apply::(&mut cx)?; - Ok(()) - }); - } - }); - - Ok(cx.undefined()) +#[napi] +pub fn attach(path: String) -> napi::Result { + Ok( + CODEMP_INSTANCE.attach(&path) + .map_err(|e| napi::Error::from(JsCodempError(e)))? + .into() + ) } -struct CursorEventsHandle(CursorControllerHandle); -impl Finalize for CursorEventsHandle {} -fn callback_cursor(mut cx: FunctionContext) -> JsResult { - let this = cx.this(); - let boxed : Handle> = this.get(&mut cx, "boxed")?; - let callback = Arc::new(cx.argument::(0)?.root(&mut cx)); - let mut rc = boxed.0.clone(); - let channel = cx.channel(); - // TODO when garbage collecting OperationController stop this worker - runtime(&mut cx)?.spawn(async move { - while let Some(op) = rc.poll().await { - let cb = callback.clone(); - channel.send(move |mut cx| { - cb.to_inner(&mut cx) - .call_with(&cx) - .arg(cx.string(&op.user)) - .arg(cx.string(&op.buffer)) - .arg(tuple(&mut cx, op.start().row, op.start().col)?) - .arg(tuple(&mut cx, op.end().row, op.end().col)?) - .apply::(&mut cx)?; - Ok(()) - }); - } - }); - - Ok(cx.undefined()) -} - -fn send_cursor(mut cx: FunctionContext) -> JsResult { - let this = cx.this(); - let boxed : Handle> = this.get(&mut cx, "boxed")?; - let path = cx.argument::(0)?.value(&mut cx); - let start_obj = cx.argument::(1)?; - let start = unpack_tuple(&mut cx, start_obj)?; - let end_obj = cx.argument::(2)?; - let end = unpack_tuple(&mut cx, end_obj)?; - - let rc = boxed.0.clone(); - let (deferred, promise) = cx.promise(); - let channel = cx.channel(); - - runtime(&mut cx)?.spawn(async move { - rc.send(&path, start.into(), end.into()).await; - deferred.settle_with(&channel, |mut cx| Ok(cx.undefined())) - }); - - Ok(promise) -} - -#[neon::main] -fn main(mut cx: ModuleContext) -> NeonResult<()> { - cx.export_function("connect", connect)?; - - Ok(()) -}