179 lines
4.3 KiB
VimL
179 lines
4.3 KiB
VimL
if !has('vim9script') || v:version < 900
|
|
finish
|
|
endif
|
|
|
|
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>): 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
|
|
augroup CollabVim
|
|
autocmd!
|
|
autocmd TextChanged,TextChangedI * call SendBuffer()
|
|
autocmd CursorMoved,CursorMovedI <buffer> call SendCursor()
|
|
augroup END
|
|
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 = ""
|
|
augroup CollabVim
|
|
autocmd!
|
|
augroup END
|
|
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()
|