colabbd/adapters/vim/collab.vim

192 lines
4.5 KiB
VimL

if !has('vim9script') || v:version < 900
finish
endif
vim9script
# collab.vim - collaborative editing adapter for collabd
# requires: bun, collabd daemon running
hlset([{
name: 'PeerCursor',
ctermbg: 'yellow',
ctermfg: 'black',
guibg: 'yellow',
guifg: 'black'
}])
var bridge_job: job = null_job
var bridge_channel: channel = null_channel
var connected = false
var ready = false
var room = ""
var suppressing = false
var peer_match_ids: dict<number> = {}
# path to bridge script (adjust as needed)
const bridge_script = expand('<sfile>:p:h') .. '/bridge.ts'
def Send(msg: dict<any>): void
if bridge_channel != null_channel
ch_sendraw(bridge_channel, json_encode(msg) .. "\n")
endif
enddef
def OnOutput(ch: channel, msg: string): void
if msg->empty()
return
endif
var data: dict<any>
try
data = json_decode(msg)
catch
return
endtry
if data.type == 'ready'
ready = true
elseif data.type == 'connected'
connected = true
echom $'[collab] connected to room: {data.room}'
elseif data.type == 'disconnected'
connected = false
echom '[collab] disconnected'
elseif data.type == 'content'
ApplyRemoteContent(data.text)
elseif data.type == 'peers'
echom $'[collab] peers: {data.count}'
elseif data.type == 'cursor'
ShowPeerCursor(data.data)
elseif data.type == 'error'
echoerr '[collab] ' .. data.message
endif
enddef
def ApplyRemoteContent(content: string): void
if suppressing
return
endif
suppressing = true
var lines = content->split("\n", true)
var view = winsaveview()
silent! :%delete _
setline(1, lines)
winrestview(view)
suppressing = false
enddef
def SendBuffer(): void
if !connected || suppressing
return
endif
var lines = getline(1, '$')
var content = lines->join("\n")
Send({type: 'content', text: content})
enddef
def SendCursor(): void
if !connected
return
endif
const pos = getpos('.')
# pos is [bufnum, line, col, off] - line/col are 1-indexed
Send({type: 'cursor', line: pos[1] - 1, col: pos[2] - 1})
enddef
def ShowPeerCursor(data: dict<any>): void
const client_id = string(data.clientId)
# Clear previous highlight for this peer
if has_key(peer_match_ids, client_id)
silent! matchdelete(peer_match_ids[client_id])
endif
# Highlight the cursor position (line, col are 0-indexed from bridge)
const line_nr = data.line + 1
const col_nr = data.col + 1
# Create a 1-char highlight at cursor position
const pattern = $'\%{line_nr}l\%{col_nr}c.'
peer_match_ids[client_id] = matchadd('PeerCursor', pattern, 10)
enddef
def ClearPeerCursors(): void
for id in values(peer_match_ids)
silent! matchdelete(id)
endfor
peer_match_ids = {}
enddef
export def Connect(room_name: string): void
if bridge_job != null_job
Disconnect()
endif
room = room_name
ready = false
bridge_job = job_start(['bun', 'run', bridge_script], {
mode: 'nl',
out_cb: OnOutput,
err_io: 'null'
})
bridge_channel = job_getchannel(bridge_job)
# wait for bridge ready signal
var timeout = 50
while !ready && timeout > 0
sleep 10m
timeout -= 1
endwhile
if !ready
echoerr '[collab] bridge failed to start'
Disconnect()
return
endif
Send({type: 'connect', room: room_name})
# set up autocmds to send changes
autocmd_add([
{
group: 'CollabVim',
event: ['TextChanged', 'TextChangedI'],
pattern: '*',
cmd: 'SendBuffer()'
},
{
group: 'CollabVim',
event: ['CursorMoved', 'CursorMovedI'],
bufnr: bufnr(),
cmd: 'SendCursor()'
}
])
enddef
export def Disconnect(): void
if bridge_job != null_job
Send({type: 'disconnect'})
job_stop(bridge_job)
bridge_job = null_job
bridge_channel = null_channel
endif
ClearPeerCursors()
connected = false
ready = false
room = ""
autocmd_delete([{group: 'CollabVim'}])
echom '[collab] disconnected'
enddef
export def Status(): void
if connected
echom $'[collab] connected to room: {room}'
else
echom '[collab] not connected'
endif
enddef
# commands
command! -nargs=1 CollabJoin call Connect(<q-args>)
command! CollabLeave call Disconnect()
command! CollabStatus call Status()