feat: added lua plugin but split into modules

way more understandable now! also everything under :MP command (with
completions!!!)
This commit is contained in:
əlemi 2024-08-06 01:11:09 +02:00
parent d4e4e99dac
commit 2b7b861329
Signed by: alemi
GPG key ID: A4895B84D311642C
7 changed files with 474 additions and 0 deletions

46
src/async.lua Normal file
View file

@ -0,0 +1,46 @@
local function register_controller_handler(workspace, 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()
local ok, res = pcall(handler, event)
if not ok then
print(" !! error running callback handler: " .. res)
end
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, _workspace, _target, _delay)
local _codemp = require("codemp.loader")() -- TODO maybe make a native.load() idk
local _ws = _codemp.get_workspace(_workspace)
local _controller = _target ~= nil and _ws:get_buffer(_target) or _ws.cursor
while true do
local success, _ = pcall(_controller.poll, _controller)
if success then
_async:send()
if _delay ~= nil then vim.loop.sleep(_delay) end
else
local my_name = "cursor"
if _target ~= nil then
my_name = "buffer(" .. _target .. ")"
end
print(" -- stopping " .. my_name .. " controller poller")
break
end
end
end, async, workspace, target, delay)
end
return {
handler = register_controller_handler,
}

102
src/buffer.lua Normal file
View file

@ -0,0 +1,102 @@
local native = require('codemp.loader')()
local utils = require('codemp.utils')
local async = require('codemp.async')
local id_buffer_map = {}
local buffer_id_map = {}
local ticks = {}
local function create(workspace, name, content)
native.get_workspace(workspace):create_buffer(name, content)
print(" ++ created buffer '" .. name .. "' on " .. workspace)
end
local function attach(workspace, name, force)
local buffer = nil
if force then
buffer = vim.api.nvim_get_current_buf()
utils.buffer.set_content(buffer, "")
else
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') -- TODO get from codemp?
vim.api.nvim_buf_set_name(buffer, "codemp::" .. name)
vim.api.nvim_set_current_buf(buffer)
end
local controller = native.get_workspace(workspace):attach_buffer(name)
-- TODO map name to uuid
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
-- local content = table.concat(
-- vim.api.nvim_buf_get_text(buf, start_row, start_col, start_row + new_end_row, start_col + new_end_col, {}),
-- '\n'
-- )
-- print(string.format("%s %s %s %s -- '%s'", start_row, start_col, start_row + new_end_row, start_col + new_end_col, content))
-- controller:send(start_offset, start_offset + old_end_byte_len, content)
controller:send_diff(utils.buffer.get_content(buf))
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
async.handler(workspace, name, controller, function(event)
ticks[buffer] = vim.api.nvim_buf_get_changedtick(buffer)
local before = utils.buffer.get_content(buffer)
local after = event:apply(before)
utils.buffer.set_content(buffer, after)
-- buffer_set_content(buffer, event.content, event.first, event.last)
-- buffer_replace_content(buffer, event.first, event.last, event.content)
end, 20) -- wait 20ms before polling again because it overwhelms libuv?
print(" ++ attached to buffer " .. name)
end
local function detach(workspace, name)
local buffer = buffer_id_map[name]
id_buffer_map[buffer] = nil
buffer_id_map[name] = nil
native.get_workspace(workspace):disconnect_buffer(name)
vim.api.nvim_buf_delete(buffer, {})
print(" -- detached from buffer " .. name)
end
local function sync(workspace)
local buffer = vim.api.nvim_get_current_buf()
local name = id_buffer_map[buffer]
if name ~= nil then
local controller = native.get_workspace(workspace):get_buffer(name)
ticks[buffer] = vim.api.nvim_buf_get_changedtick(buffer)
utils.buffer.set_content(buffer, controller.content)
print(" :: synched buffer " .. name)
else
print(" !! buffer not managed")
end
end
return {
create = create,
sync = sync,
attach = attach,
detach = detach,
map = id_buffer_map,
map_rev = buffer_id_map,
ticks = ticks,
}

11
src/client.lua Normal file
View file

@ -0,0 +1,11 @@
local native = require('codemp.loader')()
local function login(username, password, workspace)
native.login(username, password, workspace)
print(" ++ logged in as '" .. username .. "' on " .. workspace)
end
return {
login = login,
}

98
src/init.lua Normal file
View file

@ -0,0 +1,98 @@
local native = require('codemp.loader')() -- make sure we can load the native library correctly, otherwise no point going forward
local client = require('codemp.client')
local buffers = require('codemp.buffer')
local workspace = require('codemp.workspace')
-- TODO nvim docs say that we should stop all threads before exiting nvim
-- but we like to live dangerously (:
vim.loop.new_thread({}, function()
vim.loop.sleep(500) -- sleep a bit leaving user config time to override logger opts
local _codemp = require('codemp.loader')()
local logger = _codemp.setup_tracing()
while true do
print(logger:recv())
end
end)
local active_workspace = nil -- TODO dont use a single global one!!!
local function filter(needle, haystack)
local hints = {}
for _, opt in pairs(haystack) do
if vim.startswith(opt, needle) then
table.insert(hints, opt)
end
end
return hints
end
vim.api.nvim_create_user_command(
"MP",
function (args)
if args.fargs[1] == "login" then
client.login(args.fargs[2], args.fargs[3], args.fargs[4])
elseif args.fargs[1] == "create" then
if #args.fargs < 2 then error("missing buffer name") end
if active_workspace == nil then error("connect to a workspace first") end
buffers.create(active_workspace, args.fargs[2])
elseif args.fargs[1] == "join" then
if #args.fargs < 2 then error("missing workspace name") end
active_workspace = args.fargs[2]
workspace.join(active_workspace)
elseif args.fargs[1] == "attach" then
if #args.fargs < 2 then error("missing buffer name") end
if active_workspace == nil then error("connect to a workspace first") end
buffers.attach(active_workspace, args.fargs[2], args.bang)
elseif args.fargs[1] == "sync" then
if active_workspace == nil then error("connect to a workspace first") end
buffers.sync(active_workspace)
elseif args.fargs[1] == "buffers" then
if active_workspace == nil then error("connect to a workspace first") end
workspace.buffers(active_workspace)
elseif args.fargs[1] == "users" then
if active_workspace == nil then error("connect to a workspace first") end
workspace.users(active_workspace)
elseif args.fargs[1] == "detach" then
if #args.fargs < 2 then error("missing buffer name") end
if active_workspace == nil then error("connect to a workspace first") end
buffers.detach(active_workspace, args.fargs[2])
elseif args.fargs[1] == "leave" then
if active_workspace == nil then error("connect to a workspace first") end
workspace.leave()
active_workspace = nil
end
end,
{
nargs = "+",
complete = function (lead, cmd, _pos)
local args = vim.split(cmd, " ", { plain = true, trimempty = false })
local stage = #args
if stage == 1 then
return { "MP" }
elseif stage == 2 then
return filter(lead, {'login', 'create', 'join', 'attach', 'sync', 'buffers', 'users', 'detach', 'leave'})
elseif stage == 3 then
if args[#args-1] == 'attach' or args[#args-1] == 'detach' then
if active_workspace ~= nil then
local ws = native.get_workspace(active_workspace)
if ws ~= nil then
return filter(lead, ws.filetree)
end
end
end
return {}
end
end,
}
)
return {
native = native,
client = client,
buffers = buffers,
workspace = workspace,
utils = require('codemp.utils'),
async = require('codemp.async'),
}

6
src/loader.lua Normal file
View file

@ -0,0 +1,6 @@
-- TODO check for platform? download it? check for updates?
local function loader()
return require("libcodemp")
end
return loader

125
src/utils.lua Normal file
View file

@ -0,0 +1,125 @@
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, first, last)
if first == nil and last == nil then
local lines = split_without_trim(content, "\n")
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
else
-- TODO there is no api equivalent of byte2line afaik!
-- this is theoretically a big deal because we can only
-- operate on current buffer, but in practice we may only
-- really change the currently active buffer so this
-- may not matter
local first_row = vim.fn.byte2line(first + 1) - 1
local first_col = first - vim.api.nvim_buf_get_offset(buf, first_row)
local last_row = vim.fn.byte2line(last + 1) - 1
local last_col = last - vim.api.nvim_buf_get_offset(buf, last_row)
vim.api.nvim_buf_set_text(
buf, first_row, first_col, last_row, last_col,
split_without_trim(content, "\n")
)
end
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
return {
split_without_trim = split_without_trim,
order_tuples = order_tuples,
multiline_highlight = multiline_highlight,
cursor = {
position = cursor_position,
},
buffer = {
get_content = buffer_get_content,
set_content = buffer_set_content,
replace_content = buffer_replace_content,
},
}

86
src/workspace.lua Normal file
View file

@ -0,0 +1,86 @@
local native = require('codemp.loader')()
local utils = require('codemp.utils')
local buffers = require('codemp.buffer')
local async = require('codemp.async')
local user_hl = {}
local available_colors = { -- TODO these are definitely not portable!
"ErrorMsg",
"WarningMsg",
"MatchParen",
"SpecialMode",
"CmpItemKindFunction",
"CmpItemKindValue",
"CmpItemKindInterface",
}
local function register_cursor_callback(controller, workspace, buffer)
vim.api.nvim_create_autocmd({"CursorMoved", "CursorMovedI", "ModeChanged"}, {
group = vim.api.nvim_create_augroup("codemp-workspace-" .. workspace, { clear = true }),
callback = function (_)
local cur = utils.cursor.position()
local buf = buffer or vim.api.nvim_get_current_buf()
if buffers.map[buf] ~= nil then
controller:send(buffers.map[buf], cur[1][1], cur[1][2], cur[2][1], cur[2][2])
end
end
})
end
local function register_cursor_handler(controller, workspace)
async.handler(workspace, nil, controller, function(event)
if user_hl[event.user] == nil then
user_hl[event.user] = {
ns = vim.api.nvim_create_namespace("codemp-cursor-" .. event.user),
hi = available_colors[ math.random( #available_colors ) ],
}
end
local buffer = buffers.map_rev[event.position.buffer]
if buffer ~= nil then
vim.api.nvim_buf_clear_namespace(buffer, user_hl[event.user].ns, 0, -1)
utils.multiline_highlight(
buffer,
user_hl[event.user].ns,
user_hl[event.user].hi,
event.position.start,
event.position.finish
)
end
end, 20)
end
local function join(workspace)
local controller = native.join_workspace(workspace)
register_cursor_callback(controller, workspace)
register_cursor_handler(controller, workspace)
print(" ++ joined workspace " .. workspace)
end
local function leave()
native.leave_workspace()
print(" -- left workspace")
end
local function list_users(workspace)
local workspace = native.get_workspace(workspace)
for _, buffer in ipairs(workspace.users) do
print(" - " .. buffer)
end
end
local function list_buffers(workspace)
local workspace = native.get_workspace(workspace)
for _, buffer in ipairs(workspace.filetree) do
print(" > " .. buffer)
end
end
return {
join = join,
leave = leave,
buffers = list_buffers,
users = list_users,
map = user_hl,
colors = available_colors,
}