From 9dea608f042b97f4d70a4e0cf296f231f5edcfa0 Mon Sep 17 00:00:00 2001 From: alemi Date: Tue, 17 Sep 2024 16:33:22 +0200 Subject: [PATCH] feat: non-blocking API --- lua/codemp/buffers.lua | 203 +++++++++++++++---------------- lua/codemp/client.lua | 8 +- lua/codemp/command.lua | 17 +-- lua/codemp/neo-tree/commands.lua | 29 +++-- lua/codemp/workspace.lua | 92 +++++++------- 5 files changed, 179 insertions(+), 170 deletions(-) diff --git a/lua/codemp/buffers.lua b/lua/codemp/buffers.lua index 1fb4b65..8ad386c 100644 --- a/lua/codemp/buffers.lua +++ b/lua/codemp/buffers.lua @@ -13,7 +13,6 @@ local ticks = {} ---@param buffer? integer buffer to use for attaching (will clear content) ---@param content? string if provided, set this content after attaching ---@param nowait? boolean skip waiting for initial content sync ----@return BufferController local function attach(name, buffer, content, nowait) if buffer_id_map[name] ~= nil then error("already attached to buffer " .. name) @@ -25,114 +24,113 @@ local function attach(name, buffer, content, nowait) vim.api.nvim_set_option_value('fileformat', 'unix', { buf = buffer }) vim.api.nvim_buf_set_name(buffer, name) - local controller = session.workspace:attach(name):await() - - if not nowait then - local promise = controller:poll() - for i=1, 20, 1 do - if promise.ready then break end - vim.uv.sleep(100) + session.workspace:attach(name):and_then(function (controller) + if not nowait then + local promise = controller:poll() + for i=1, 20, 1 do + if promise.ready then break end + vim.uv.sleep(100) + end end - end - -- TODO map name to uuid + -- TODO map name to uuid - id_buffer_map[buffer] = name - buffer_id_map[name] = buffer - ticks[buffer] = 0 + id_buffer_map[buffer] = name + buffer_id_map[name] = buffer + ticks[buffer] = 0 - -- hook serverbound callbacks - -- TODO breaks when deleting whole lines at buffer end - vim.api.nvim_buf_attach(buffer, false, { - on_bytes = function(_, buf, tick, start_row, start_col, start_offset, old_end_row, old_end_col, old_end_byte_len, new_end_row, new_end_col, new_byte_len) - if tick <= ticks[buf] then return end - if id_buffer_map[buf] == nil then return true end -- unregister callback handler - if CODEMP.config.debug then print(string.format( - "start(row:%s, col:%s) offset:%s end(row:%s, col:%s new(row:%s, col:%s)) len(old:%s, new:%s)", - start_row, start_col, start_offset, old_end_row, old_end_col, new_end_row, new_end_col, old_end_byte_len, new_byte_len - )) end - local end_offset = start_offset + old_end_byte_len - local change_content = "" - local len = utils.buffer.len(buf) - if start_offset + new_byte_len + 1 > len then - -- i dont know why but we may go out of bounds when doing 'dd' at the end of buffer?? - local delta = (start_offset + new_byte_len + 1) - len - if CODEMP.config.debug then print("/!\\ bytes out of bounds by " .. delta .. ", adjusting") end - end_offset = end_offset - delta - start_offset = start_offset - delta - end - if new_byte_len > 0 then - local actual_end_col = new_end_col - if new_end_row == 0 then actual_end_col = new_end_col + start_col end - local actual_end_row = start_row + new_end_row - -- -- when bulk inserting at the end we go out of bounds, so we probably need to clamp? - -- -- issue: row=x+1 col=0 and row=x col=0 may be very far apart! we need to get last col of row x, ughh.. - -- -- also, this doesn't work so it will stay commented out for a while longer - -- if new_end_row ~= old_end_row and new_end_col == 0 then - -- -- we may be dealing with the last line of the buffer, get_text could error because out-of-bounds - -- local row_count = vim.api.nvim_buf_line_count(buf) - -- if actual_end_row + 1 > row_count then - -- local delta = (actual_end_row + 1) - row_count - -- if CODEMP.config.debug then print("/!\\ row out of bounds by " .. delta .. ", adjusting") end - -- actual_end_row = actual_end_row - delta - -- actual_end_col = len - vim.api.nvim_buf_get_offset(buf, row_count) - -- end - -- end - local lines = vim.api.nvim_buf_get_text(buf, start_row, start_col, actual_end_row, actual_end_col, {}) - change_content = table.concat(lines, '\n') - end - if CODEMP.config.debug then - print(string.format("sending: %s..%s '%s'", start_offset, start_offset + old_end_byte_len, change_content)) - end - controller:send({ - start = start_offset, finish = end_offset, content = change_content - }):await() - end, - }) + -- hook serverbound callbacks + -- TODO breaks when deleting whole lines at buffer end + vim.api.nvim_buf_attach(buffer, false, { + on_bytes = function(_, buf, tick, start_row, start_col, start_offset, old_end_row, old_end_col, old_end_byte_len, new_end_row, new_end_col, new_byte_len) + if tick <= ticks[buf] then return end + if id_buffer_map[buf] == nil then return true end -- unregister callback handler + if CODEMP.config.debug then print(string.format( + "start(row:%s, col:%s) offset:%s end(row:%s, col:%s new(row:%s, col:%s)) len(old:%s, new:%s)", + start_row, start_col, start_offset, old_end_row, old_end_col, new_end_row, new_end_col, old_end_byte_len, new_byte_len + )) end + local end_offset = start_offset + old_end_byte_len + local change_content = "" + local len = utils.buffer.len(buf) + if start_offset + new_byte_len + 1 > len then + -- i dont know why but we may go out of bounds when doing 'dd' at the end of buffer?? + local delta = (start_offset + new_byte_len + 1) - len + if CODEMP.config.debug then print("/!\\ bytes out of bounds by " .. delta .. ", adjusting") end + end_offset = end_offset - delta + start_offset = start_offset - delta + end + if new_byte_len > 0 then + local actual_end_col = new_end_col + if new_end_row == 0 then actual_end_col = new_end_col + start_col end + local actual_end_row = start_row + new_end_row + -- -- when bulk inserting at the end we go out of bounds, so we probably need to clamp? + -- -- issue: row=x+1 col=0 and row=x col=0 may be very far apart! we need to get last col of row x, ughh.. + -- -- also, this doesn't work so it will stay commented out for a while longer + -- if new_end_row ~= old_end_row and new_end_col == 0 then + -- -- we may be dealing with the last line of the buffer, get_text could error because out-of-bounds + -- local row_count = vim.api.nvim_buf_line_count(buf) + -- if actual_end_row + 1 > row_count then + -- local delta = (actual_end_row + 1) - row_count + -- if CODEMP.config.debug then print("/!\\ row out of bounds by " .. delta .. ", adjusting") end + -- actual_end_row = actual_end_row - delta + -- actual_end_col = len - vim.api.nvim_buf_get_offset(buf, row_count) + -- end + -- end + local lines = vim.api.nvim_buf_get_text(buf, start_row, start_col, actual_end_row, actual_end_col, {}) + change_content = table.concat(lines, '\n') + end + if CODEMP.config.debug then + print(string.format("sending: %s..%s '%s'", start_offset, start_offset + old_end_byte_len, change_content)) + end + controller:send({ + start = start_offset, finish = end_offset, content = change_content + }):await() + end, + }) - local async = vim.loop.new_async(vim.schedule_wrap(function () - while true do - local event = controller:try_recv():await() - if event == nil then break end - ticks[buffer] = vim.api.nvim_buf_get_changedtick(buffer) - if CODEMP.config.debug then - print(" ~~ applying change ~~ " .. event.start .. ".." .. event.finish .. "::[" .. event.content .. "]") - end - utils.buffer.set_content(buffer, event.content, event.start, event.finish) - if event.hash ~= nil then - if utils.hash(utils.buffer.get_content(buffer)) ~= event.hash then - -- OUT OF SYNC! - -- TODO this may be destructive! we should probably prompt the user before doing this - print(" /!\\ out of sync, resynching...") - utils.buffer.set_content(buffer, controller:content():await()) - return + local async = vim.loop.new_async(vim.schedule_wrap(function () + while true do + local event = controller:try_recv():await() + if event == nil then break end + ticks[buffer] = vim.api.nvim_buf_get_changedtick(buffer) + if CODEMP.config.debug then + print(" ~~ applying change ~~ " .. event.start .. ".." .. event.finish .. "::[" .. event.content .. "]") + end + utils.buffer.set_content(buffer, event.content, event.start, event.finish) + if event.hash ~= nil then + if utils.hash(utils.buffer.get_content(buffer)) ~= event.hash then + -- OUT OF SYNC! + -- TODO this may be destructive! we should probably prompt the user before doing this + print(" /!\\ out of sync, resynching...") + utils.buffer.set_content(buffer, controller:content():await()) + return + end end end + end)) + + local remote_content = controller:content():await() + if content ~= nil then + -- TODO this may happen too soon!! + local _ = controller:send({ + start = 0, finish = #remote_content, content = content + }) -- no need to await + else + local current_content = utils.buffer.get_content(buffer) + if current_content ~= remote_content then + ticks[buffer] = vim.api.nvim_buf_get_changedtick(buffer) + utils.buffer.set_content(buffer, remote_content) + end end - end)) - local remote_content = controller:content():await() - if content ~= nil then - -- TODO this may happen too soon!! - local _ = controller:send({ - start = 0, finish = #remote_content, content = content - }) -- no need to await - else - local current_content = utils.buffer.get_content(buffer) - if current_content ~= remote_content then - ticks[buffer] = vim.api.nvim_buf_get_changedtick(buffer) - utils.buffer.set_content(buffer, remote_content) - end - end + controller:callback(function (_) async:send() end) + vim.defer_fn(function() async:send() end, 500) -- force a try_recv after 500ms - controller:callback(function (_controller) async:send() end) - vim.defer_fn(function() async:send() end, 500) -- force a try_recv after 500ms - - local filetype = vim.filetype.match({ buf = buffer }) - vim.api.nvim_set_option_value("filetype", filetype, { buf = buffer }) - print(" ++ attached to buffer " .. name) - require('codemp.window').update() - return controller + local filetype = vim.filetype.match({ buf = buffer }) + vim.api.nvim_set_option_value("filetype", filetype, { buf = buffer }) + print(" ++ attached to buffer " .. name) + require('codemp.window').update() + end) end ---@param name string @@ -180,9 +178,10 @@ local function create(buffer) if session.workspace == nil then error("join a workspace first") end - session.workspace:create(buffer):await() - print(" ++ created buffer " .. buffer) - require('codemp.window').update() + session.workspace:create(buffer):and_then(function () + print(" ++ created buffer " .. buffer) + require('codemp.window').update() + end) end return { diff --git a/lua/codemp/client.lua b/lua/codemp/client.lua index f4555b7..edf8020 100644 --- a/lua/codemp/client.lua +++ b/lua/codemp/client.lua @@ -8,9 +8,11 @@ local function connect() if CODEMP.config.password == nil then CODEMP.config.password = vim.g.codemp_password or vim.fn.input("password > ", "") end - session.client = CODEMP.native.connect(CODEMP.config):await() - require('codemp.window').update() - vim.schedule(function () workspace.list() end) + CODEMP.native.connect(CODEMP.config):and_then(function (client) + session.client = client + require('codemp.window').update() + workspace.list() + end) end return { diff --git a/lua/codemp/command.lua b/lua/codemp/command.lua index 565d7f6..b3a611b 100644 --- a/lua/codemp/command.lua +++ b/lua/codemp/command.lua @@ -50,9 +50,10 @@ local connected_actions = { start = function(ws) if ws == nil then error("missing workspace name") end - session.client:create_workspace(ws):await() - vim.schedule(function () workspace.list() end) - print(" <> created workspace " .. ws) + session.client:create_workspace(ws):and_then(function () + print(" <> created workspace " .. ws) + workspace.list() + end) end, available = function() @@ -71,8 +72,9 @@ local connected_actions = { else ws = vim.fn.input("workspace > ", "") end - session.client:invite_to_workspace(ws, user):await() - print(" ][ invited " .. user .. " to workspace " .. ws) + session.client:invite_to_workspace(ws, user):and_then(function () + print(" :: invited " .. user .. " to workspace " .. ws) + end) end, disconnect = function() @@ -108,8 +110,9 @@ local joined_actions = { delete = function(path) if path == nil then error("missing buffer name") end - session.workspace:delete(path):await() - print(" xx deleted buffer " .. path) + session.workspace:delete(path):and_then(function() + print(" xx deleted buffer " .. path) + end) end, buffers = function() diff --git a/lua/codemp/neo-tree/commands.lua b/lua/codemp/neo-tree/commands.lua index 97df27a..02e364d 100644 --- a/lua/codemp/neo-tree/commands.lua +++ b/lua/codemp/neo-tree/commands.lua @@ -116,18 +116,20 @@ M.delete = function(state, path, extra) vim.ui.input({ prompt = "delete buffer '" .. selected.name .. "'?" }, function (input) if input == nil then return end if not vim.startswith("y", string.lower(input)) then return end - session.workspace:delete(selected.name):await() - print("deleted buffer " .. selected.name) - manager.refresh("codemp") + session.workspace:delete(selected.name):and_then(function () + print("deleted buffer " .. selected.name) + manager.refresh("codemp") + end) end) elseif selected.type == "workspace" then if session.client == nil then error("connect to server first") end vim.ui.input({ prompt = "delete buffer '" .. selected.name .. "'?" }, function (input) if input == nil then return end if not vim.startswith("y", string.lower(input)) then return end - session.client:delete_workspace(selected.name):await() - print("deleted workspace " .. selected.name) - manager.refresh("codemp") + session.client:delete_workspace(selected.name):and_then(function () + print("deleted workspace " .. selected.name) + manager.refresh("codemp") + end) end) end end @@ -138,21 +140,24 @@ M.add = function(state, path, extra) if vim.startswith(selected.name, "#") then vim.ui.input({ prompt = "new buffer path" }, function(input) if input == nil or input == "" then return end - session.workspace:create(input):await() - manager.refresh("codemp") + session.workspace:create(input):and_then(function () + manager.refresh("codemp") + end) end) elseif selected.name == "workspaces" then vim.ui.input({ prompt = "new workspace name" }, function(input) if input == nil or input == "" then return end - session.client:create_workspace(input):await() - vim.schedule(function () require('codemp.workspace').list() end) + session.client:create_workspace(input):and_then(function () + require('codemp.workspace').list() + end) end) end elseif selected.type == "workspace" then vim.ui.input({ prompt = "user name to invite" }, function(input) if input == nil or input == "" then return end - session.client:invite_to_workspace(selected.name, input):await() - print("invited user " .. input .. " to workspace " .. selected.name) + session.client:invite_to_workspace(selected.name, input):and_then(function () + print("invited user " .. input .. " to workspace " .. selected.name) + end) end) end manager.refresh("codemp") diff --git a/lua/codemp/workspace.lua b/lua/codemp/workspace.lua index 92db562..ced1801 100644 --- a/lua/codemp/workspace.lua +++ b/lua/codemp/workspace.lua @@ -12,22 +12,24 @@ local user_hl = {} local function fetch_workspaces_list() local new_list = {} - local owned = session.client:list_workspaces(true, false):await() - for _, ws in pairs(owned) do - table.insert(new_list, { - name = ws, - owned = true, - }) - end - local invited = session.client:list_workspaces(false, true):await() - for _, ws in pairs(invited) do - table.insert(new_list, { - name = ws, - owned = false, - }) - end - session.available = new_list - require('codemp.window').update() + session.client:list_workspaces(true, false):and_then(function (owned) + for _, ws in pairs(owned) do + table.insert(new_list, { + name = ws, + owned = true, + }) + end + session.client:list_workspaces(false, true):and_then(function (invited) + for _, ws in pairs(invited) do + table.insert(new_list, { + name = ws, + owned = false, + }) + end + session.available = new_list + require('codemp.window').update() + end) + end) end ---@param ws Workspace @@ -105,42 +107,40 @@ local function register_cursor_handler(ws) end ---@param workspace string workspace name to join ----@return Workspace ---join a workspace and register event handlers local function join(workspace) - local ws = session.client:join_workspace(workspace):await() - print(" >< joined workspace " .. ws.name) - register_cursor_callback(ws) - register_cursor_handler(ws) - utils.poller( - function() return ws:event() end, - function(event) - if event.type == "leave" then - if buffers.users[event.value] ~= nil then - local buf_name = buffers.users[event.value] - local buf_id = buffers.map_rev[buf_name] - if buf_id ~= nil then - vim.api.nvim_buf_clear_namespace(buf_id, user_hl[event.value].ns, 0, -1) + session.client:join_workspace(workspace):and_then(function (ws) + print(" >< joined workspace " .. ws.name) + register_cursor_callback(ws) + register_cursor_handler(ws) + utils.poller( + function() return ws:event() end, + function(event) + if event.type == "leave" then + if buffers.users[event.value] ~= nil then + local buf_name = buffers.users[event.value] + local buf_id = buffers.map_rev[buf_name] + if buf_id ~= nil then + vim.api.nvim_buf_clear_namespace(buf_id, user_hl[event.value].ns, 0, -1) + end + buffers.users[event.value] = nil + user_hl[event.value] = nil end - buffers.users[event.value] = nil - user_hl[event.value] = nil + elseif event.type == "join" then + buffers.users[event.value] = "" + user_hl[event.value] = { + ns = vim.api.nvim_create_namespace("codemp-cursor-" .. event.value), + hi = utils.color(event.value), + pos = { 0, 0 }, + } end - elseif event.type == "join" then - buffers.users[event.value] = "" - user_hl[event.value] = { - ns = vim.api.nvim_create_namespace("codemp-cursor-" .. event.value), - hi = utils.color(event.value), - pos = { 0, 0 }, - } + require('codemp.window').update() end - require('codemp.window').update() - end - ) + ) - session.workspace = ws - require('codemp.window').update() - - return ws + session.workspace = ws + require('codemp.window').update() + end) end local function leave()