mirror of
https://github.com/hexedtech/codemp.git
synced 2024-12-22 21:04:53 +01:00
feat:added js glue
This commit is contained in:
parent
830ef1fa9b
commit
c748f49941
11 changed files with 502 additions and 9 deletions
14
.gitignore
vendored
14
.gitignore
vendored
|
@ -1,12 +1,10 @@
|
|||
/target
|
||||
|
||||
Cargo.lock
|
||||
|
||||
# vscode extension build files
|
||||
/client/vscode/node_modules/
|
||||
/client/vscode/*.vsix
|
||||
/client/vscode/codemp.node
|
||||
|
||||
.cargo
|
||||
.vscode/
|
||||
|
||||
.vscode/
|
||||
# js
|
||||
node_modules/
|
||||
package-lock.json
|
||||
index.d.ts
|
||||
index.node
|
|
@ -39,14 +39,21 @@ log = { version = "0.4.21", optional = true }
|
|||
mlua = { version = "0.9.6", features = ["module", "luajit", "send"], optional = true }
|
||||
thiserror = { version = "1.0.57", optional = true }
|
||||
derive_more = { version = "0.99.17", optional = true }
|
||||
|
||||
# glue (js)
|
||||
rmpv = { version = "1", optional = true }
|
||||
napi = { version = "2", features = ["full"], optional = true }
|
||||
napi-derive = { version="2", optional = true}
|
||||
futures = { version = "0.3.28", optional = true }
|
||||
[build-dependencies]
|
||||
# glue (java)
|
||||
flapigen = { version = "0.6.0", optional = true }
|
||||
rifgen = { git = "https://github.com/Kofituo/rifgen.git", rev = "d27d9785b2febcf5527f1deb6a846be5d583f7d7", optional = true }
|
||||
# glue (js)
|
||||
napi-build = { version = "2", optional = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
lua = ["mlua", "thiserror", "derive_more", "lazy_static", "tracing-subscriber"]
|
||||
java = ["lazy_static", "jni", "jni-sys", "flapigen", "rifgen", "log"]
|
||||
java-artifact = ["java"] # also builds the jar
|
||||
js = ["napi-build", "tracing-subscriber", "rmpv", "napi", "napi-derive", "futures"]
|
7
build.rs
7
build.rs
|
@ -1,3 +1,6 @@
|
|||
#[cfg(feature = "js")]
|
||||
extern crate napi_build;
|
||||
|
||||
/// The main method of the buildscript, required by some glue modules.
|
||||
fn main() {
|
||||
#[cfg(feature = "java")] {
|
||||
|
@ -74,6 +77,10 @@ fn main() {
|
|||
println!("cargo:rerun-if-changed={}", generated_glue_file.display());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "js")] {
|
||||
napi_build::setup();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "java")]
|
||||
|
|
5
package.json
Normal file
5
package.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
}
|
||||
}
|
110
src/ffi/js/buffer.rs
Normal file
110
src/ffi/js/buffer.rs
Normal file
|
@ -0,0 +1,110 @@
|
|||
use std::sync::Arc;
|
||||
use napi::threadsafe_function::{ErrorStrategy::Fatal, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
||||
use napi_derive::napi;
|
||||
use crate::api::Controller;
|
||||
use crate::ffi::js::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::<crate::api::TextChange> for JsTextChange {
|
||||
fn from(value: crate::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<crate::buffer::Controller>> for JsBufferController {
|
||||
fn from(value: Arc<crate::buffer::Controller>) -> Self {
|
||||
JsBufferController(value)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[napi]
|
||||
pub struct JsBufferController(Arc<crate::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<crate::api::TextChange, Fatal> =
|
||||
fun.create_threadsafe_function(0,
|
||||
|ctx : ThreadSafeCallContext<crate::api::TextChange>| {
|
||||
Ok(vec![JsTextChange::from(ctx.value)])
|
||||
}
|
||||
)?;
|
||||
let _controller = self.0.clone();
|
||||
tokio::spawn(async move {
|
||||
//tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
loop {
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
match _controller.recv().await {
|
||||
Ok(event) => {
|
||||
tsfn.call(event, ThreadsafeFunctionCallMode::NonBlocking); //check this shit with tracing also we could use Ok(event) to get the error
|
||||
},
|
||||
Err(crate::Error::Deadlocked) => continue,
|
||||
Err(e) => break tracing::warn!("error receiving: {}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
#[napi]
|
||||
pub fn content(&self) -> String {
|
||||
self.0.content()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_name(&self) -> String {
|
||||
self.0.name().to_string()
|
||||
}
|
||||
|
||||
#[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 = crate::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/ffi/js/client.rs
Normal file
45
src/ffi/js/client.rs
Normal file
|
@ -0,0 +1,45 @@
|
|||
use napi_derive::napi;
|
||||
use crate::ffi::js::JsCodempError;
|
||||
use crate::ffi::js::workspace::JsWorkspace;
|
||||
|
||||
#[napi]
|
||||
/// main codemp client session
|
||||
pub struct JsCodempClient(tokio::sync::RwLock<crate::Client>);
|
||||
|
||||
#[napi]
|
||||
/// connect to codemp servers and return a client session
|
||||
pub async fn connect(addr: Option<String>) -> napi::Result<JsCodempClient>{
|
||||
let client = crate::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) -> Option<JsWorkspace> {
|
||||
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) -> String {
|
||||
self.0.read().await.user_id().to_string()
|
||||
}
|
||||
}
|
90
src/ffi/js/cursor.rs
Normal file
90
src/ffi/js/cursor.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
use std::sync::Arc;
|
||||
use napi_derive::napi;
|
||||
use uuid::Uuid;
|
||||
use napi::threadsafe_function::{ThreadsafeFunction, ThreadSafeCallContext, ThreadsafeFunctionCallMode, ErrorStrategy};
|
||||
use crate::api::Controller;
|
||||
use crate::ffi::js::JsCodempError;
|
||||
|
||||
#[napi]
|
||||
pub struct JsCursorController(Arc<crate::cursor::Controller>);
|
||||
|
||||
impl From::<Arc<crate::cursor::Controller>> for JsCursorController {
|
||||
fn from(value: Arc<crate::cursor::Controller>) -> Self {
|
||||
JsCursorController(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl JsCursorController {
|
||||
|
||||
#[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(crate::Error::Deadlocked) => continue,
|
||||
Err(e) => break tracing::warn!("error receiving: {}", e),
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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 }
|
||||
}
|
||||
}
|
68
src/ffi/js/mod.rs
Normal file
68
src/ffi/js/mod.rs
Normal file
|
@ -0,0 +1,68 @@
|
|||
#![deny(clippy::all)]
|
||||
|
||||
pub mod client;
|
||||
pub mod workspace;
|
||||
pub mod cursor;
|
||||
pub mod buffer;
|
||||
pub mod op_cache;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct JsCodempError(crate::Error);
|
||||
|
||||
impl From::<JsCodempError> for napi::Error {
|
||||
fn from(value: JsCodempError) -> Self {
|
||||
napi::Error::new(napi::Status::GenericFailure, &format!("CodempError: {:?}", value))
|
||||
}
|
||||
}
|
||||
|
||||
use napi_derive::napi;
|
||||
|
||||
#[napi]
|
||||
pub struct JsLogger(std::sync::Arc<tokio::sync::Mutex<tokio::sync::mpsc::Receiver<String>>>);
|
||||
|
||||
#[napi]
|
||||
impl JsLogger {
|
||||
#[napi(constructor)]
|
||||
pub fn new(debug: Option<bool>) -> JsLogger {
|
||||
let (tx, rx) = tokio::sync::mpsc::channel(256);
|
||||
let level = if debug.unwrap_or(false) { tracing::Level::DEBUG } else {tracing::Level::INFO }; //TODO: study this tracing subscriber and customize it
|
||||
let format = tracing_subscriber::fmt::format()
|
||||
.with_level(true)
|
||||
.with_target(true)
|
||||
.with_thread_ids(false)
|
||||
.with_thread_names(false)
|
||||
.with_ansi(false)
|
||||
.with_file(false)
|
||||
.with_line_number(false)
|
||||
.with_source_location(false)
|
||||
.compact();
|
||||
tracing_subscriber::fmt()
|
||||
.event_format(format)
|
||||
.with_max_level(level)
|
||||
.with_writer(std::sync::Mutex::new(JsLoggerProducer(tx)))
|
||||
.init();
|
||||
JsLogger(std::sync::Arc::new(tokio::sync::Mutex::new(rx)))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn message(&self) -> Option<String> {
|
||||
self.0
|
||||
.lock()
|
||||
.await
|
||||
.recv()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct JsLoggerProducer(tokio::sync::mpsc::Sender<String>);
|
||||
|
||||
impl std::io::Write for JsLoggerProducer {
|
||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||
// TODO this is a LOSSY logger!!
|
||||
let _ = self.0.try_send(String::from_utf8_lossy(buf).to_string()); // ignore: logger disconnected or with full buffer
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
|
||||
}
|
104
src/ffi/js/op_cache.rs
Normal file
104
src/ffi/js/op_cache.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
use std::collections::HashMap;
|
||||
use napi_derive::napi;
|
||||
|
||||
pub type OpTuple = (String, u32, String, u32); // buf_path, start, text, end
|
||||
|
||||
#[napi]
|
||||
pub struct OpCache {
|
||||
store: HashMap<OpTuple, i32>
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl OpCache {
|
||||
#[napi(constructor)]
|
||||
pub fn new() -> Self {
|
||||
OpCache {
|
||||
store: HashMap::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn to_string(&self) -> String {
|
||||
self.store.iter()
|
||||
.map(|(k, v)| format!("{}x Op(@{} {}:{} '{}')", k.0, v, k.1, k.3, k.2))
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn put(&mut self, buf: String, start: u32, text: String, end: u32) -> i32 {
|
||||
let op = (buf, start, text, end);
|
||||
match self.store.get_mut(&op) {
|
||||
Some(val) => {
|
||||
if *val < 0 { *val = 0 }
|
||||
*val += 1;
|
||||
*val
|
||||
},
|
||||
None => {
|
||||
self.store.insert(op, 1);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get(&mut self, buf: String, start: u32, text: String, end: u32) -> bool {
|
||||
let op = (buf, start, text, end);
|
||||
match self.store.get_mut(&op) {
|
||||
Some(val) => {
|
||||
*val -= 1;
|
||||
*val >= 0
|
||||
}
|
||||
None => {
|
||||
tracing::warn!("never seen this op: {:?}", op);
|
||||
self.store.insert(op, -1);
|
||||
false
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
//a
|
||||
//consume a
|
||||
//a
|
||||
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
fn opcache_put_increments_internal_counter() {
|
||||
let mut op = super::OpCache::new();
|
||||
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 1); // 1: did not already contain it
|
||||
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 2); // 2: already contained it
|
||||
}
|
||||
#[test]
|
||||
fn op_cache_get_checks_count() {
|
||||
let mut op = super::OpCache::new();
|
||||
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
|
||||
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 1);
|
||||
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), true);
|
||||
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
|
||||
}
|
||||
#[test]
|
||||
fn op_cache_get_works_for_multiple_puts() {
|
||||
let mut op = super::OpCache::new();
|
||||
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
|
||||
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 1);
|
||||
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 2);
|
||||
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), true);
|
||||
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), true);
|
||||
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn op_cache_different_keys(){
|
||||
let mut op = super::OpCache::new();
|
||||
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), false);
|
||||
assert_eq!(op.put("default".into(), 0, "hello world".into(), 0), 1);
|
||||
assert_eq!(op.get("workspace".into(), 0, "hi".into(), 0), false);
|
||||
assert_eq!(op.put("workspace".into(), 0, "hi".into(), 0), 1);
|
||||
assert_eq!(op.get("workspace".into(), 0, "hi".into(), 0), true);
|
||||
assert_eq!(op.get("default".into(), 0, "hello world".into(), 0), true);
|
||||
}
|
||||
}
|
56
src/ffi/js/workspace.rs
Normal file
56
src/ffi/js/workspace.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use napi_derive::napi;
|
||||
|
||||
use crate::ffi::js::{JsCodempError, buffer::JsBufferController, cursor::JsCursorController};
|
||||
|
||||
|
||||
#[napi]
|
||||
/// a reference to a codemp workspace
|
||||
pub struct JsWorkspace(Arc<crate::Workspace>);
|
||||
|
||||
impl From<Arc<crate::Workspace>> for JsWorkspace {
|
||||
fn from(value: Arc<crate::Workspace>) -> Self {
|
||||
JsWorkspace(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
impl JsWorkspace {
|
||||
|
||||
#[napi]
|
||||
pub fn id(&self) -> String {
|
||||
self.0.id()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn filetree(&self) -> Vec<String> {
|
||||
self.0.filetree()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn cursor(&self) -> JsCursorController {
|
||||
JsCursorController::from(self.0.cursor())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn buffer_by_name(&self, path: String) -> Option<JsBufferController> {
|
||||
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)
|
||||
}*/
|
||||
|
||||
}
|
|
@ -3,3 +3,6 @@ pub mod java;
|
|||
|
||||
#[cfg(feature = "lua")]
|
||||
pub mod lua;
|
||||
|
||||
#[cfg(feature = "js")]
|
||||
pub mod js;
|
Loading…
Reference in a new issue