From 480becf5884d44dcebc5de157ff64eadf1a02c13 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 21 Aug 2023 04:06:10 +0200 Subject: [PATCH 01/23] feat: initial work on mlua bindings --- Cargo.toml | 19 ++-- codemp.lua | 190 ------------------------------- src/lib.rs | 186 ++++++++++++++++++++++++++++++ src/main.rs | 318 ---------------------------------------------------- 4 files changed, 194 insertions(+), 519 deletions(-) delete mode 100644 codemp.lua create mode 100644 src/lib.rs delete mode 100644 src/main.rs 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(()) -} From 23b4adafcb9337cd894dc89256240a6622ab55bd Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 21 Aug 2023 04:07:25 +0200 Subject: [PATCH 02/23] feat: lua impl for cursors, tweaks to bindings --- Cargo.toml | 4 +- src/codemp.lua | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 51 ++++++++++++++++------- 3 files changed, 148 insertions(+), 18 deletions(-) create mode 100644 src/codemp.lua diff --git a/Cargo.toml b/Cargo.toml index f7d252b..1d9f9ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -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"] } +codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.4.4", features = ["global", "sync"] } +mlua = { version = "0.9.0", features = ["module", "luajit"] } thiserror = "1.0.47" derive_more = "0.99.17" diff --git a/src/codemp.lua b/src/codemp.lua new file mode 100644 index 0000000..65018ce --- /dev/null +++ b/src/codemp.lua @@ -0,0 +1,111 @@ +local codemp = require("libcodemp_nvim") + +local function register_async_waker(target, cb) + local async = vim.loop.new_async(cb) + vim.loop.new_thread(function(_async, _target) + local _codemp = require("libcodemp_nvim") + local _cntrl + if _target ~= nil then + _cntrl = _codemp.get_buffer(_target) + else + _cntrl = _codemp.get_cursor() + end + while true do + _cntrl:poll() + _async:send() + end + end, async, target) +end + +local function order_tuples(x) -- TODO send help... + if x[1][1] < x[2][1] then + return { { x[1][1], x[1][2] }, { x[2][1], x[2][2] } } + elseif x[1][1] > x[2][1] then + return { { x[2][1], x[2][2] }, { x[1][1], x[1][2] } } + elseif x[1][2] < x[2][2] then + return { { x[1][1], x[1][2] }, { x[2][1], x[2][2] } } + else + return { { x[2][1], x[2][2] }, { x[1][1], x[1][2] } } + end +end + +local function cursor_position() + local mode = vim.api.nvim_get_mode().mode + if mode == "v" or mode == "V" then + local _, ls, cs = unpack(vim.fn.getpos('v')) + local _, le, ce = unpack(vim.fn.getpos('.')) + return order_tuples({ { ls-1, cs-1 }, { le-1, ce } }) + else + local win = vim.api.nvim_get_current_win() + local cur = vim.api.nvim_win_get_cursor(win) + return order_tuples({ { cur[1]-1, cur[2] }, { cur[1]-1, cur[2]+1 } }) + end +end + +local function multiline_highlight(buf, ns, group, start, fini) + for i=start[1],fini[1] do + if i == start[1] and i == fini[1] then + vim.api.nvim_buf_add_highlight(buf, ns, group, i, start[2], fini[2]) + elseif i == start[1] then + vim.api.nvim_buf_add_highlight(buf, ns, group, i, start[2], -1) + elseif i == fini[1] then + vim.api.nvim_buf_add_highlight(buf, ns, group, i, 0, fini[2]) + else + vim.api.nvim_buf_add_highlight(buf, ns, group, i, 0, -1) + end + end +end + +vim.api.nvim_create_user_command( + "Connect", + function (args) + codemp.connect(#args.args > 0 and args.args or nil) + print(" ++ connected") + end, + { nargs = "?" } +) + +vim.api.nvim_create_user_command( + "Join", + function (args) + local controller = codemp.join(args.args) + + -- hook serverbound callbacks + local group = vim.api.nvim_create_augroup("codemp-workspace", { clear = true }) + vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, { + group = group, + callback = function (_) + local cur = cursor_position() + controller:send("", cur[1][1], cur[1][2], cur[2][1], cur[2][2]) + end + }) + + -- hook clientbound callbacks + local ns = vim.api.nvim_create_namespace("codemp-cursors") + local buffer = vim.api.nvim_get_current_buf() + register_async_waker(nil, function() + while true do + local event = controller:recv() + if event == nil then break end + vim.schedule(function() + vim.api.nvim_buf_clear_namespace(buffer, ns, 0, -1) + multiline_highlight(buffer, ns, "ErrorMsg", event.start, event.finish) + end) + end + end) + + print(" ++ joined workspace " .. args.args) + end, + { nargs = 1 } +) + +vim.api.nvim_create_user_command( + "Attach", + function (args) + codemp.connect(#args.args > 0 and args.args or nil) + print(" ++ connected") + end, + { nargs = 1 } +) + +return codemp diff --git a/src/lib.rs b/src/lib.rs index 46139bd..6b0e8ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,11 +16,11 @@ impl From:: for LuaError { 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)?; + start.set(1, pos.start().row)?; + start.set(2, pos.start().col)?; let end = lua.create_table()?; - end.set(0, pos.end().row)?; - end.set(1, pos.end().col)?; + end.set(1, pos.end().row)?; + end.set(2, pos.end().col)?; let out = lua.create_table()?; out.set("user", cur.user)?; out.set("buffer", pos.buffer)?; @@ -51,7 +51,13 @@ fn connect(_: &Lua, (host,): (Option,)) -> LuaResult<()> { Ok(()) } - +fn get_cursor(_: &Lua, _args: ()) -> LuaResult { + Ok( + CODEMP_INSTANCE.get_cursor() + .map_err(LuaCodempError::from)? + .into() + ) +} /// join a remote workspace and start processing cursor events fn join(_: &Lua, (session,): (String,)) -> LuaResult { @@ -60,17 +66,24 @@ fn join(_: &Lua, (session,): (String,)) -> LuaResult { Ok(LuaCursorController(controller)) } -#[derive(derive_more::From)] +#[derive(Debug, 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)| { + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + methods.add_method("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()) + methods.add_method("recv", |lua, this, ()| { + match this.0.try_recv() .map_err(LuaCodempError::from)? { + Some(x) => Ok(Some(cursor_to_table(lua, x)?)), + None => Ok(None), + } + }); + methods.add_method("poll", |_, this, ()| { + CODEMP_INSTANCE.rt().block_on(this.0.poll()) .map_err(LuaCodempError::from)?; - cursor_to_table(lua, event) + Ok(()) }); } } @@ -132,26 +145,27 @@ fn attach(_: &Lua, (path,): (String,)) -> LuaResult { Ok(LuaBufferController(controller)) } -#[derive(derive_more::From)] +#[derive(Debug, 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)| { + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + methods.add_method("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| { + methods.add_method("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)| { + methods.add_method("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, ()| { + methods.add_method("recv", |_, this, ()| { let change = this.0.blocking_recv(CODEMP_INSTANCE.rt()) .map_err(LuaCodempError::from)?; Ok(LuaTextChange(change)) @@ -163,7 +177,7 @@ impl LuaUserData for LuaBufferController { } } -#[derive(derive_more::From)] +#[derive(Debug, derive_more::From)] struct LuaTextChange(CodempTextChange); impl LuaUserData for LuaTextChange { fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { @@ -171,6 +185,10 @@ impl LuaUserData for LuaTextChange { fields.add_field_method_get("start", |_, this| Ok(this.0.span.start)); fields.add_field_method_get("finish", |_, this| Ok(this.0.span.end)); } + + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + } } @@ -182,5 +200,6 @@ fn libcodemp_nvim(lua: &Lua) -> LuaResult { exports.set("join", lua.create_function(join)?)?; exports.set("create", lua.create_function(create)?)?; exports.set("attach", lua.create_function(attach)?)?; + exports.set("get_cursor", lua.create_function(get_cursor)?)?; Ok(exports) } From d6b9e218366d3a80b3cd341359c882ef477557ba Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 22 Aug 2023 12:51:17 +0200 Subject: [PATCH 03/23] feat: added get_buffer, renamed recv -> try_recv --- src/lib.rs | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6b0e8ae..148c3c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,14 @@ fn get_cursor(_: &Lua, _args: ()) -> LuaResult { ) } +fn get_buffer(_: &Lua, (path,): (String,)) -> LuaResult { + Ok( + CODEMP_INSTANCE.get_buffer(&path) + .map_err(LuaCodempError::from)? + .into() + ) +} + /// join a remote workspace and start processing cursor events fn join(_: &Lua, (session,): (String,)) -> LuaResult { let controller = CODEMP_INSTANCE.join(&session) @@ -74,7 +82,7 @@ impl LuaUserData for LuaCursorController { methods.add_method("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("recv", |lua, this, ()| { + methods.add_method("try_recv", |lua, this, ()| { match this.0.try_recv() .map_err(LuaCodempError::from)? { Some(x) => Ok(Some(cursor_to_table(lua, x)?)), None => Ok(None), @@ -165,10 +173,16 @@ impl LuaUserData for LuaBufferController { methods.add_method("insert", |_, this, (txt, pos):(String, u64)| { Ok(this.0.send(this.0.insert(&txt, pos)).map_err(LuaCodempError::from)?) }); - methods.add_method("recv", |_, this, ()| { - let change = this.0.blocking_recv(CODEMP_INSTANCE.rt()) - .map_err(LuaCodempError::from)?; - Ok(LuaTextChange(change)) + methods.add_method("try_recv", |_, this, ()| { + match this.0.try_recv() .map_err(LuaCodempError::from)? { + Some(x) => Ok(Some(LuaTextChange(x))), + None => Ok(None), + } + }); + methods.add_method("poll", |_, this, ()| { + CODEMP_INSTANCE.rt().block_on(this.0.poll()) + .map_err(LuaCodempError::from)?; + Ok(()) }); } @@ -201,5 +215,6 @@ fn libcodemp_nvim(lua: &Lua) -> LuaResult { exports.set("create", lua.create_function(create)?)?; exports.set("attach", lua.create_function(attach)?)?; exports.set("get_cursor", lua.create_function(get_cursor)?)?; + exports.set("get_buffer", lua.create_function(get_buffer)?)?; Ok(exports) } From 773d76e7f83e3613f47b96ca7ccdc51a43fce948 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 22 Aug 2023 12:51:35 +0200 Subject: [PATCH 04/23] feat: implemented crude buffer joining and synching --- src/codemp.lua | 69 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index 65018ce..f2a0cc8 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -42,6 +42,19 @@ local function cursor_position() end end +local function buffer_get_content(buf) + if buf == nil then + buf = vim.api.nvim_get_current_buf() + end + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + return table.concat(lines, '\n') +end + +local function buffer_set_content(buf, content) + local lines = vim.fn.split(content, "\n") + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) +end + local function multiline_highlight(buf, ns, group, start, fini) for i=start[1],fini[1] do if i == start[1] and i == fini[1] then @@ -69,11 +82,12 @@ vim.api.nvim_create_user_command( "Join", function (args) local controller = codemp.join(args.args) + local buffer = vim.api.nvim_get_current_buf() + local ns = vim.api.nvim_create_namespace("codemp-cursors") -- hook serverbound callbacks - local group = vim.api.nvim_create_augroup("codemp-workspace", { clear = true }) vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, { - group = group, + group = vim.api.nvim_create_augroup("codemp-workspace-" .. args.args, { clear = true }), callback = function (_) local cur = cursor_position() controller:send("", cur[1][1], cur[1][2], cur[2][1], cur[2][2]) @@ -81,11 +95,9 @@ vim.api.nvim_create_user_command( }) -- hook clientbound callbacks - local ns = vim.api.nvim_create_namespace("codemp-cursors") - local buffer = vim.api.nvim_get_current_buf() register_async_waker(nil, function() while true do - local event = controller:recv() + local event = controller:try_recv() if event == nil then break end vim.schedule(function() vim.api.nvim_buf_clear_namespace(buffer, ns, 0, -1) @@ -99,13 +111,54 @@ vim.api.nvim_create_user_command( { nargs = 1 } ) +vim.api.nvim_create_user_command( + "Create", + function (args) + local content = nil + if args.bang then + local buf = vim.api.nvim_get_current_buf() + content = buffer_get_content(buf) + end + codemp.create(args.args, content) + + print(" ++ created buffer " .. args.args) + end, + { nargs = 1, bang = true } +) + vim.api.nvim_create_user_command( "Attach", function (args) - codemp.connect(#args.args > 0 and args.args or nil) - print(" ++ connected") + local controller = codemp.attach(args.args) + local buffer = vim.api.nvim_get_current_buf() + + buffer_set_content(buffer, controller.content) + + -- hook serverbound callbacks + vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, { + group = vim.api.nvim_create_augroup("codemp-buffer-" .. args.args, { clear = true }), + buffer = buffer, + callback = function (_) + controller:replace(buffer_get_content(buffer)) + end + }) + + -- hook clientbound callbacks + register_async_waker(args.args, function() + vim.schedule(function() + buffer_set_content(buffer, controller.content) + end) + end) + + print(" ++ joined workspace " .. args.args) end, { nargs = 1 } ) -return codemp +return { + lib = codemp, + utils = { + buffer = buffer_get_content, + cursor = cursor_position, + } +} From ec873ee8b406daf6fa2e21982674b910bfd7c219 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 4 Sep 2023 03:22:57 +0200 Subject: [PATCH 05/23] feat: more capillar but broken text changes requires more help from codemp lib --- src/codemp.lua | 117 +++++++++++++++++++++++++++++++++++-------------- src/lib.rs | 76 +++++++++++++++----------------- 2 files changed, 121 insertions(+), 72 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index f2a0cc8..d273bb1 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -1,22 +1,52 @@ local codemp = require("libcodemp_nvim") -local function register_async_waker(target, cb) - local async = vim.loop.new_async(cb) - vim.loop.new_thread(function(_async, _target) - local _codemp = require("libcodemp_nvim") - local _cntrl - if _target ~= nil then - _cntrl = _codemp.get_buffer(_target) - else - _cntrl = _codemp.get_cursor() - end +local codemp_changed_tick = nil -- TODO this doesn't work when events are coalesced + +local function register_controller_handler(target, controller, handler) + local async = vim.loop.new_async(function() while true do - _cntrl:poll() + local event = controller:try_recv() + if event == nil then break end + vim.schedule(function() handler(event) end) + end + end) + -- TODO controller can't be passed to the uvloop new_thread: when sent to the new + -- Lua runtime it "loses" its methods defined with mlua, making the userdata object + -- completely useless. We can circumvent this by requiring codemp again in the new + -- thread and requesting a new reference to the same controller from che global instance + -- NOTE variables prefixed with underscore live in another Lua runtime + vim.loop.new_thread({}, function(_async, _target) + local _codemp = require("libcodemp_nvim") + local _controller = _target ~= nil and _codemp.get_buffer(_target) or _codemp.get_cursor() + while true do + _controller:poll() _async:send() end end, async, target) end +-- local function byte2rowcol(buf, x) +-- local row +-- local row_start +-- vim.api.nvim_buf_call(buf, function () +-- row = vim.fn.byte2line(x) +-- row_start = vim.fn.line2byte(row) +-- end) +-- local col = x - row_start +-- return { row, col } +-- end + +local function split_without_trim(str, sep) + local res = vim.fn.split(str, sep) + if str:sub(1,1) == "\n" then + table.insert(res, 1, '') + end + if str:sub(-1) == "\n" then + table.insert(res, '') + end + return res +end + local function order_tuples(x) -- TODO send help... if x[1][1] < x[2][1] then return { { x[1][1], x[1][2] }, { x[2][1], x[2][2] } } @@ -31,10 +61,20 @@ end local function cursor_position() local mode = vim.api.nvim_get_mode().mode - if mode == "v" or mode == "V" then + if mode == "v" then local _, ls, cs = unpack(vim.fn.getpos('v')) local _, le, ce = unpack(vim.fn.getpos('.')) return order_tuples({ { ls-1, cs-1 }, { le-1, ce } }) + elseif mode == "V" then + local _, ls, _ = unpack(vim.fn.getpos('v')) + local _, le, _ = unpack(vim.fn.getpos('.')) + if le > ls then + local ce = vim.fn.strlen(vim.fn.getline(le)) + return { { ls-1, 0 }, { le-1, ce } } + else + local ce = vim.fn.strlen(vim.fn.getline(ls)) + return { { le-1, 0 }, { ls-1, ce } } + end else local win = vim.api.nvim_get_current_win() local cur = vim.api.nvim_win_get_cursor(win) @@ -51,7 +91,7 @@ local function buffer_get_content(buf) end local function buffer_set_content(buf, content) - local lines = vim.fn.split(content, "\n") + local lines = split_without_trim(content, "\n") vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) end @@ -86,7 +126,7 @@ vim.api.nvim_create_user_command( local ns = vim.api.nvim_create_namespace("codemp-cursors") -- hook serverbound callbacks - vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, { + vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI", "ModeChanged"}, { group = vim.api.nvim_create_augroup("codemp-workspace-" .. args.args, { clear = true }), callback = function (_) local cur = cursor_position() @@ -95,15 +135,9 @@ vim.api.nvim_create_user_command( }) -- hook clientbound callbacks - register_async_waker(nil, function() - while true do - local event = controller:try_recv() - if event == nil then break end - vim.schedule(function() - vim.api.nvim_buf_clear_namespace(buffer, ns, 0, -1) - multiline_highlight(buffer, ns, "ErrorMsg", event.start, event.finish) - end) - end + register_controller_handler(nil, controller, function(event) + vim.api.nvim_buf_clear_namespace(buffer, ns, 0, -1) + multiline_highlight(buffer, ns, "ErrorMsg", event.start, event.finish) end) print(" ++ joined workspace " .. args.args) @@ -130,24 +164,42 @@ vim.api.nvim_create_user_command( "Attach", function (args) local controller = codemp.attach(args.args) + local buffer = vim.api.nvim_get_current_buf() buffer_set_content(buffer, controller.content) -- hook serverbound callbacks - vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI"}, { - group = vim.api.nvim_create_augroup("codemp-buffer-" .. args.args, { clear = true }), - buffer = buffer, - callback = function (_) - controller:replace(buffer_get_content(buffer)) + vim.api.nvim_buf_attach(buffer, false, { + on_lines = function (_, buf, tick, firstline, lastline, new_lastline, old_byte_size) + if tick == codemp_changed_tick then return end + print(string.format(">[%s] %s:%s|%s (%s)", tick, firstline, lastline, new_lastline, old_byte_size)) + local start_index = firstline == 0 and 0 or vim.fn.line2byte(firstline + 1) - 1 + local text = table.concat( + vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, true), + "\n" + ) + if lastline ~= new_lastline then + text = text .. "\n" + end + print(string.format(">delta [%d,%s,%d]", start_index, text, start_index + old_byte_size - 1)) + controller:delta(start_index, text, start_index + old_byte_size - 1) end }) -- hook clientbound callbacks - register_async_waker(args.args, function() - vim.schedule(function() - buffer_set_content(buffer, controller.content) - end) + register_controller_handler(args.args, controller, function(event) + codemp_changed_tick = vim.api.nvim_buf_get_changedtick(buffer) + 1 + local start = controller:byte2rowcol(event.start) + local finish = controller:byte2rowcol(event.finish) + print(string.format( + "buf_set_text(%s,%s, %s,%s, '%s')", + start.row, start.col, finish.row, finish.col, vim.inspect(split_without_trim(event.content, "\n")) + )) + vim.api.nvim_buf_set_text( + buffer, start.row, start.col, finish.row, finish.col, + split_without_trim(event.content, "\n") + ) end) print(" ++ joined workspace " .. args.args) @@ -160,5 +212,6 @@ return { utils = { buffer = buffer_get_content, cursor = cursor_position, + split = split_without_trim, } } diff --git a/src/lib.rs b/src/lib.rs index 148c3c3..c0ce2f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,16 @@ impl From:: for LuaError { } } +fn byte_to_rowcol(text: &str, index: usize) -> CodempRowCol { + let lines_before = text[..index].split('\n').count() - 1; + let chars_before = text[..index].split('\n').last().unwrap_or_default().len(); + + CodempRowCol { + row: lines_before as i32, + col: chars_before as i32, + } +} + fn cursor_to_table(lua: &Lua, cur: CodempCursorEvent) -> LuaResult { let pos = cur.position.unwrap_or_default(); let start = lua.create_table()?; @@ -96,45 +106,6 @@ impl LuaUserData for LuaCursorController { } } -// #[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 @@ -161,7 +132,7 @@ impl LuaUserData for LuaBufferController { methods.add_method("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(()), + None => Err(LuaError::RuntimeError("wtf".into())), } }); methods.add_method("replace", |_, this, txt:String| { @@ -184,6 +155,10 @@ impl LuaUserData for LuaBufferController { .map_err(LuaCodempError::from)?; Ok(()) }); + + methods.add_method("byte2rowcol", |_, this, (byte,)| { + Ok(LuaRowCol(byte_to_rowcol(&this.0.content(), byte))) + }); } fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { @@ -205,6 +180,15 @@ impl LuaUserData for LuaTextChange { } } +#[derive(Debug, 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.row)); + fields.add_field_method_get("col", |_, this| Ok(this.0.col)); + } +} + #[mlua::lua_module] @@ -216,5 +200,17 @@ fn libcodemp_nvim(lua: &Lua) -> LuaResult { exports.set("attach", lua.create_function(attach)?)?; exports.set("get_cursor", lua.create_function(get_cursor)?)?; exports.set("get_buffer", lua.create_function(get_buffer)?)?; + exports.set("byte2rowcol",lua.create_function(byte2rowcol)?)?; Ok(exports) } + + +// TODO this is wasteful because, just to calculate two indices, we clone a +// potentially big string. this is necessary because vim doesn't provide an +// api equivalent of byte2line (we need to specify arbitrary buffers). +fn byte2rowcol(_: &Lua, (txt, index): (String, usize)) -> LuaResult<(usize, usize)> { + let lines = txt[..index].split('\n'); + let col = lines.clone().last().unwrap_or("").len(); + let row = lines.count() - 1; + Ok((row, col)) +} From 2b5fd19a0ecf05d478a0a5acaafd6b52f52135d0 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 4 Sep 2023 18:35:24 +0200 Subject: [PATCH 06/23] fix: make it work cheating --- Cargo.toml | 2 +- src/codemp.lua | 45 ++++++++++++++++++++++++--------------------- src/lib.rs | 32 ++++---------------------------- 3 files changed, 29 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d9f9ea..2d20b17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.4.4", features = ["global", "sync"] } +codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.4.5", features = ["global", "sync"] } mlua = { version = "0.9.0", features = ["module", "luajit"] } thiserror = "1.0.47" derive_more = "0.99.17" diff --git a/src/codemp.lua b/src/codemp.lua index d273bb1..101cf29 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -173,33 +173,36 @@ vim.api.nvim_create_user_command( vim.api.nvim_buf_attach(buffer, false, { on_lines = function (_, buf, tick, firstline, lastline, new_lastline, old_byte_size) if tick == codemp_changed_tick then return end - print(string.format(">[%s] %s:%s|%s (%s)", tick, firstline, lastline, new_lastline, old_byte_size)) - local start_index = firstline == 0 and 0 or vim.fn.line2byte(firstline + 1) - 1 - local text = table.concat( - vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, true), - "\n" - ) - if lastline ~= new_lastline then - text = text .. "\n" - end - print(string.format(">delta [%d,%s,%d]", start_index, text, start_index + old_byte_size - 1)) - controller:delta(start_index, text, start_index + old_byte_size - 1) + -- print(string.format(">[%s] %s:%s|%s (%s)", tick, firstline, lastline, new_lastline, old_byte_size)) + -- local start_index = firstline == 0 and 0 or vim.fn.line2byte(firstline + 1) - 1 + -- local text = table.concat( + -- vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, true), + -- "\n" + -- ) + -- -- if lastline ~= new_lastline then + -- -- text = text .. "\n" + -- -- end + -- print(string.format(">delta [%d,%s,%d]", start_index, text, start_index + old_byte_size - 1)) + -- controller:delta(start_index, text, start_index + old_byte_size - 1) + local content = buffer_get_content(buf) + controller:replace(content) end }) -- hook clientbound callbacks register_controller_handler(args.args, controller, function(event) codemp_changed_tick = vim.api.nvim_buf_get_changedtick(buffer) + 1 - local start = controller:byte2rowcol(event.start) - local finish = controller:byte2rowcol(event.finish) - print(string.format( - "buf_set_text(%s,%s, %s,%s, '%s')", - start.row, start.col, finish.row, finish.col, vim.inspect(split_without_trim(event.content, "\n")) - )) - vim.api.nvim_buf_set_text( - buffer, start.row, start.col, finish.row, finish.col, - split_without_trim(event.content, "\n") - ) + -- local start = event.start + -- local finish = event.finish + -- print(string.format( + -- "buf_set_text(%s,%s, %s,%s, '%s')", + -- start.row, start.col, finish.row, finish.col, vim.inspect(split_without_trim(event.content, "\n")) + -- )) + -- vim.api.nvim_buf_set_text( + -- buffer, start.row, start.col, finish.row, finish.col, + -- split_without_trim(event.content, "\n") + -- ) + buffer_set_content(buffer, controller.content) end) print(" ++ joined workspace " .. args.args) diff --git a/src/lib.rs b/src/lib.rs index c0ce2f9..1070130 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,16 +13,6 @@ impl From:: for LuaError { } } -fn byte_to_rowcol(text: &str, index: usize) -> CodempRowCol { - let lines_before = text[..index].split('\n').count() - 1; - let chars_before = text[..index].split('\n').last().unwrap_or_default().len(); - - CodempRowCol { - row: lines_before as i32, - col: chars_before as i32, - } -} - fn cursor_to_table(lua: &Lua, cur: CodempCursorEvent) -> LuaResult { let pos = cur.position.unwrap_or_default(); let start = lua.create_table()?; @@ -155,10 +145,6 @@ impl LuaUserData for LuaBufferController { .map_err(LuaCodempError::from)?; Ok(()) }); - - methods.add_method("byte2rowcol", |_, this, (byte,)| { - Ok(LuaRowCol(byte_to_rowcol(&this.0.content(), byte))) - }); } fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { @@ -171,8 +157,10 @@ 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)); + // fields.add_field_method_get("start", |_, this| Ok(LuaRowCol(this.0.start()))); + // fields.add_field_method_get("finish", |_, this| Ok(LuaRowCol(this.0.end()))); + // fields.add_field_method_get("before", |_, this| Ok((*this.0.before).clone())); + // fields.add_field_method_get("after", |_, this| Ok((*this.0.after).clone())); } fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { @@ -200,17 +188,5 @@ fn libcodemp_nvim(lua: &Lua) -> LuaResult { exports.set("attach", lua.create_function(attach)?)?; exports.set("get_cursor", lua.create_function(get_cursor)?)?; exports.set("get_buffer", lua.create_function(get_buffer)?)?; - exports.set("byte2rowcol",lua.create_function(byte2rowcol)?)?; Ok(exports) } - - -// TODO this is wasteful because, just to calculate two indices, we clone a -// potentially big string. this is necessary because vim doesn't provide an -// api equivalent of byte2line (we need to specify arbitrary buffers). -fn byte2rowcol(_: &Lua, (txt, index): (String, usize)) -> LuaResult<(usize, usize)> { - let lines = txt[..index].split('\n'); - let col = lines.clone().last().unwrap_or("").len(); - let row = lines.count() - 1; - Ok((row, col)) -} From d0da62f6fd8c6e6d4026598fed8bad228d517bf0 Mon Sep 17 00:00:00 2001 From: alemi Date: Mon, 4 Sep 2023 22:31:29 +0200 Subject: [PATCH 07/23] fix: if cursor is long 0, add 1 idk how to display a bar in vim since it's a terminal --- src/codemp.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/src/codemp.lua b/src/codemp.lua index 101cf29..47ee16a 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -96,6 +96,7 @@ local function buffer_set_content(buf, content) end local function multiline_highlight(buf, ns, group, start, fini) + if start[2] == fini[2] then fini[2] = fini[2] + 1 end for i=start[1],fini[1] do if i == start[1] and i == fini[1] then vim.api.nvim_buf_add_highlight(buf, ns, group, i, start[2], fini[2]) From b4ac11bade1d917ebe6a8758625529e64fe32257 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 5 Sep 2023 02:51:15 +0200 Subject: [PATCH 08/23] feat: only send cursor updates in shared buffers also put correct buffer in cursor updates --- src/codemp.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/codemp.lua b/src/codemp.lua index 47ee16a..0176fd3 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -110,6 +110,8 @@ local function multiline_highlight(buf, ns, group, start, fini) end end +local buffer_mappings = {} + vim.api.nvim_create_user_command( "Connect", function (args) @@ -131,7 +133,10 @@ vim.api.nvim_create_user_command( group = vim.api.nvim_create_augroup("codemp-workspace-" .. args.args, { clear = true }), callback = function (_) local cur = cursor_position() - controller:send("", cur[1][1], cur[1][2], cur[2][1], cur[2][2]) + local buf = vim.api.nvim_get_current_buf() + if buffer_mappings[buf] ~= nil then + controller:send(buffer_mappings[buf], cur[1][1], cur[1][2], cur[2][1], cur[2][2]) + end end }) @@ -167,6 +172,7 @@ vim.api.nvim_create_user_command( local controller = codemp.attach(args.args) local buffer = vim.api.nvim_get_current_buf() + buffer_mappings[buffer] = args.args buffer_set_content(buffer, controller.content) From e526d874f9d0d678391faf697bd035621e3c00a3 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 5 Sep 2023 03:05:20 +0200 Subject: [PATCH 09/23] feat: distinct cursors, some different colors --- src/codemp.lua | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index 0176fd3..33dbae3 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -111,6 +111,13 @@ local function multiline_highlight(buf, ns, group, start, fini) end local buffer_mappings = {} +local user_mappings = {} +local available_colors = { + "ErrorMsg", + "WarningMsg", + "MatchParen", + "SpecialMode", +} vim.api.nvim_create_user_command( "Connect", @@ -126,7 +133,6 @@ vim.api.nvim_create_user_command( function (args) local controller = codemp.join(args.args) local buffer = vim.api.nvim_get_current_buf() - local ns = vim.api.nvim_create_namespace("codemp-cursors") -- hook serverbound callbacks vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI", "ModeChanged"}, { @@ -142,8 +148,20 @@ vim.api.nvim_create_user_command( -- hook clientbound callbacks register_controller_handler(nil, controller, function(event) - vim.api.nvim_buf_clear_namespace(buffer, ns, 0, -1) - multiline_highlight(buffer, ns, "ErrorMsg", event.start, event.finish) + if user_mappings[event.user] == nil then + user_mappings[event.user] = { + ns = vim.api.nvim_create_namespace("codemp-cursor-" .. event.user), + hi = available_colors[ math.random( #available_colors ) ], + } + end + vim.api.nvim_buf_clear_namespace(buffer, user_mappings[event.user].ns, 0, -1) + multiline_highlight( + buffer, + user_mappings[event.user].ns, + user_mappings[event.user].hi, + event.start, + event.finish + ) end) print(" ++ joined workspace " .. args.args) From 6791055f9eda460951b334f383b3bfd9c21fa44d Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 5 Sep 2023 23:57:57 +0200 Subject: [PATCH 10/23] feat: add ability to log to file --- Cargo.toml | 2 ++ src/lib.rs | 16 +++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2d20b17..694519b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,5 @@ codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/cod mlua = { version = "0.9.0", features = ["module", "luajit"] } thiserror = "1.0.47" derive_more = "0.99.17" +tracing-subscriber = "0.3.17" +tracing = "0.1.37" diff --git a/src/lib.rs b/src/lib.rs index 1070130..8919e3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{sync::{Arc, Mutex}, fs::File}; use codemp::prelude::*; use mlua::prelude::*; @@ -179,9 +179,23 @@ impl LuaUserData for LuaRowCol { +// setup library logging to file +fn setup_tracing(_: &Lua, (path,): (String,)) -> LuaResult<()> { + let log_file = File::create(path)?; + tracing_subscriber::fmt() + .with_max_level(tracing::Level::INFO) + .with_writer(Mutex::new(log_file)) + .init(); + Ok(()) +} + + + +// define module and exports #[mlua::lua_module] fn libcodemp_nvim(lua: &Lua) -> LuaResult { let exports = lua.create_table()?; + exports.set("setup_tracing", lua.create_function(setup_tracing)?)?; exports.set("connect", lua.create_function(connect)?)?; exports.set("join", lua.create_function(join)?)?; exports.set("create", lua.create_function(create)?)?; From a19beb2f1a73d64a0344a136972e0d1b5354a181 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 5 Sep 2023 23:59:20 +0200 Subject: [PATCH 11/23] feat: show cursors on correct buffers also made proper bindings for CursorEvent and CursorPosition --- src/codemp.lua | 20 +++++++++++--------- src/lib.rs | 44 ++++++++++++++++++++++++++------------------ 2 files changed, 37 insertions(+), 27 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index 33dbae3..7633925 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -132,7 +132,6 @@ vim.api.nvim_create_user_command( "Join", function (args) local controller = codemp.join(args.args) - local buffer = vim.api.nvim_get_current_buf() -- hook serverbound callbacks vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI", "ModeChanged"}, { @@ -154,14 +153,17 @@ vim.api.nvim_create_user_command( hi = available_colors[ math.random( #available_colors ) ], } end - vim.api.nvim_buf_clear_namespace(buffer, user_mappings[event.user].ns, 0, -1) - multiline_highlight( - buffer, - user_mappings[event.user].ns, - user_mappings[event.user].hi, - event.start, - event.finish - ) + local buffer = buffer_mappings[event.position.buffer] + if buffer ~= nil then + vim.api.nvim_buf_clear_namespace(buffer, user_mappings[event.user].ns, 0, -1) + multiline_highlight( + buffer, + user_mappings[event.user].ns, + user_mappings[event.user].hi, + event.position.start, + event.position.finish + ) + end end) print(" ++ joined workspace " .. args.args) diff --git a/src/lib.rs b/src/lib.rs index 8919e3c..4b76345 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,22 +13,7 @@ impl From:: for LuaError { } } -fn cursor_to_table(lua: &Lua, cur: CodempCursorEvent) -> LuaResult { - let pos = cur.position.unwrap_or_default(); - let start = lua.create_table()?; - start.set(1, pos.start().row)?; - start.set(2, pos.start().col)?; - let end = lua.create_table()?; - end.set(1, pos.end().row)?; - end.set(2, 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) -} - +// TODO put friendlier constructor directly in lib? fn make_cursor(buffer: String, start_row: i32, start_col: i32, end_row: i32, end_col: i32) -> CodempCursorPosition { CodempCursorPosition { buffer, @@ -67,6 +52,8 @@ fn get_buffer(_: &Lua, (path,): (String,)) -> LuaResult { ) } + + /// join a remote workspace and start processing cursor events fn join(_: &Lua, (session,): (String,)) -> LuaResult { let controller = CODEMP_INSTANCE.join(&session) @@ -82,9 +69,9 @@ impl LuaUserData for LuaCursorController { methods.add_method("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("try_recv", |lua, this, ()| { + methods.add_method("try_recv", |_, this, ()| { match this.0.try_recv() .map_err(LuaCodempError::from)? { - Some(x) => Ok(Some(cursor_to_table(lua, x)?)), + Some(x) => Ok(Some(LuaCursorEvent(x))), None => Ok(None), } }); @@ -96,6 +83,27 @@ impl LuaUserData for LuaCursorController { } } +#[derive(Debug, 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.clone())); + fields.add_field_method_get("position", |_, this| + Ok(this.0.position.as_ref().map(|x| LuaCursorPosition(x.clone()))) + ); + } +} + +#[derive(Debug, 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.clone())); + fields.add_field_method_get("start", |_, this| Ok(LuaRowCol(this.0.start()))); + fields.add_field_method_get("finish", |_, this| Ok(LuaRowCol(this.0.end()))); + } +} + /// create a new buffer in current workspace From 1998ee986c858c1edac3f97241bbe39c23e41924 Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 6 Sep 2023 00:09:38 +0200 Subject: [PATCH 12/23] fix: need reverse mapping.. also they arent tuples --- src/codemp.lua | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index 7633925..984d94e 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -96,14 +96,15 @@ local function buffer_set_content(buf, content) end local function multiline_highlight(buf, ns, group, start, fini) - if start[2] == fini[2] then fini[2] = fini[2] + 1 end - for i=start[1],fini[1] do - if i == start[1] and i == fini[1] then - vim.api.nvim_buf_add_highlight(buf, ns, group, i, start[2], fini[2]) - elseif i == start[1] then - vim.api.nvim_buf_add_highlight(buf, ns, group, i, start[2], -1) - elseif i == fini[1] then - vim.api.nvim_buf_add_highlight(buf, ns, group, i, 0, fini[2]) + for i=start.row,fini.row do + if i == start.row and i == fini.row then + local fini_col = fini.col + if start.col == fini.col then fini_col = fini_col + 1 end + vim.api.nvim_buf_add_highlight(buf, ns, group, i, start.col, fini_col) + elseif i == start.row then + vim.api.nvim_buf_add_highlight(buf, ns, group, i, start.col, -1) + elseif i == fini.row then + vim.api.nvim_buf_add_highlight(buf, ns, group, i, 0, fini.col) else vim.api.nvim_buf_add_highlight(buf, ns, group, i, 0, -1) end @@ -111,6 +112,7 @@ local function multiline_highlight(buf, ns, group, start, fini) end local buffer_mappings = {} +local buffer_mappings_reverse = {} -- TODO maybe not??? local user_mappings = {} local available_colors = { "ErrorMsg", @@ -153,7 +155,7 @@ vim.api.nvim_create_user_command( hi = available_colors[ math.random( #available_colors ) ], } end - local buffer = buffer_mappings[event.position.buffer] + local buffer = buffer_mappings_reverse[event.position.buffer] if buffer ~= nil then vim.api.nvim_buf_clear_namespace(buffer, user_mappings[event.user].ns, 0, -1) multiline_highlight( @@ -193,6 +195,7 @@ vim.api.nvim_create_user_command( local buffer = vim.api.nvim_get_current_buf() buffer_mappings[buffer] = args.args + buffer_mappings_reverse[args.args] = buffer buffer_set_content(buffer, controller.content) From 00aee70fc375cfee467c472d410f7184d6fa7acb Mon Sep 17 00:00:00 2001 From: alemi Date: Wed, 6 Sep 2023 00:15:36 +0200 Subject: [PATCH 13/23] feat: allow setting debug log level --- src/lib.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4b76345..e47c7a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -188,10 +188,11 @@ impl LuaUserData for LuaRowCol { // setup library logging to file -fn setup_tracing(_: &Lua, (path,): (String,)) -> LuaResult<()> { +fn setup_tracing(_: &Lua, (path, debug): (String, Option)) -> LuaResult<()> { let log_file = File::create(path)?; + let level = if debug.unwrap_or(false) { tracing::Level::DEBUG } else {tracing::Level::INFO }; tracing_subscriber::fmt() - .with_max_level(tracing::Level::INFO) + .with_max_level(level) .with_writer(Mutex::new(log_file)) .init(); Ok(()) @@ -203,12 +204,17 @@ fn setup_tracing(_: &Lua, (path,): (String,)) -> LuaResult<()> { #[mlua::lua_module] fn libcodemp_nvim(lua: &Lua) -> LuaResult { let exports = lua.create_table()?; - exports.set("setup_tracing", lua.create_function(setup_tracing)?)?; + + // core proto functions 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)?)?; + // state helpers exports.set("get_cursor", lua.create_function(get_cursor)?)?; exports.set("get_buffer", lua.create_function(get_buffer)?)?; + // debug + exports.set("setup_tracing", lua.create_function(setup_tracing)?)?; + Ok(exports) } From bcbb7a74556e0e362f944b199f8ecfe47f7371cd Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 10 Nov 2023 05:33:20 +0100 Subject: [PATCH 14/23] feat: add lua logger to collect tracing msgs --- src/codemp.lua | 10 ++++++++++ src/lib.rs | 50 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index 984d94e..3808865 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -240,6 +240,16 @@ vim.api.nvim_create_user_command( { nargs = 1 } ) +-- TODO nvim docs say that we should stop all threads before exiting nvim +-- but we like to live dangerously (: +vim.loop.new_thread({}, function() + local _codemp = require("libcodemp_nvim") + local logger = _codemp.setup_tracing() + while true do + print(logger:recv()) + end +end) + return { lib = codemp, utils = { diff --git a/src/lib.rs b/src/lib.rs index e47c7a5..32fc27b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ -use std::{sync::{Arc, Mutex}, fs::File}; +use std::io::Write; +use std::sync::{Arc, Mutex, mpsc}; use codemp::prelude::*; use mlua::prelude::*; @@ -188,14 +189,53 @@ impl LuaUserData for LuaRowCol { // setup library logging to file -fn setup_tracing(_: &Lua, (path, debug): (String, Option)) -> LuaResult<()> { - let log_file = File::create(path)?; +#[derive(Debug, derive_more::From)] +struct LuaLogger(Arc>>); +impl LuaUserData for LuaLogger { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_method("recv", |_, this, ()| { + Ok( + this.0 + .lock() + .expect("logger mutex poisoned") + .recv() + .expect("logger channel closed") + ) + }); + } +} + +#[derive(Debug, Clone)] +struct LuaLoggerProducer(mpsc::Sender); +impl Write for LuaLoggerProducer { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.0.send(String::from_utf8_lossy(buf).to_string()) + .expect("could not write on logger channel"); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { Ok(()) } +} + +fn setup_tracing(_: &Lua, (debug,): (Option,)) -> LuaResult { + let (tx, rx) = mpsc::channel(); let level = if debug.unwrap_or(false) { tracing::Level::DEBUG } else {tracing::Level::INFO }; + 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(Mutex::new(log_file)) + .with_writer(Mutex::new(LuaLoggerProducer(tx))) .init(); - Ok(()) + Ok(LuaLogger(Arc::new(Mutex::new(rx)))) } From 140dd0ff4c4671cd92a524b817f23c04dec5df1a Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 10 Nov 2023 05:35:18 +0100 Subject: [PATCH 15/23] feat: initial broken attempt with early woot api --- Cargo.toml | 2 +- src/codemp.lua | 25 +++++++++--------------- src/lib.rs | 53 +++++++++++++++++++++++++++----------------------- 3 files changed, 39 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 694519b..0929874 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.4.5", features = ["global", "sync"] } +codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", branch = "woot", features = ["global", "sync"] } mlua = { version = "0.9.0", features = ["module", "luajit"] } thiserror = "1.0.47" derive_more = "0.99.17" diff --git a/src/codemp.lua b/src/codemp.lua index 3808865..989a30a 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -25,17 +25,6 @@ local function register_controller_handler(target, controller, handler) end, async, target) end --- local function byte2rowcol(buf, x) --- local row --- local row_start --- vim.api.nvim_buf_call(buf, function () --- row = vim.fn.byte2line(x) --- row_start = vim.fn.line2byte(row) --- end) --- local col = x - row_start --- return { row, col } --- end - local function split_without_trim(str, sep) local res = vim.fn.split(str, sep) if str:sub(1,1) == "\n" then @@ -114,11 +103,14 @@ end local buffer_mappings = {} local buffer_mappings_reverse = {} -- TODO maybe not??? local user_mappings = {} -local available_colors = { +local available_colors = { -- TODO these are definitely not portable! "ErrorMsg", "WarningMsg", "MatchParen", "SpecialMode", + "CmpItemKindFunction", + "CmpItemKindValue", + "CmpItemKindInterface", } vim.api.nvim_create_user_command( @@ -193,6 +185,8 @@ vim.api.nvim_create_user_command( function (args) local controller = codemp.attach(args.args) + -- TODO map name to uuid + local buffer = vim.api.nvim_get_current_buf() buffer_mappings[buffer] = args.args buffer_mappings_reverse[args.args] = buffer @@ -202,7 +196,8 @@ vim.api.nvim_create_user_command( -- hook serverbound callbacks vim.api.nvim_buf_attach(buffer, false, { on_lines = function (_, buf, tick, firstline, lastline, new_lastline, old_byte_size) - if tick == codemp_changed_tick then return end + local content = buffer_get_content(buf) + controller:send(0, #content - 1, content) -- print(string.format(">[%s] %s:%s|%s (%s)", tick, firstline, lastline, new_lastline, old_byte_size)) -- local start_index = firstline == 0 and 0 or vim.fn.line2byte(firstline + 1) - 1 -- local text = table.concat( @@ -214,8 +209,6 @@ vim.api.nvim_create_user_command( -- -- end -- print(string.format(">delta [%d,%s,%d]", start_index, text, start_index + old_byte_size - 1)) -- controller:delta(start_index, text, start_index + old_byte_size - 1) - local content = buffer_get_content(buf) - controller:replace(content) end }) @@ -232,7 +225,7 @@ vim.api.nvim_create_user_command( -- buffer, start.row, start.col, finish.row, finish.col, -- split_without_trim(event.content, "\n") -- ) - buffer_set_content(buffer, controller.content) + buffer_set_content(buffer, event) end) print(" ++ joined workspace " .. args.args) diff --git a/src/lib.rs b/src/lib.rs index 32fc27b..9aae7e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ use std::io::Write; use std::sync::{Arc, Mutex, mpsc}; use codemp::prelude::*; +use codemp::woot::crdt::Op; use mlua::prelude::*; @@ -27,7 +28,9 @@ fn make_cursor(buffer: String, start_row: i32, start_col: i32, end_row: i32, end } } - +#[derive(Debug, derive_more::From)] +struct LuaOp(Op); +impl LuaUserData for LuaOp { } /// connect to remote server fn connect(_: &Lua, (host,): (Option,)) -> LuaResult<()> { @@ -66,7 +69,7 @@ fn join(_: &Lua, (session,): (String,)) -> LuaResult { struct LuaCursorController(Arc); impl LuaUserData for LuaCursorController { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this.0))); methods.add_method("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)?) }); @@ -127,25 +130,22 @@ fn attach(_: &Lua, (path,): (String,)) -> LuaResult { struct LuaBufferController(Arc); impl LuaUserData for LuaBufferController { fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); - methods.add_method("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 => Err(LuaError::RuntimeError("wtf".into())), - } - }); - methods.add_method("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("insert", |_, this, (txt, pos):(String, u64)| { - Ok(this.0.send(this.0.insert(&txt, pos)).map_err(LuaCodempError::from)?) + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this.0))); + methods.add_method("send", |_, this, (start, end, text): (usize, usize, String)| { + Ok( + this.0.send( + CodempTextChange { + span: start..end, + content: text, + after: "".into(), + } + ) + .map_err(LuaCodempError::from)? + ) }); methods.add_method("try_recv", |_, this, ()| { match this.0.try_recv() .map_err(LuaCodempError::from)? { - Some(x) => Ok(Some(LuaTextChange(x))), + Some(x) => Ok(Some(x)), None => Ok(None), } }); @@ -157,7 +157,7 @@ impl LuaUserData for LuaBufferController { } fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("content", |_, this| Ok(this.0.content())); + fields.add_field_method_get("content", |_, this| Ok(this.0.try_recv().unwrap().unwrap())); } } @@ -166,14 +166,19 @@ 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(LuaRowCol(this.0.start()))); - // fields.add_field_method_get("finish", |_, this| Ok(LuaRowCol(this.0.end()))); - // fields.add_field_method_get("before", |_, this| Ok((*this.0.before).clone())); - // fields.add_field_method_get("after", |_, this| Ok((*this.0.after).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)); } fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this))); + methods.add_meta_function(LuaMetaMethod::Call, |_, (start, end, txt): (usize, usize, String)| { + Ok(LuaTextChange(CodempTextChange { + span: start..end, + content: txt, + after: "".into(), + })) + }); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this.0))); } } From b59086bda6a6bae5c4f4faf0ae49e5da078d4ce7 Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 16 Nov 2023 06:54:32 +0100 Subject: [PATCH 16/23] fix: make it work with new api --- src/codemp.lua | 5 +++-- src/lib.rs | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index 989a30a..aa0f7e7 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -1,6 +1,6 @@ local codemp = require("libcodemp_nvim") -local codemp_changed_tick = nil -- TODO this doesn't work when events are coalesced +local codemp_changed_tick = 0 -- TODO this doesn't work when events are coalesced local function register_controller_handler(target, controller, handler) local async = vim.loop.new_async(function() @@ -191,11 +191,12 @@ vim.api.nvim_create_user_command( buffer_mappings[buffer] = args.args buffer_mappings_reverse[args.args] = buffer - buffer_set_content(buffer, controller.content) + -- buffer_set_content(buffer, controller.content) -- hook serverbound callbacks vim.api.nvim_buf_attach(buffer, false, { on_lines = function (_, buf, tick, firstline, lastline, new_lastline, old_byte_size) + if tick <= codemp_changed_tick then return end local content = buffer_get_content(buf) controller:send(0, #content - 1, content) -- print(string.format(">[%s] %s:%s|%s (%s)", tick, firstline, lastline, new_lastline, old_byte_size)) diff --git a/src/lib.rs b/src/lib.rs index 9aae7e7..5e03494 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,7 @@ impl LuaUserData for LuaBufferController { }); methods.add_method("try_recv", |_, this, ()| { match this.0.try_recv() .map_err(LuaCodempError::from)? { - Some(x) => Ok(Some(x)), + Some(x) => Ok(Some(x.content)), None => Ok(None), } }); @@ -157,7 +157,10 @@ impl LuaUserData for LuaBufferController { } fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { - fields.add_field_method_get("content", |_, this| Ok(this.0.try_recv().unwrap().unwrap())); + fields.add_field_method_get("content", |_, this| Ok( + this.0.try_recv().map(|x| x.map(|y| y.content)) + .map_err(LuaCodempError::from)? + )); } } From f5a241211337b894b66df2418db1e07dee87c9ba Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 16 Nov 2023 22:07:09 +0100 Subject: [PATCH 17/23] fix: proper text events handling this works properly but only now that codemp lib sends whole buffer as text change every time --- src/codemp.lua | 37 +++++++++++++------------------------ src/lib.rs | 6 +++--- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index aa0f7e7..e704537 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -191,42 +191,31 @@ vim.api.nvim_create_user_command( buffer_mappings[buffer] = args.args buffer_mappings_reverse[args.args] = buffer - -- buffer_set_content(buffer, controller.content) - -- hook serverbound callbacks vim.api.nvim_buf_attach(buffer, false, { on_lines = function (_, buf, tick, firstline, lastline, new_lastline, old_byte_size) if tick <= codemp_changed_tick then return end - local content = buffer_get_content(buf) - controller:send(0, #content - 1, content) - -- print(string.format(">[%s] %s:%s|%s (%s)", tick, firstline, lastline, new_lastline, old_byte_size)) - -- local start_index = firstline == 0 and 0 or vim.fn.line2byte(firstline + 1) - 1 - -- local text = table.concat( - -- vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, true), - -- "\n" - -- ) - -- -- if lastline ~= new_lastline then - -- -- text = text .. "\n" - -- -- end - -- print(string.format(">delta [%d,%s,%d]", start_index, text, start_index + old_byte_size - 1)) - -- controller:delta(start_index, text, start_index + old_byte_size - 1) + local start = vim.api.nvim_buf_get_offset(buf, firstline) + local content = table.concat(vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, false), '\n') + if new_lastline < lastline then old_byte_size = old_byte_size + 1 end + controller:send(start, start + old_byte_size - 1, content) end }) -- hook clientbound callbacks register_controller_handler(args.args, controller, function(event) codemp_changed_tick = vim.api.nvim_buf_get_changedtick(buffer) + 1 - -- local start = event.start - -- local finish = event.finish - -- print(string.format( - -- "buf_set_text(%s,%s, %s,%s, '%s')", - -- start.row, start.col, finish.row, finish.col, vim.inspect(split_without_trim(event.content, "\n")) - -- )) + buffer_set_content(buffer, event.content) + -- local start_row = vim.api.nvim_buf_get_offset(buffer, event.first) + -- local end_row = vim.api.nvim_buf_get_offset(buffer, event.last - 1) -- vim.api.nvim_buf_set_text( - -- buffer, start.row, start.col, finish.row, finish.col, - -- split_without_trim(event.content, "\n") + -- buffer, + -- start_row, + -- event.first - start_row, + -- end_row, + -- event.last - end_row, + -- vim.fn.split(event.content, '\n', true) -- ) - buffer_set_content(buffer, event) end) print(" ++ joined workspace " .. args.args) diff --git a/src/lib.rs b/src/lib.rs index 5e03494..7592e3b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -145,7 +145,7 @@ impl LuaUserData for LuaBufferController { }); methods.add_method("try_recv", |_, this, ()| { match this.0.try_recv() .map_err(LuaCodempError::from)? { - Some(x) => Ok(Some(x.content)), + Some(x) => Ok(Some(LuaTextChange(x))), None => Ok(None), } }); @@ -169,8 +169,8 @@ 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)); + fields.add_field_method_get("first", |_, this| Ok(this.0.span.start)); + fields.add_field_method_get("last", |_, this| Ok(this.0.span.end)); } fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { From f69d91b9f632d531411cb4190d824e346a460c07 Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 17 Nov 2023 03:44:57 +0100 Subject: [PATCH 18/23] fix: insert at index 0, locked dep --- Cargo.toml | 2 +- src/codemp.lua | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0929874..4a2b82a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", branch = "woot", features = ["global", "sync"] } +codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", rev = "486999bc31b8a925d56ee6d3747b06fcd97b00d5", features = ["global", "sync"] } mlua = { version = "0.9.0", features = ["module", "luajit"] } thiserror = "1.0.47" derive_more = "0.99.17" diff --git a/src/codemp.lua b/src/codemp.lua index e704537..d871bad 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -197,6 +197,7 @@ vim.api.nvim_create_user_command( if tick <= codemp_changed_tick then return end local start = vim.api.nvim_buf_get_offset(buf, firstline) local content = table.concat(vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, false), '\n') + if start == -1 then start = 0 end if new_lastline < lastline then old_byte_size = old_byte_size + 1 end controller:send(start, start + old_byte_size - 1, content) end @@ -206,16 +207,6 @@ vim.api.nvim_create_user_command( register_controller_handler(args.args, controller, function(event) codemp_changed_tick = vim.api.nvim_buf_get_changedtick(buffer) + 1 buffer_set_content(buffer, event.content) - -- local start_row = vim.api.nvim_buf_get_offset(buffer, event.first) - -- local end_row = vim.api.nvim_buf_get_offset(buffer, event.last - 1) - -- vim.api.nvim_buf_set_text( - -- buffer, - -- start_row, - -- event.first - start_row, - -- end_row, - -- event.last - end_row, - -- vim.fn.split(event.content, '\n', true) - -- ) end) print(" ++ joined workspace " .. args.args) From 67b9389c5b2ac1708c1ec136440a381a5d22d6af Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 17 Nov 2023 03:55:45 +0100 Subject: [PATCH 19/23] fix: apply received changes granularly --- src/codemp.lua | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index d871bad..bca3b17 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -79,9 +79,29 @@ local function buffer_get_content(buf) return table.concat(lines, '\n') end -local function buffer_set_content(buf, content) - local lines = split_without_trim(content, "\n") - vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) +-- local function buffer_set_content(buf, content) +-- local lines = split_without_trim(content, "\n") +-- vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) +-- end + +local function buffer_replace_content(buffer, first, last, content) + -- TODO send help it works but why is lost knowledge + local start_row = vim.fn.byte2line(first + 1) - 1 + if start_row < 0 then start_row = 0 end + local start_row_byte = vim.fn.line2byte(start_row + 1) - 1 + if start_row_byte < 0 then start_row_byte = 0 end + local end_row = vim.fn.byte2line(last + 1) - 1 + if end_row < 0 then end_row = 0 end + local end_row_byte = vim.fn.line2byte(end_row + 1) - 1 + if end_row_byte < 0 then end_row_byte = 0 end + vim.api.nvim_buf_set_text( + buffer, + start_row, + first - start_row_byte, + end_row, + last - end_row_byte, + vim.fn.split(content, '\n', true) + ) end local function multiline_highlight(buf, ns, group, start, fini) @@ -206,7 +226,7 @@ vim.api.nvim_create_user_command( -- hook clientbound callbacks register_controller_handler(args.args, controller, function(event) codemp_changed_tick = vim.api.nvim_buf_get_changedtick(buffer) + 1 - buffer_set_content(buffer, event.content) + buffer_replace_content(buffer, event.first, event.last, event.content) end) print(" ++ joined workspace " .. args.args) From a084b6e1b69b3113116dc2ee8194bf876f85b1fb Mon Sep 17 00:00:00 2001 From: alemi Date: Fri, 17 Nov 2023 06:02:55 +0100 Subject: [PATCH 20/23] fix: sleep to avoid bugs... --- Cargo.toml | 2 +- src/codemp.lua | 15 +++++++++++---- src/lib.rs | 2 -- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4a2b82a..bf46e33 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", rev = "486999bc31b8a925d56ee6d3747b06fcd97b00d5", features = ["global", "sync"] } +codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", rev = "c7319b9f68b19b0b34ecf71cc4b00999248bbd14", features = ["global", "sync"] } mlua = { version = "0.9.0", features = ["module", "luajit"] } thiserror = "1.0.47" derive_more = "0.99.17" diff --git a/src/codemp.lua b/src/codemp.lua index bca3b17..35b0a87 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -2,7 +2,7 @@ local codemp = require("libcodemp_nvim") local codemp_changed_tick = 0 -- TODO this doesn't work when events are coalesced -local function register_controller_handler(target, controller, handler) +local function register_controller_handler(target, controller, handler, delay) local async = vim.loop.new_async(function() while true do local event = controller:try_recv() @@ -15,14 +15,15 @@ local function register_controller_handler(target, controller, handler) -- completely useless. We can circumvent this by requiring codemp again in the new -- thread and requesting a new reference to the same controller from che global instance -- NOTE variables prefixed with underscore live in another Lua runtime - vim.loop.new_thread({}, function(_async, _target) + vim.loop.new_thread({}, function(_async, _target, _delay) + if _delay ~= nil then vim.loop.sleep(_delay) end local _codemp = require("libcodemp_nvim") local _controller = _target ~= nil and _codemp.get_buffer(_target) or _codemp.get_cursor() while true do _controller:poll() _async:send() end - end, async, target) + end, async, target, delay) end local function split_without_trim(str, sep) @@ -223,11 +224,17 @@ vim.api.nvim_create_user_command( end }) + -- This is an ugly as hell fix: basically we receive all operations real fast at the start + -- so the buffer changes rapidly and it messes up tracking our delta/diff state and we + -- get borked translated TextChanges (the underlying CRDT is fine) + -- basically delay a bit so that it has time to sync and we can then get "normal slow" changes + -- vim.loop.sleep(200) -- moved inside poller thread to at least not block ui + -- hook clientbound callbacks register_controller_handler(args.args, controller, function(event) codemp_changed_tick = vim.api.nvim_buf_get_changedtick(buffer) + 1 buffer_replace_content(buffer, event.first, event.last, event.content) - end) + end, 200) -- delay by 200 ms as ugly fix print(" ++ joined workspace " .. args.args) end, diff --git a/src/lib.rs b/src/lib.rs index 7592e3b..13f6cf4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -137,7 +137,6 @@ impl LuaUserData for LuaBufferController { CodempTextChange { span: start..end, content: text, - after: "".into(), } ) .map_err(LuaCodempError::from)? @@ -178,7 +177,6 @@ impl LuaUserData for LuaTextChange { Ok(LuaTextChange(CodempTextChange { span: start..end, content: txt, - after: "".into(), })) }); methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this.0))); From 0a990c733c6ab35866deae784d4409896e421cfb Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 23 Nov 2023 15:09:13 +0100 Subject: [PATCH 21/23] fix: build on mac --- .cargo/config | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.cargo/config b/.cargo/config index c91c3f3..31ffe83 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,14 @@ +[target.x86_64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] + +[target.aarch64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] + [net] git-fetch-with-cli = true From 2393a2ceb56c9fb9c69ed4730b4bd0edd479c741 Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 23 Nov 2023 15:10:30 +0100 Subject: [PATCH 22/23] feat: catch worker error, add disconnect and detach --- src/codemp.lua | 58 +++++++++++++++++++++++++++++++++++++++++++------- src/lib.rs | 15 +++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/codemp.lua b/src/codemp.lua index 35b0a87..8a7145f 100644 --- a/src/codemp.lua +++ b/src/codemp.lua @@ -5,9 +5,13 @@ local codemp_changed_tick = 0 -- TODO this doesn't work when events are coalesce local function register_controller_handler(target, controller, handler, delay) local async = vim.loop.new_async(function() while true do - local event = controller:try_recv() - if event == nil then break end - vim.schedule(function() handler(event) end) + local success, event = pcall(controller.try_recv, controller) + if success then + if event == nil then break end + vim.schedule(function() handler(event) end) + else + print("error receiving: deadlocked?") + end end end) -- TODO controller can't be passed to the uvloop new_thread: when sent to the new @@ -20,8 +24,16 @@ local function register_controller_handler(target, controller, handler, delay) local _codemp = require("libcodemp_nvim") local _controller = _target ~= nil and _codemp.get_buffer(_target) or _codemp.get_cursor() while true do - _controller:poll() - _async:send() + local success, _ = pcall(_controller.poll, _controller) + if success then + _async:send() + else + local my_name = "cursor" + if _target ~= nil then + my_name = "buffer(" .. _target .. ")" + end + print(" -- stopping " .. my_name .. " controller poller") + end end end, async, target, delay) end @@ -204,18 +216,24 @@ vim.api.nvim_create_user_command( vim.api.nvim_create_user_command( "Attach", function (args) + local buffer = vim.api.nvim_create_buf(true, true) + vim.api.nvim_buf_set_option(buffer, 'fileformat', 'unix') + vim.api.nvim_buf_set_option(buffer, 'filetype', 'codemp') + vim.api.nvim_buf_set_name(buffer, "codemp::" .. args.args) + vim.api.nvim_set_current_buf(buffer) local controller = codemp.attach(args.args) -- TODO map name to uuid - local buffer = vim.api.nvim_get_current_buf() buffer_mappings[buffer] = args.args buffer_mappings_reverse[args.args] = buffer -- hook serverbound callbacks + -- TODO breaks when deleting whole lines at buffer end vim.api.nvim_buf_attach(buffer, false, { on_lines = function (_, buf, tick, firstline, lastline, new_lastline, old_byte_size) if tick <= codemp_changed_tick then return end + if buffer_mappings[buf] == nil then return true end -- exit worker local start = vim.api.nvim_buf_get_offset(buf, firstline) local content = table.concat(vim.api.nvim_buf_get_lines(buf, firstline, new_lastline, false), '\n') if start == -1 then start = 0 end @@ -234,13 +252,37 @@ vim.api.nvim_create_user_command( register_controller_handler(args.args, controller, function(event) codemp_changed_tick = vim.api.nvim_buf_get_changedtick(buffer) + 1 buffer_replace_content(buffer, event.first, event.last, event.content) - end, 200) -- delay by 200 ms as ugly fix + end, 500) -- delay by 200 ms as ugly fix - print(" ++ joined workspace " .. args.args) + print(" ++ attached to buffer " .. args.args) end, { nargs = 1 } ) +vim.api.nvim_create_user_command( + "Detach", + function (args) + local buffer = buffer_mappings_reverse[args.args] + if buffer == nil then buffer = vim.api.nvim_get_current_buf() end + local name = buffer_mappings[buffer] + buffer_mappings[buffer] = nil + buffer_mappings_reverse[name] = nil + codemp.disconnect_buffer(name) + vim.api.nvim_buf_delete(buffer, {}) + print(" -- detached from buffer " .. name) + end, + { nargs = '?' } +) + +vim.api.nvim_create_user_command( + "Leave", + function (_) + codemp.leave_workspace() + print(" -- left workspace") + end, + {} +) + -- TODO nvim docs say that we should stop all threads before exiting nvim -- but we like to live dangerously (: vim.loop.new_thread({}, function() diff --git a/src/lib.rs b/src/lib.rs index 13f6cf4..a2daa5a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -223,6 +223,18 @@ impl Write for LuaLoggerProducer { fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } +fn disconnect_buffer(_: &Lua, (path,): (String,)) -> LuaResult<()> { + CODEMP_INSTANCE.disconnect_buffer(&path) + .map_err(LuaCodempError::from)?; + Ok(()) +} + +fn leave_workspace(_: &Lua, (): ()) -> LuaResult<()> { + CODEMP_INSTANCE.leave_workspace() + .map_err(LuaCodempError::from)?; + Ok(()) +} + fn setup_tracing(_: &Lua, (debug,): (Option,)) -> LuaResult { let (tx, rx) = mpsc::channel(); let level = if debug.unwrap_or(false) { tracing::Level::DEBUG } else {tracing::Level::INFO }; @@ -259,6 +271,9 @@ fn libcodemp_nvim(lua: &Lua) -> LuaResult { // state helpers exports.set("get_cursor", lua.create_function(get_cursor)?)?; exports.set("get_buffer", lua.create_function(get_buffer)?)?; + // cleanup + exports.set("disconnect_buffer", lua.create_function(disconnect_buffer)?)?; + exports.set("leave_workspace", lua.create_function(leave_workspace)?)?; // debug exports.set("setup_tracing", lua.create_function(setup_tracing)?)?; From 3462851f4a54e6dffe959885146a67cbdb474427 Mon Sep 17 00:00:00 2001 From: alemi Date: Thu, 23 Nov 2023 15:35:10 +0100 Subject: [PATCH 23/23] feat: bump lib version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bf46e33..1d41336 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" crate-type = ["cdylib"] [dependencies] -codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", rev = "c7319b9f68b19b0b34ecf71cc4b00999248bbd14", features = ["global", "sync"] } +codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", rev = "7fc03e3fd936240fdbadd368bfce38832ce34767", features = ["global", "sync"] } mlua = { version = "0.9.0", features = ["module", "luajit"] } thiserror = "1.0.47" derive_more = "0.99.17"