mirror of
https://github.com/hexedtech/codemp-nvim.git
synced 2024-12-23 22:04:52 +01:00
feat: added lua plugin but split into modules
way more understandable now! also everything under :MP command (with completions!!!)
This commit is contained in:
parent
d4e4e99dac
commit
2b7b861329
7 changed files with 474 additions and 0 deletions
46
src/async.lua
Normal file
46
src/async.lua
Normal 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
102
src/buffer.lua
Normal 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
11
src/client.lua
Normal 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
98
src/init.lua
Normal 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
6
src/loader.lua
Normal 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
125
src/utils.lua
Normal 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
86
src/workspace.lua
Normal 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,
|
||||
}
|
Loading…
Reference in a new issue