#!/usr/bin/env bun // bridge between vim (stdin/stdout json lines) and collabd (websocket) // vim spawns this process and communicates via channels import * as Y from "yjs"; const DAEMON_URL = process.env.COLLABD_URL || "ws://localhost:4040/ws"; let ws: WebSocket | null = null; let doc: Y.Doc | null = null; let text: Y.Text | null = null; let room: string | null = null; let suppressLocal = false; function send(msg: object) { console.log(JSON.stringify(msg)); } function connect(roomName: string) { room = roomName; doc = new Y.Doc(); text = doc.getText("content"); // when remote changes come in, notify vim text.observe(() => { if (!suppressLocal) { send({ type: "content", text: text?.toString() || "" }); } }); ws = new WebSocket(DAEMON_URL); ws.onopen = () => { ws?.send(JSON.stringify({ type: "join", room: roomName })); send({ type: "connected", room: roomName }); }; ws.onmessage = (ev) => { const msg = JSON.parse(ev.data.toString()); switch (msg.type) { case "sync": case "update": { if (!doc) break; suppressLocal = true; Y.applyUpdate(doc, new Uint8Array(msg.data)); suppressLocal = false; send({ type: "content", text: text?.toString() || "" }); break; } case "peers": { send({ type: "peers", count: msg.count }); break; } } }; ws.onclose = () => { send({ type: "disconnected" }); }; ws.onerror = (err) => { send({ type: "error", message: "websocket error" }); }; } function setContent(newContent: string) { if (!doc || !text || !ws) return; const oldContent = text.toString(); if (oldContent === newContent) return; // compute diff and apply // simple approach: delete all, insert all // TODO: proper diff for efficiency const t = text; doc.transact(() => { t.delete(0, t.length); t.insert(0, newContent); }); // send update to daemon const update = Y.encodeStateAsUpdate(doc); ws.send(JSON.stringify({ type: "update", data: Array.from(update) })); } // read json lines from stdin const decoder = new TextDecoder(); for await (const chunk of Bun.stdin.stream()) { const lines = decoder.decode(chunk).trim().split("\n"); for (const line of lines) { if (!line) continue; try { const msg = JSON.parse(line); switch (msg.type) { case "connect": connect(msg.room); break; case "content": setContent(msg.text); break; case "disconnect": ws?.close(); break; } } catch { send({ type: "error", message: "invalid json" }); } } }