From d79886e613fd506a5e9a53eaaf60f6b071409b45 Mon Sep 17 00:00:00 2001 From: alemi Date: Sun, 2 Jul 2023 23:58:06 +0200 Subject: [PATCH] feat: initial rust bindings for vscode client --- .editorconfig | 3 + Cargo.toml | 2 +- client/vscode/Cargo.toml | 22 +++ client/vscode/package.json | 28 ++++ client/vscode/src/extension.js | 19 +++ client/vscode/src/lib.rs | 244 +++++++++++++++++++++++++++++++++ 6 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 client/vscode/Cargo.toml create mode 100644 client/vscode/package.json create mode 100644 client/vscode/src/extension.js create mode 100644 client/vscode/src/lib.rs diff --git a/.editorconfig b/.editorconfig index 011b707..4935540 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,3 +8,6 @@ indent_size = 4 [*.rs] indent_size = 2 + +[*.js] +indent_size = 2 diff --git a/Cargo.toml b/Cargo.toml index ca5b0c7..0ffc756 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["client/nvim", "server"] +members = ["client/nvim", "client/vscode", "server"] [package] name = "codemp" diff --git a/client/vscode/Cargo.toml b/client/vscode/Cargo.toml new file mode 100644 index 0000000..457e394 --- /dev/null +++ b/client/vscode/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "codemp-vscode" +version = "0.0.1" +description = "VSCode extension for CodeMP" +edition = "2021" +exclude = ["index.node"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +codemp = { path = "../.." } +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"] } diff --git a/client/vscode/package.json b/client/vscode/package.json new file mode 100644 index 0000000..53e6801 --- /dev/null +++ b/client/vscode/package.json @@ -0,0 +1,28 @@ +{ + "name": "codemp-vscode", + "version": "0.0.1", + "description": "VSCode extension for CodeMP", + "main": "index.node", + "engines": { + "vscode": "^1.32.0" + }, + "scripts": { + "build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", + "install": "npm run build", + "test": "cargo test" + }, + "devDependencies": { + "cargo-cp-artifact": "^0.1" + }, + "contributes": { + "commands": [ + { + "command": "example.helloWorld", + "title": "Hello World" + } + ] + }, + "activationEvents": [ + "onCommand:example.helloWorld" + ] +} diff --git a/client/vscode/src/extension.js b/client/vscode/src/extension.js new file mode 100644 index 0000000..df5b991 --- /dev/null +++ b/client/vscode/src/extension.js @@ -0,0 +1,19 @@ +const vscode = require("vscode"); + +module.exports = { + activate, + deactivate, +}; + +function activate(context) { + // This must match the command property in the package.json + const commandID = "codemp.connect"; + let disposable = vscode.commands.registerCommand(commandID, sayHello); + context.subscriptions.push(disposable); +} + +function connect() { + vscode.window.showInformationMessage("Connecting to CodeMP!"); +} + +function deactivate() {} diff --git a/client/vscode/src/lib.rs b/client/vscode/src/lib.rs new file mode 100644 index 0000000..e1d7de8 --- /dev/null +++ b/client/vscode/src/lib.rs @@ -0,0 +1,244 @@ +use std::sync::Arc; + +use neon::prelude::*; +use once_cell::sync::OnceCell; +use codemp::{cursor::Cursor, client::CodempClient, tokio::{runtime::Runtime, sync::{Mutex, broadcast}}, proto::buffer_client::BufferClient, operation::{OperationController, OperationFactory}}; + +fn runtime<'a, C: Context<'a>>(cx: &mut C) -> NeonResult<&'static Runtime> { + static RUNTIME: OnceCell = OnceCell::new(); + + RUNTIME.get_or_try_init(|| { + Runtime::new() + .or_else(|err| cx.throw_error(err.to_string())) + }) +} + +struct ClientHandle(Arc>); +impl Finalize for ClientHandle {} + +fn connect(mut cx: FunctionContext) -> JsResult { + let host = cx.argument::(0).ok().map(|x| x.value(&mut cx)); + + let (deferred, promise) = cx.promise(); + let channel = cx.channel(); + + 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)?; + obj.set(&mut cx, "create", method_create)?; + let method_listen = JsFunction::new(&mut cx, listen)?; + obj.set(&mut cx, "listen", method_listen)?; + let method_attach = JsFunction::new(&mut cx, attach)?; + obj.set(&mut cx, "attach", method_attach)?; + let method_cursor = JsFunction::new(&mut cx, cursor)?; + obj.set(&mut cx, "cursor", method_cursor)?; + let test = cx.null(); + obj.set(&mut cx, "test", test)?; + Ok(obj) + }), + } + }); + + Ok(promise) +} + +fn create(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")?; + let rc = boxed.0.clone(); + + let (deferred, promise) = cx.promise(); + let channel = cx.channel(); + + 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())), + } + }); + + Ok(promise) +} + +fn listen(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) => { + let sub = controller.sub(); + deferred.settle_with(&channel, move |mut cx| { + let obj = cx.empty_object(); + let boxed_value = cx.boxed(CursorEventsHandle(Arc::new(Mutex::new(sub)))); + obj.set(&mut cx, "boxed", boxed_value)?; + let poll_method = JsFunction::new(&mut cx, poll_cursor)?; + obj.set(&mut cx, "poll", poll_method)?; + Ok(obj) + }) + }, + Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::>(e.to_string())), + } + }); + + Ok(promise) +} + +fn attach(mut cx: FunctionContext) -> JsResult { + let this = cx.this(); + let boxed : Handle> = this.get(&mut cx, "boxed")?; + let rc = boxed.0.clone(); + let path = cx.argument::(0)?.value(&mut cx); + + 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(OperationControllerHandle(controller)); + obj.set(&mut cx, "boxed", boxed_value)?; + let poll_method = JsFunction::new(&mut cx, poll_operation)?; + obj.set(&mut cx, "poll", poll_method)?; + let content_method = JsFunction::new(&mut cx, content_operation)?; + obj.set(&mut cx, "content", content_method)?; + Ok(obj) + }) + }, + Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::>(e.to_string())), + } + }); + + Ok(promise) +} + +fn cursor(mut cx: FunctionContext) -> JsResult { + let this = cx.this(); + let boxed : Handle> = this.get(&mut cx, "boxed")?; + let rc = boxed.0.clone(); + let path = cx.argument::(0)?.value(&mut cx); + let row = cx.argument::(0)?.value(&mut cx) as i64; + let col = cx.argument::(0)?.value(&mut cx) as i64; + + let (deferred, promise) = cx.promise(); + let channel = cx.channel(); + + runtime(&mut cx)?.spawn(async move { + match rc.lock().await.cursor(path, row, col).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())), + } + }); + + Ok(promise) +} + + +struct OperationControllerHandle(Arc); +impl Finalize for OperationControllerHandle {} + +fn poll_operation(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 { + let span = rc.wait().await; + deferred.settle_with(&channel, move |mut cx| { + let obj = cx.empty_array(); + let start_value = cx.number(span.start as u32); + obj.set(&mut cx, 0, start_value)?; + let end_value = cx.number(span.end as u32); + obj.set(&mut cx, 1, end_value)?; + Ok(obj) + }); + }); + + Ok(promise) +} + +fn content_operation(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 { + let content = rc.content(); + deferred.settle_with(&channel, move |mut cx| { + Ok(cx.string(content)) + }); + }); + + Ok(promise) +} + + + +struct CursorEventsHandle(Arc>>); +impl Finalize for CursorEventsHandle {} + +fn poll_cursor(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.recv().await { + Ok((name, cursor)) => { + deferred.settle_with(&channel, move |mut cx| { + let obj = cx.empty_object(); + let name_value = cx.string(name); + obj.set(&mut cx, "user", name_value)?; + let buffer_value = cx.string(cursor.buffer); + obj.set(&mut cx, "buffer", buffer_value)?; + let start_value = cx.empty_array(); + let start_value_row = cx.number(cursor.start.row as i32); + start_value.set(&mut cx, 0, start_value_row)?; + let start_value_col = cx.number(cursor.start.col as i32); + start_value.set(&mut cx, 0, start_value_col)?; + obj.set(&mut cx, "start", start_value)?; + let end_value = cx.empty_array(); + let end_value_row = cx.number(cursor.end.row as i32); + end_value.set(&mut cx, 0, end_value_row)?; + let end_value_col = cx.number(cursor.end.col as i32); + end_value.set(&mut cx, 0, end_value_col)?; + obj.set(&mut cx, "end", end_value)?; + Ok(obj) + }) + }, + Err(e) => deferred.settle_with(&channel, move |mut cx| cx.throw_error::>(e.to_string())), + } + }); + + Ok(promise) +} + +#[neon::main] +fn main(mut cx: ModuleContext) -> NeonResult<()> { + + cx.export_function("connect", connect)?; + + Ok(()) +}