colabbd/adapters/vim/collab.vim

165 lines
3.9 KiB
VimL

vim9script
# collab.vim - collaborative editing adapter for collabd
# requires: bun, collabd daemon running
highlight PeerCursor ctermbg=yellow guibg=yellow ctermfg=black 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>)
if bridge_channel != null_channel
ch_sendraw(bridge_channel, json_encode(msg) .. "\n")
endif
enddef
def OnOutput(ch: channel, msg: string)
if empty(msg)
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)
if suppressing
return
endif
suppressing = true
var lines = split(content, "\n", true)
var view = winsaveview()
silent! :%delete _
setline(1, lines)
winrestview(view)
suppressing = false
enddef
def SendBuffer()
if !connected || suppressing
return
endif
var lines = getline(1, '$')
var content = join(lines, "\n")
Send({type: 'content', text: content})
enddef
def ShowPeerCursor(data: dict<any>)
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()
for id in values(peer_match_ids)
silent! matchdelete(id)
endfor
peer_match_ids = {}
enddef
export def Connect(room_name: string)
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
augroup CollabVim
autocmd!
autocmd TextChanged,TextChangedI * call SendBuffer()
augroup END
enddef
export def Disconnect()
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 = ""
augroup CollabVim
autocmd!
augroup END
echom '[collab] disconnected'
enddef
export def Status()
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()