109 lines
2.6 KiB
TypeScript
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" });
|
|
}
|
|
}
|
|
}
|