local utils = require('codemp.utils') local buffers = require('codemp.buffers') ---@class UserHighlight ---@field ns integer namespace to use for this user ---@field hi HighlightPair color for user to use ---@field mark integer | nil extmark id ---@field pos [integer, integer] cursor start position of this user ---@type table<string, UserHighlight> local user_hl = {} local function fetch_workspaces_list() local new_list = {} CODEMP.client:fetch_owned_workspaces():and_then(function (owned) for _, ws in pairs(owned) do table.insert(new_list, { name = ws, owned = true, }) end CODEMP.client:fetch_joined_workspaces():and_then(function (invited) for _, ws in pairs(invited) do table.insert(new_list, { name = ws, owned = false, }) end CODEMP.available = new_list require('codemp.window').update() end) end) end local last_jump = { 0, 0 } local workspace_callback_group = nil ---@param controller CursorController ---@param name string local function register_cursor_callback(controller, name) local once = true workspace_callback_group = vim.api.nvim_create_augroup("codemp-workspace-" .. name, { clear = true }) vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI", "ModeChanged"}, { group = workspace_callback_group, callback = function (_ev) if CODEMP.ignore_following_action then CODEMP.ignore_following_action = false return elseif CODEMP.following ~= nil then print(" / / unfollowing " .. CODEMP.following) CODEMP.following = nil require('codemp.window').update() end local cur = utils.cursor.position() local buf = vim.api.nvim_get_current_buf() local bufname if buffers.map[buf] ~= nil then bufname = buffers.map[buf] once = true local _ = controller:send({ buffer = bufname, start_row = cur[1][1], start_col = cur[1][2], end_row = cur[2][1], end_col = cur[2][2], }) else -- set ourselves "away" only once bufname = "" if once then controller:send({ buffer = bufname, start_row = 0, start_col = 0, end_row = 1, end_col = 0, }) end once = false end local oldbuf = buffers.users[CODEMP.client:current_user().name] buffers.users[CODEMP.client:current_user().name] = bufname if oldbuf ~= bufname then require('codemp.window').update() end end }) end ---@param controller CursorController local function register_cursor_handler(controller) 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 local user = event.user -- do it on separate line so language server understands that it wont be nil if user ~= nil then if user_hl[user] == nil then user_hl[user] = { ns = vim.api.nvim_create_namespace("codemp-cursor-" .. event.user), hi = utils.color(event.user), mark = nil, pos = { 0, 0 }, } end user_hl[user].pos = { event.sel.start_row, event.sel.start_col } local old_buffer = buffers.users[event.user] if old_buffer ~= nil then local old_buffer_id = buffers.map_rev[old_buffer] if old_buffer_id ~= nil then vim.api.nvim_buf_clear_namespace(old_buffer_id, user_hl[event.user].ns, 0, -1) end end buffers.users[event.user] = event.sel.buffer local buffer_id = buffers.map_rev[event.sel.buffer] buffers.cursors[event.user] = event if buffer_id ~= nil then local hl = user_hl[event.user] user_hl[event.user].mark = utils.cursor.draw(event, buffer_id, hl.ns, hl.mark, hl.hi) end if old_buffer ~= event.sel.buffer then require('codemp.window').update() -- redraw user positions end if CODEMP.following ~= nil and CODEMP.following == event.user then local buf_id = buffers.map_rev[event.sel.buffer] if buf_id ~= nil then local win = vim.api.nvim_get_current_win() local curr_buf = vim.api.nvim_get_current_buf() CODEMP.ignore_following_action = true if curr_buf ~= buf_id then vim.api.nvim_win_set_buf(win, buf_id) end -- keep centered the cursor end that is currently being moved, but prefer start if event.sel.start_row == last_jump[1] and event.sel.start_col == last_jump[2] then vim.api.nvim_win_set_cursor(win, { event.sel.end_row + 1, event.sel.end_col }) else vim.api.nvim_win_set_cursor(win, { event.sel.start_row + 1, event.sel.start_col }) end last_jump = { event.sel.start_row, event.sel.start_col } end end end end end)) controller:callback(function (_controller) async:send() end) end local events_poller = nil ---@param workspace string workspace name to join ---join a workspace and register event handlers local function join(workspace) print(" <> joining workspace " .. workspace .. " ...") CODEMP.client:attach_workspace(workspace):and_then(function (ws) print(" >< joined workspace " .. ws:id()) register_cursor_callback(ws:cursor(), ws:id()) register_cursor_handler(ws:cursor()) CODEMP.workspace = ws vim.schedule(function () for _, user in pairs(CODEMP.workspace:user_list()) do buffers.users[user.name] = "" user_hl[user.name] = { ns = vim.api.nvim_create_namespace("codemp-cursor-" .. user.name), hi = utils.color(user.name), pos = { 0, 0 }, mark = nil, } end require('codemp.window').update() end) local async = vim.uv.new_async(function () while true do local event = ws:try_recv():await() if event == nil then break end if event.type == "UserLeave" then if buffers.users[event.name] ~= nil then vim.schedule(function() local buf_name = buffers.users[event.name] local buf_id = buffers.map_rev[buf_name] if buf_id ~= nil then vim.api.nvim_buf_clear_namespace(buf_id, user_hl[event.name].ns, 0, -1) end buffers.users[event.name] = nil user_hl[event.name] = nil end) end elseif event.type == "UserJoin" then vim.schedule(function() buffers.users[event.name] = "" user_hl[event.name] = { ns = vim.api.nvim_create_namespace("codemp-cursor-" .. event.name), hi = utils.color(event.name), pos = { 0, 0 }, mark = nil, } end) end end vim.schedule(function () require('codemp.window').update() end) end) ws:callback(function(_) async:send() end) end) end local function leave() local ws_name = CODEMP.workspace:id() CODEMP.workspace:cursor():clear_callback() vim.api.nvim_clear_autocmds({ group = workspace_callback_group }) for id, name in pairs(buffers.map) do CODEMP.workspace:get_buffer(name):clear_callback() buffers.map[id] = nil buffers.map_rev[name] = nil end for user, _buf in pairs(buffers.users) do buffers.users[user] = nil end CODEMP.workspace = nil if events_poller ~= nil then events_poller:stop() events_poller = nil end if not CODEMP.client:leave_workspace(ws_name) then collectgarbage("collect") -- TODO codemp disconnects when all references to its objects are dropped. since it -- hands out Arc<> of things, all references still not garbage collected in Lua will -- prevent it from disconnecting. while running a full cycle may be a bit slow, this -- only happens when manually requested, and it's not like the extra garbage collection -- is an effort for nothing... still it would be more elegant to not need this!! end print(" -- left workspace " .. ws_name) require('codemp.window').update() end return { join = join, leave = leave, map = user_hl, list = fetch_workspaces_list, }