diff --git a/Cargo.toml b/Cargo.toml index 88d5b81..f7d252b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,13 @@ [package] name = "codemp-nvim" -version = "0.2.0" +version = "0.3.0" edition = "2021" +[lib] +crate-type = ["cdylib"] + [dependencies] -codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.3" } -tracing = "0.1" -tracing-subscriber = "0.3" -uuid = { version = "1.3.1", features = ["v4"] } -serde = "1" -serde_json = "1" -rmpv = "1" -clap = { version = "4.2.1", features = ["derive"] } -nvim-rs = { version = "0.5", features = ["use_tokio"] } -async-trait = "0.1.68" +codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.4.2", features = ["global", "sync"] } +mlua = { version = "0.9.0", features = ["send", "module", "luajit"] } +thiserror = "1.0.47" +derive_more = "0.99.17" diff --git a/codemp.lua b/codemp.lua deleted file mode 100644 index 099c98a..0000000 --- a/codemp.lua +++ /dev/null @@ -1,190 +0,0 @@ -local BINARY = vim.g.codemp_binary or "./codemp-client-nvim" - -local M = {} - -M.jobid = nil -M.create = function(path, content) return vim.rpcrequest(M.jobid, "create", path, content) end -M.insert = function(path, txt, pos) return vim.rpcrequest(M.jobid, "insert", path, txt, pos) end -M.delete = function(path, pos, count) return vim.rpcrequest(M.jobid, "delete", path, pos, count) end -M.replace = function(path, txt) return vim.rpcrequest(M.jobid, "replace", path, txt) end -M.cursor = function(path, cur) return vim.rpcrequest(M.jobid, "cursor", path, cur[1][1], cur[1][2], cur[2][1], cur[2][2]) end -M.attach = function(path) return vim.rpcrequest(M.jobid, "attach", path) end -M.listen = function(path) return vim.rpcrequest(M.jobid, "listen", path) end -M.detach = function(path) return vim.rpcrequest(M.jobid, "detach", path) end - -local function cursor_offset() - local cursor = vim.api.nvim_win_get_cursor(0) - return vim.fn.line2byte(cursor[1]) + cursor[2] - 1 -end - -local codemp_autocmds = vim.api.nvim_create_augroup("CodempAuGroup", { clear = true }) - -local function get_cursor_range() - local mode = vim.fn.mode() - if mode == "" or mode == "s" or mode == "Vs" or mode == "V" or mode == "vs" or mode == "v" then - local start = vim.fn.getpos("'<") - local finish = vim.fn.getpos("'>") - return { - { start[2], start[3] }, - { finish[2], finish[3] } - } - else - local cursor = vim.api.nvim_win_get_cursor(0) - return { - { cursor[1], cursor[2] }, - { cursor[1], cursor[2] + 1 }, - } - end -end - -local function hook_callbacks(path, buffer) - vim.api.nvim_create_autocmd( - { "InsertCharPre" }, - { - callback = function(_) - pcall(M.insert, path, vim.v.char, cursor_offset()) -- TODO log errors - end, - buffer = buffer, - group = codemp_autocmds, - } - ) - vim.api.nvim_create_autocmd( - { "CursorMoved", "CompleteDone", "InsertEnter", "InsertLeave" }, - { - callback = function(args) - local lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, false) - pcall(M.replace, path, vim.fn.join(lines, "\n")) -- TODO log errors - pcall(M.cursor, path, get_cursor_range()) -- TODO log errors - end, - buffer = buffer, - group = codemp_autocmds, - } - ) - local last_line = 0 - vim.api.nvim_create_autocmd( - { "CursorMovedI" }, - { - callback = function(args) - local cursor = get_cursor_range() - pcall(M.cursor, path, cursor) -- TODO log errors - if cursor[1][1] == last_line then - return - end - last_line = cursor[1][1] - local lines = vim.api.nvim_buf_get_lines(args.buf, 0, -1, false) - pcall(M.replace, path, vim.fn.join(lines, "\n")) -- TODO log errors - end, - buffer = buffer, - group = codemp_autocmds, - } - ) - vim.keymap.set('i', '', function() pcall(M.delete, path, cursor_offset(), 1) return '' end, {expr = true, buffer = buffer}) -- TODO log errors - vim.keymap.set('i', '', function() pcall(M.delete, path, cursor_offset() + 1, 1) return '' end, {expr = true, buffer = buffer}) -- TODO log errors -end - -local function unhook_callbacks(buffer) - vim.api.nvim_clear_autocmds({ group = codemp_autocmds, buffer = buffer }) - vim.keymap.del('i', '', { buffer = buffer }) - vim.keymap.del('i', '', { buffer = buffer }) -end - -local function auto_address(addr) - if not string.find(addr, "://") then - addr = string.format("http://%s", addr) - end - if not string.find(addr, ":", 7) then -- skip first 7 chars because 'https://' - addr = string.format("%s:50051", addr) - end - return addr -end - -vim.api.nvim_create_user_command('Connect', - function(args) - if M.jobid ~= nil and M.jobid > 0 then - print("already connected, disconnect first") - return - end - local bin_args = { BINARY } - if #args.fargs > 0 then - table.insert(bin_args, "--host") - table.insert(bin_args, auto_address(args.fargs[1])) - end - if vim.g.codemp_remote_debug then - table.insert(bin_args, "--remote-debug") - table.insert(bin_args, vim.g.codemp_remote_debug) - end - if args.bang then - table.insert(bin_args, "--debug") - end - M.jobid = vim.fn.jobstart( - bin_args, - { - rpc = true, - on_stderr = function(_, data, _) - for _, line in pairs(data) do - print(line) - end - -- print(vim.fn.join(data, "\n")) - end, - stderr_buffered = false, - env = { RUST_BACKTRACE = 1 } - } - ) - if M.jobid <= 0 then - print("[!] could not start codemp client") - end - end, -{ nargs='?', bang=true }) - -vim.api.nvim_create_user_command('Stop', - function(_) - vim.fn.jobstop(M.jobid) - M.jobid = nil - end, -{ bang=true }) - -vim.api.nvim_create_user_command('Share', - function(args) - if M.jobid == nil or M.jobid <= 0 then - print("[!] connect to codemp server first") - return - end - local path = args.fargs[1] - local bufnr = vim.api.nvim_get_current_buf() - local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) - vim.opt.fileformat = "unix" - M.create(path, vim.fn.join(lines, "\n")) - hook_callbacks(path, bufnr) - M.attach(path) - M.listen(path) - end, -{ nargs=1 }) - -vim.api.nvim_create_user_command('Join', - function(args) - if M.jobid == nil or M.jobid <= 0 then - print("[!] connect to codemp server first") - return - end - local path = args.fargs[1] - local bufnr = vim.api.nvim_get_current_buf() - vim.opt.fileformat = "unix" - hook_callbacks(path, bufnr) - M.attach(path) - M.listen(path) - end, -{ nargs=1 }) - -vim.api.nvim_create_user_command('Detach', - function(args) - local bufnr = vim.api.nvim_get_current_buf() - if M.detach(args.fargs[1]) then - unhook_callbacks(bufnr) - print("[/] detached from buffer") - else - print("[!] error detaching from buffer") - end - end, -{ nargs=1 }) - -return M diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..46139bd --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,186 @@ +use std::sync::Arc; + +use codemp::prelude::*; +use mlua::prelude::*; + + +#[derive(Debug, thiserror::Error, derive_more::From, derive_more::Display)] +struct LuaCodempError(CodempError); + +impl From:: for LuaError { + fn from(value: LuaCodempError) -> Self { + LuaError::external(value) + } +} + +fn cursor_to_table(lua: &Lua, cur: CodempCursorEvent) -> LuaResult { + let pos = cur.position.unwrap_or_default(); + let start = lua.create_table()?; + start.set(0, pos.start().row)?; + start.set(1, pos.start().col)?; + let end = lua.create_table()?; + end.set(0, pos.end().row)?; + end.set(1, pos.end().col)?; + let out = lua.create_table()?; + out.set("user", cur.user)?; + out.set("buffer", pos.buffer)?; + out.set("start", start)?; + out.set("finish", end)?; + Ok(out) +} + +fn make_cursor(buffer: String, start_row: i32, start_col: i32, end_row: i32, end_col: i32) -> CodempCursorPosition { + CodempCursorPosition { + buffer, + start: Some(CodempRowCol { + row: start_row, col: start_col, + }), + end: Some(CodempRowCol { + row: end_row, col: end_col, + }), + } +} + + + +/// connect to remote server +fn connect(_: &Lua, (host,): (Option,)) -> LuaResult<()> { + let addr = host.unwrap_or("http://127.0.0.1:50051".into()); + CODEMP_INSTANCE.connect(&addr) + .map_err(LuaCodempError::from)?; + Ok(()) +} + + + +/// join a remote workspace and start processing cursor events +fn join(_: &Lua, (session,): (String,)) -> LuaResult { + let controller = CODEMP_INSTANCE.join(&session) + .map_err(LuaCodempError::from)?; + Ok(LuaCursorController(controller)) +} + +#[derive(derive_more::From)] +struct LuaCursorController(Arc); +impl LuaUserData for LuaCursorController { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method_mut("send", |_, this, (usr, sr, sc, er, ec):(String, i32, i32, i32, i32)| { + Ok(this.0.send(make_cursor(usr, sr, sc, er, ec)).map_err(LuaCodempError::from)?) + }); + methods.add_method_mut("recv", |lua, this, ()| { + let event = this.0.blocking_recv(CODEMP_INSTANCE.rt()) + .map_err(LuaCodempError::from)?; + cursor_to_table(lua, event) + }); + } +} + +// #[derive(derive_more::From)] +// struct LuaCursorEvent(CodempCursorEvent); +// impl LuaUserData for LuaCursorEvent { +// fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { +// fields.add_field_method_get("user", |_, this| Ok(this.0.user)); +// fields.add_field_method_set("user", |_, this, val| Ok(this.0.user = val)); +// +// fields.add_field_method_get("user", |_, this| Ok(this.0.user)); +// fields.add_field_method_set("user", |_, this, val| Ok(this.0.user = val)); +// } +// } +// +// #[derive(derive_more::From)] +// struct LuaCursorPosition(CodempCursorPosition); +// impl LuaUserData for LuaCursorPosition { +// fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { +// fields.add_field_method_get("buffer", |_, this| Ok(this.0.buffer)); +// fields.add_field_method_set("buffer", |_, this, val| Ok(this.0.buffer = val)); +// +// fields.add_field_method_get("start", |_, this| Ok(this.0.start.into())); +// fields.add_field_method_set("start", |_, this, (val,):(LuaRowCol,)| Ok(this.0.start = Some(val.0))); +// +// fields.add_field_method_get("end", |_, this| Ok(this.0.end.unwrap_or_default())); +// fields.add_field_method_set("end", |_, this, val| Ok(this.0.end = Some(val))); +// } +// } +// +// #[derive(derive_more::From)] +// struct LuaRowCol(CodempRowCol); +// impl LuaUserData for LuaRowCol { +// fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { +// fields.add_field_method_get("row", |_, this| Ok(this.0.col)); +// fields.add_field_method_set("row", |_, this, val| Ok(this.0.col = val)); +// +// fields.add_field_method_get("col", |_, this| Ok(this.0.col)); +// fields.add_field_method_set("col", |_, this, val| Ok(this.0.col = val)); +// } +// } + + + +/// create a new buffer in current workspace +fn create(_: &Lua, (path, content): (String, Option)) -> LuaResult<()> { + CODEMP_INSTANCE.create(&path, content.as_deref()) + .map_err(LuaCodempError::from)?; + Ok(()) +} + + + +/// attach to remote buffer and start processing buffer events +fn attach(_: &Lua, (path,): (String,)) -> LuaResult { + let controller = CODEMP_INSTANCE.attach(&path) + .map_err(LuaCodempError::from)?; + Ok(LuaBufferController(controller)) +} + +#[derive(derive_more::From)] +struct LuaBufferController(Arc); +impl LuaUserData for LuaBufferController { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method_mut("delta", |_, this, (start, txt, end):(usize, String, usize)| { + match this.0.delta(start, &txt, end) { + Some(op) => Ok(this.0.send(op).map_err(LuaCodempError::from)?), + None => Ok(()), + } + }); + methods.add_method_mut("replace", |_, this, txt:String| { + match this.0.replace(&txt) { + Some(op) => Ok(this.0.send(op).map_err(LuaCodempError::from)?), + None => Ok(()), + } + }); + methods.add_method_mut("insert", |_, this, (txt, pos):(String, u64)| { + Ok(this.0.send(this.0.insert(&txt, pos)).map_err(LuaCodempError::from)?) + }); + methods.add_method_mut("recv", |_, this, ()| { + let change = this.0.blocking_recv(CODEMP_INSTANCE.rt()) + .map_err(LuaCodempError::from)?; + Ok(LuaTextChange(change)) + }); + } + + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("content", |_, this| Ok(this.0.content())); + } +} + +#[derive(derive_more::From)] +struct LuaTextChange(CodempTextChange); +impl LuaUserData for LuaTextChange { + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("content", |_, this| Ok(this.0.content.clone())); + fields.add_field_method_get("start", |_, this| Ok(this.0.span.start)); + fields.add_field_method_get("finish", |_, this| Ok(this.0.span.end)); + } +} + + + +#[mlua::lua_module] +fn libcodemp_nvim(lua: &Lua) -> LuaResult { + let exports = lua.create_table()?; + exports.set("connect", lua.create_function(connect)?)?; + exports.set("join", lua.create_function(join)?)?; + exports.set("create", lua.create_function(create)?)?; + exports.set("attach", lua.create_function(attach)?)?; + Ok(exports) +} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index a585225..0000000 --- a/src/main.rs +++ /dev/null @@ -1,318 +0,0 @@ -use std::sync::Arc; -use std::{net::TcpStream, sync::Mutex, collections::BTreeMap}; - -use codemp::client::CodempClient; -use codemp::controller::buffer::{OperationControllerHandle, OperationControllerSubscriber}; -use codemp::controller::cursor::{CursorControllerHandle, CursorSubscriber}; -use codemp::factory::OperationFactory; -use codemp::proto::buffer_client::BufferClient; -use codemp::tokio; - -use rmpv::Value; -use clap::Parser; - -use nvim_rs::{compat::tokio::Compat, create::tokio as create, Handler, Neovim}; -use tracing::{error, warn, debug, info}; - -#[derive(Clone)] -struct NeovimHandler { - client: CodempClient, - factories: Arc>>, - cursors: Arc>>, -} - -fn nullable_optional_str(args: &[Value], index: usize) -> Option { - Some(args.get(index)?.as_str()?.to_string()) -} - -fn default_empty_str(args: &[Value], index: usize) -> String { - nullable_optional_str(args, index).unwrap_or("".into()) -} - -fn nullable_optional_number(args: &[Value], index: usize) -> Option { - args.get(index)?.as_i64() -} - -fn default_zero_number(args: &[Value], index: usize) -> i64 { - nullable_optional_number(args, index).unwrap_or(0) -} - -impl NeovimHandler { - fn buffer_controller(&self, path: &String) -> Option { - Some(self.factories.lock().unwrap().get(path)?.clone()) - } - - fn cursor_controller(&self, path: &String) -> Option { - Some(self.cursors.lock().unwrap().get(path)?.clone()) - } -} - -#[async_trait::async_trait] -impl Handler for NeovimHandler { - type Writer = Compat; - - async fn handle_request( - &self, - name: String, - args: Vec, - nvim: Neovim>, - ) -> Result { - debug!("processing '{}' - {:?}", name, args); - match name.as_ref() { - "ping" => Ok(Value::from("pong")), - - "create" => { - if args.is_empty() { - return Err(Value::from("no path given")); - } - let path = default_empty_str(&args, 0); - let content = nullable_optional_str(&args, 1); - let mut c = self.client.clone(); - match c.create(path, content).await { - Ok(r) => match r { - true => Ok(Value::Nil), - false => Err(Value::from("rejected")), - }, - Err(e) => Err(Value::from(format!("could not create buffer: {}", e))), - } - }, - - "insert" => { - if args.len() < 3 { - return Err(Value::from("not enough arguments")); - } - let path = default_empty_str(&args, 0); - let txt = default_empty_str(&args, 1); - let mut pos = default_zero_number(&args, 2); - - if pos <= 0 { pos = 0 } // TODO wtf vim?? - - match self.buffer_controller(&path) { - None => Err(Value::from("no controller for given path")), - Some(controller) => { - controller.apply(controller.insert(&txt, pos as u64)).await; - Ok(Value::Nil) - }, - } - }, - - "delete" => { - if args.len() < 3 { - return Err(Value::from("not enough arguments")); - } - let path = default_empty_str(&args, 0); - let pos = default_zero_number(&args, 1) as u64; - let count = default_zero_number(&args, 2) as u64; - - match self.buffer_controller(&path) { - None => Err(Value::from("no controller for given path")), - Some(controller) => { - controller.apply(controller.delete(pos, count)).await; - Ok(Value::Nil) - } - } - }, - - "replace" => { - if args.len() < 2 { - return Err(Value::from("not enough arguments")); - } - let path = default_empty_str(&args, 0); - let txt = default_empty_str(&args, 1); - - match self.buffer_controller(&path) { - None => Err(Value::from("no controller for given path")), - Some(controller) => { - if let Some(op) = controller.replace(&txt) { - controller.apply(op).await; - } - Ok(Value::Nil) - } - } - }, - - "attach" => { - if args.is_empty() { - return Err(Value::from("no path given")); - } - let path = default_empty_str(&args, 0); - let buffer = match nvim.get_current_buf().await { - Ok(b) => b, - Err(e) => return Err(Value::from(format!("could not get current buffer: {}", e))), - }; - - let mut c = self.client.clone(); - - match c.attach(path.clone()).await { - Err(e) => Err(Value::from(format!("could not attach to stream: {}", e))), - Ok(controller) => { - let mut _controller = controller.clone(); - let lines : Vec = _controller.content().split('\n').map(|x| x.to_string()).collect(); - match buffer.set_lines(0, -1, false, lines).await { - Err(e) => Err(Value::from(format!("could not sync buffer: {}", e))), - Ok(()) => { - tokio::spawn(async move { - while let Some(_change) = _controller.poll().await { - let lines : Vec = _controller.content().split('\n').map(|x| x.to_string()).collect(); - // TODO only change lines affected! - if let Err(e) = buffer.set_lines(0, -1, false, lines).await { - error!("could not update buffer: {}", e); - } - } - }); - self.factories.lock().unwrap().insert(path, controller); - Ok(Value::Nil) - } - } - }, - } - }, - - "detach" => { - Err(Value::String("not implemented".into())) - // if args.is_empty() { - // return Err(Value::from("no path given")); - // } - // let path = default_empty_str(&args, 0); - // match self.buffer_controller(&path) { - // None => Err(Value::from("no controller for given path")), - // Some(controller) => Ok(Value::from(controller.stop())), - // } - }, - - "listen" => { - if args.is_empty() { - return Err(Value::from("no path given")); - } - let path = default_empty_str(&args, 0); - - let ns = nvim.create_namespace("Cursor").await - .map_err(|e| Value::from(format!("could not create namespace: {}", e)))?; - - let buf = nvim.get_current_buf().await - .map_err(|e| Value::from(format!("could not get current buf: {}", e)))?; - - let mut c = self.client.clone(); - match c.listen().await { - Err(e) => Err(Value::from(format!("could not listen cursors: {}", e))), - Ok(mut cursor) => { - self.cursors.lock().unwrap().insert(path, cursor.clone()); - debug!("spawning cursor processing worker"); - tokio::spawn(async move { - while let Some(cur) = cursor.poll().await { - if let Err(e) = buf.clear_namespace(ns, 0, -1).await { - error!("could not clear previous cursor highlight: {}", e); - } - let start = cur.start(); - let end = cur.end(); - let end_col = if start.row == end.row { - end.col - } else { - 0 // TODO what the fuck - }; - if let Err(e) = buf.add_highlight( - ns, "ErrorMsg", - start.row as i64 - 1, - start.col as i64, - end_col as i64 - ).await { - error!("could not create highlight for cursor: {}", e); - } - } - if let Err(e) = buf.clear_namespace(ns, 0, -1).await { - error!("could not clear previous cursor highlight: {}", e); - } - }); - Ok(Value::Nil) - }, - } - }, - - "cursor" => { - if args.len() < 3 { - return Err(Value::from("not enough args")); - } - let path = default_empty_str(&args, 0); - let row = default_zero_number(&args, 1) as i32; - let col = default_zero_number(&args, 2) as i32; - let row_end = default_zero_number(&args, 3) as i32; - let col_end = default_zero_number(&args, 4) as i32; - - match self.cursor_controller(&path) { - None => Err(Value::from("no path given")), - Some(cur) => { - cur.send(&path, (row, col).into(), (row_end, col_end).into()).await; - Ok(Value::Nil) - } - } - }, - - _ => Err(Value::from("unimplemented")), - } - } - - async fn handle_notify( - &self, - _name: String, - _args: Vec, - _nvim: Neovim>, - ) { - warn!("notify not handled"); - } -} - -#[derive(Parser, Debug)] -struct CliArgs { - /// server host to connect to - #[arg(long, default_value = "http://[::1]:50051")] - host: String, - - /// show debug level logs - #[arg(long, default_value_t = false)] - debug: bool, - - /// dump raw tracing logs into this TCP host - #[arg(long)] - remote_debug: Option, -} - - -#[tokio::main] -async fn main() -> Result<(), Box> { - let args = CliArgs::parse(); - - match args.remote_debug { - Some(host) => - tracing_subscriber::fmt() - .with_writer(Mutex::new(TcpStream::connect(host)?)) - .with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO }) - .init(), - - None => - tracing_subscriber::fmt() - .compact() - .without_time() - .with_ansi(false) - .with_writer(std::io::stderr) - .with_max_level(if args.debug { tracing::Level::DEBUG } else { tracing::Level::INFO }) - .init(), - } - - let client = BufferClient::connect(args.host.clone()).await?; - - let handler: NeovimHandler = NeovimHandler { - client: client.into(), - factories: Arc::new(Mutex::new(BTreeMap::new())), - cursors: Arc::new(Mutex::new(BTreeMap::new())), - }; - - let (_nvim, io_handler) = create::new_parent(handler).await; - - info!("++ codemp connected: {}", args.host); - - if let Err(e) = io_handler.await? { - error!("worker stopped with error: {}", e); - } - - Ok(()) -}