colabbd/adapters/vim/bridge.ts

109 lines
2.6 KiB
TypeScript

#!/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" });
}
}
}