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 = {} # path to bridge script (adjust as needed) const bridge_script = expand(':p:h') .. '/bridge.ts' def Send(msg: dict): void if bridge_channel != null_channel ch_sendraw(bridge_channel, json_encode(msg) .. "\n") endif enddef def OnOutput(ch: channel, msg: string): void if empty(msg) return endif var data: dict 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 = split(content, "\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 = join(lines, "\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): 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 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() command! CollabLeave call Disconnect() command! CollabStatus call Status()