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)) +}