Add realtime Yjs collaboration for cloud-linked files

Claude-Session: https://claude.ai/code/session_01PoLmSR1pFVyHf8i5b1rNWk
This commit is contained in:
2026-07-21 15:55:46 -04:00
parent 566bf1a6cb
commit a16edeb876
9 changed files with 290 additions and 3 deletions
+33 -1
View File
@@ -28,6 +28,9 @@
} from "@codemirror/autocomplete";
import { lintGutter, setDiagnostics } from "@codemirror/lint";
import { LSPClient, languageServerExtensions } from "@codemirror/lsp-client";
import { yCollab } from "y-codemirror.next";
import type { Awareness } from "y-protocols/awareness";
import type * as Y from "yjs";
import { typstCompletions } from "$lib/ts/completions";
import { editorTheme } from "$lib/ts/editor-theme";
@@ -41,6 +44,7 @@
targetPath: string;
enableLsp?: boolean;
diagnostics?: Diagnostic[];
collab?: { text: Y.Text; awareness: Awareness } | null;
onchange: (value: string) => void;
onsave: () => void;
onlspstatus?: (status: "off" | "starting" | "on" | "unavailable") => void;
@@ -53,6 +57,7 @@
targetPath,
enableLsp = true,
diagnostics = [],
collab = null,
onchange,
onsave,
onlspstatus,
@@ -65,6 +70,7 @@
const languageSlot = new Compartment();
const lspSlot = new Compartment();
const themeSlot = new Compartment();
const collabSlot = new Compartment();
const bridge = new LspBridge();
let client: LSPClient | null = null;
@@ -158,6 +164,7 @@
languageSlot.of(isToml ? StreamLanguage.define(toml) : []),
lspSlot.of([]),
themeSlot.of(editorTheme(app.theme === "dark")),
collabSlot.of([]),
...(isToml
? []
: [autocompletion({ override: [typstCompletions] })]),
@@ -209,9 +216,34 @@
});
}
let boundCollab: { text: Y.Text; awareness: Awareness } | null = null;
$effect(() => {
const next = collab;
if (next === boundCollab) return;
laterDispatch((current) => {
if (next) {
current.dispatch({
changes: {
from: 0,
to: current.state.doc.length,
insert: next.text.toString(),
},
effects: collabSlot.reconfigure([
yCollab(next.text, next.awareness),
]),
});
} else {
current.dispatch({ effects: collabSlot.reconfigure([]) });
}
boundCollab = next;
});
});
$effect(() => {
const next = content;
if (!view) return;
if (!view || collab) return;
if (view.state.doc.toString() === next) return;
laterDispatch((current) => {