Hi Y.js community,
There’s a weird quirk when typing on the last line and only the last 2 characters are synced. Anything typed after that will not be synced. I’m not 100% sure what the cause is
See the video below for a demo of the issue I’m having.
Tech Stack being used:
frontend: React + codemirror + y.js + y-websocket provider (with TypeScript if that matters)
backend: Node.js + custom y-websocket setup that follows the suggestion by @dmonad in this comment (with TypeScript too)
My intention is to use the server to provide the initial value to codemirror. The code that is used is in the following format below and can’t be changed unfortunately:
[
"# Write your code below:\n",
'something = "2"\n',
"something2 = 5\n",
"print(something + something2)\n",
"\n",
"list = []\n",
"for num in range(1,11):\n",
" list.append(num)\n",
"\n",
"print(list)",
"\n",
],
The idea is to join()
the array of lines of code together and then apply it to the doc
Codemirror/frontend component below
import React, { useEffect, useRef, useState } from "react";
import * as Y from "yjs";
import { WebsocketProvider } from "y-websocket";
import { CodemirrorBinding } from "y-codemirror";
import { Controlled as CodeMirror } from "react-codemirror2";
import "codemirror/mode/python/python";
import "codemirror/lib/codemirror.css";
interface CodeMirrorEditorProps {
code: string;
cellId: string;
userId: string;
userName: string;
cursorColor: string;
}
const CodeMirrorEditor = (props: CodeMirrorEditorProps) => {
const [codeMirrorText, setCodeMirrorText] = useState("");
const [hasLineNumber, setHasLineNumber] = useState(true);
const codeMirrorRef = useRef<undefined | CodeMirror.Editor>();
useEffect(() => {
if (!codeMirrorRef.current) return;
// A Yjs document holds the shared data
const ydoc = new Y.Doc({
meta: {
cellId: props.cellId,
},
});
const wsProvider = new WebsocketProvider(
"ws://localhost:1234",
props.cellId,
ydoc
);
// Define a shared text type on the document
const yText = ydoc.getText(`codemirror`);
wsProvider.awareness.setLocalStateField("user", {
name: props.userName,
color: props.cursorColor,
});
// "Bind" the codemirror editor to a Yjs text type.
const _codemirrorBinding = new CodemirrorBinding(
yText,
codeMirrorRef.current,
wsProvider.awareness
);
wsProvider.on("status", (event: { status: string }) => {
console.log(event.status); // logs "connected" or "disconnected"
});
}, [props.cellId, props.cursorColor, props.userName]);
const handleChange = (value: string) => {
console.log("inside handle change: ", value);
setCodeMirrorText(value);
};
return (
<div style={{ marginTop: "48px" }}>
<div>Code Mirror Editor</div>
<button
type="button"
onClick={() => setHasLineNumber((prevState) => !prevState)}
>
Toggle line number
</button>
<CodeMirror
editorDidMount={(editor) => (codeMirrorRef.current = editor)}
value={codeMirrorText}
onBeforeChange={(_editor, _data, value) => {
handleChange(value);
}}
options={{
lineNumbers: hasLineNumber,
mode: "python",
extraKeys: {
"Ctrl-Space": (cm) => {
const { line, ch } = cm.getCursor();
const lineOfCode = cm.getLine(line);
// do some stuff when user hits Ctrl-Space
},
},
}}
/>
</div>
);
};
export default CodeMirrorEditor;
Server side
import { Server as WsServer } from "ws";
import { createServer as createHttpServer } from "http";
import { setupWSConnection, setPersistence } from "y-websocket/bin/utils";
import * as Y from "yjs";
const cells = [
{
metadata: {
id: "1",
},
source: [
"# Write your code below:\n",
'something = "2"\n',
"something2 = 5\n",
"print(something + something2)\n",
"\n",
"list = []\n",
"for num in range(1,11):\n",
" list.append(num)\n",
"\n",
"print(list)",
"\n",
],
},
{
metadata: {
id: "2",
},
source: [
"data = [73284, 8784.3, 9480938.2, 984958.3, 24131, 45789, 734987, 23545.3, 894859.2, 842758.3]\n",
"\n",
"# Write your code below:\n",
"maximum = data[0]\n",
"\n",
"for num in data:\n",
" if(num > maximum):\n",
" maximum = num\n",
"\n",
"print(maximum)",
"\n",
],
},
]
const port = process.env.PORT || 1234;
const wss = new WsServer({ noServer: true });
const server = createHttpServer((_request, response) => {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("okay");
});
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
setPersistence({
bindState: async (documentName: string, doc: any) => {
await delay(1000); // some random delay to signify retrieving data from db
//documentName refers to the id of the cell in the db which the frontend will have
const foundCell = cells.find(
(cell) => cell.metadata.id === documentName
);
const foundSourceCode = foundCell?.source.join("") as string;
const yText = doc.getText("codemirror");
yText.insert(0, foundSourceCode);
const ecodedState = Y.encodeStateAsUpdate(doc);
doc.on("update", (update) => {
Y.applyUpdate(doc, update);
});
return Y.applyUpdate(doc, ecodedState);
// Here you listen to granular document updates and store them in the database
// You don't have to do this, but it ensures that you don't lose content when the server crashes
// See https://github.com/yjs/yjs#Document-Updates for documentation on how to encode
// document updates
},
writeState: (_identifier, _doc) => {
// This is called when all connections to the document are closed.
// In the future, this method might also be called in intervals or after a certain number of updates.
return new Promise<void>((resolve) => {
// When the returned Promise resolves, the document will be destroyed.
// So make sure that the document really has been written to the database.
resolve();
});
},
});
wss.on("connection", setupWSConnection);
server.on("upgrade", (request, socket, head) => {
// You may check auth of request here..
const handleAuth = (ws) => {
wss.emit("connection", ws, request);
};
wss.handleUpgrade(request, socket, head, handleAuth);
});
server.listen(port, () => {
console.log("running on port", port);
});
Hope to hear back soon! thanks!