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 diff --git a/Cargo.toml b/Cargo.toml index 88d5b81..1d41336 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,15 @@ [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", rev = "7fc03e3fd936240fdbadd368bfce38832ce34767", features = ["global", "sync"] } +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/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/codemp.lua b/src/codemp.lua new file mode 100644 index 0000000..8a7145f --- /dev/null +++ b/src/codemp.lua @@ -0,0 +1,303 @@ +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, delay) + local async = vim.loop.new_async(function() + while true do + 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 + -- 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, _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 + 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 + +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] } } + 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" 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) + return order_tuples({ { cur[1]-1, cur[2] }, { cur[1]-1, cur[2]+1 } }) + 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 = 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) + 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 + end +end + +local buffer_mappings = {} +local buffer_mappings_reverse = {} -- TODO maybe not??? +local user_mappings = {} +local available_colors = { -- TODO these are definitely not portable! + "ErrorMsg", + "WarningMsg", + "MatchParen", + "SpecialMode", + "CmpItemKindFunction", + "CmpItemKindValue", + "CmpItemKindInterface", +} + +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 + 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() + 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 + }) + + -- hook clientbound callbacks + register_controller_handler(nil, controller, function(event) + 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 + 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( + buffer, + user_mappings[event.user].ns, + user_mappings[event.user].hi, + event.position.start, + event.position.finish + ) + end + end) + + print(" ++ joined workspace " .. args.args) + end, + { 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) + 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 + + 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 + if new_lastline < lastline then old_byte_size = old_byte_size + 1 end + controller:send(start, start + old_byte_size - 1, content) + 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, 500) -- delay by 200 ms as ugly fix + + 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() + local _codemp = require("libcodemp_nvim") + local logger = _codemp.setup_tracing() + while true do + print(logger:recv()) + end +end) + +return { + lib = codemp, + utils = { + buffer = buffer_get_content, + cursor = cursor_position, + split = split_without_trim, + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a2daa5a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,281 @@ +use std::io::Write; +use std::sync::{Arc, Mutex, mpsc}; + +use codemp::prelude::*; +use codemp::woot::crdt::Op; +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) + } +} + +// 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, + start: Some(CodempRowCol { + row: start_row, col: start_col, + }), + end: Some(CodempRowCol { + row: end_row, col: end_col, + }), + } +} + +#[derive(Debug, derive_more::From)] +struct LuaOp(Op); +impl LuaUserData for LuaOp { } + +/// 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(()) +} + +fn get_cursor(_: &Lua, _args: ()) -> LuaResult { + Ok( + CODEMP_INSTANCE.get_cursor() + .map_err(LuaCodempError::from)? + .into() + ) +} + +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) + .map_err(LuaCodempError::from)?; + Ok(LuaCursorController(controller)) +} + +#[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_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)?) + }); + methods.add_method("try_recv", |_, this, ()| { + match this.0.try_recv() .map_err(LuaCodempError::from)? { + Some(x) => Ok(Some(LuaCursorEvent(x))), + None => Ok(None), + } + }); + methods.add_method("poll", |_, this, ()| { + CODEMP_INSTANCE.rt().block_on(this.0.poll()) + .map_err(LuaCodempError::from)?; + Ok(()) + }); + } +} + +#[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 +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(Debug, derive_more::From)] +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.0))); + methods.add_method("send", |_, this, (start, end, text): (usize, usize, String)| { + Ok( + this.0.send( + CodempTextChange { + span: start..end, + content: text, + } + ) + .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))), + None => Ok(None), + } + }); + methods.add_method("poll", |_, this, ()| { + CODEMP_INSTANCE.rt().block_on(this.0.poll()) + .map_err(LuaCodempError::from)?; + Ok(()) + }); + } + + fn add_fields<'lua, F: LuaUserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("content", |_, this| Ok( + this.0.try_recv().map(|x| x.map(|y| y.content)) + .map_err(LuaCodempError::from)? + )); + } +} + +#[derive(Debug, 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("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) { + methods.add_meta_function(LuaMetaMethod::Call, |_, (start, end, txt): (usize, usize, String)| { + Ok(LuaTextChange(CodempTextChange { + span: start..end, + content: txt, + })) + }); + methods.add_meta_method(LuaMetaMethod::ToString, |_, this, ()| Ok(format!("{:?}", this.0))); + } +} + +#[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)); + } +} + + + +// setup library logging to file +#[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 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 }; + 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(LuaLoggerProducer(tx))) + .init(); + Ok(LuaLogger(Arc::new(Mutex::new(rx)))) +} + + + +// define module and exports +#[mlua::lua_module] +fn libcodemp_nvim(lua: &Lua) -> LuaResult { + let exports = lua.create_table()?; + + // 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)?)?; + // 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)?)?; + + 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(()) -}