diff --git a/backend/README.md b/backend/README.md index bb3dc6ba1c..ea15cc12e4 100644 --- a/backend/README.md +++ b/backend/README.md @@ -2,11 +2,19 @@ > Before proceeding to each microservice for more instructions: -1. Set up cloud MongoDB if not using docker. We recommend this if you are just testing out each microservice separately to avoid needing to manually set up multiple instances of local MongoDB. Else, if you are using docker-compose.yml to run PeerPrep, check out the READMEs in the different backend microservices to set up the env for the local MongoDB instances. +1. Set up cloud MongoDB if you are not using Docker. We recommend this if you are just testing out each microservice separately to avoid needing to manually set up multiple instances of local MongoDB. Otherwise, if you are using `docker-compose.yml` to run PeerPrep, check out the READMEs in the different backend microservices to set up the `.env` files for the local MongoDB instances. 2. Set up Firebase. -3. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. +3. For the microservices that use Redis, to view the contents stored: + + 1. Go to [http://localhost:5540](http://localhost:5540). + + 2. Click on "Add Redis Database". + + 3. Enter `host.docker.internal` as the Host. + +4. Follow the instructions [here](https://nodejs.org/en/download/package-manager) to set up Node v20. ## Setting-up cloud MongoDB (in production) diff --git a/backend/collab-service/package-lock.json b/backend/collab-service/package-lock.json index 2fc49f9eb3..009a4e8cca 100644 --- a/backend/collab-service/package-lock.json +++ b/backend/collab-service/package-lock.json @@ -9,6 +9,8 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@codemirror/collab": "^6.1.1", + "@codemirror/state": "^6.4.1", "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", @@ -17,7 +19,9 @@ "redis": "^4.7.0", "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", - "yaml": "^2.6.0" + "y-protocols": "^1.0.6", + "yaml": "^2.6.0", + "yjs": "^13.6.20" }, "devDependencies": { "@eslint/js": "^9.13.0", @@ -704,6 +708,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@codemirror/collab": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/collab/-/collab-6.1.1.tgz", + "integrity": "sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==", + "dependencies": { + "@codemirror/state": "^6.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -4642,6 +4659,15 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -5485,6 +5511,26 @@ "node": ">= 0.8.0" } }, + "node_modules/lib0": { + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7210,6 +7256,25 @@ } } }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -7268,6 +7333,22 @@ "node": ">=12" } }, + "node_modules/yjs": { + "version": "13.6.20", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", + "integrity": "sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==", + "dependencies": { + "lib0": "^0.2.98" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/backend/collab-service/package.json b/backend/collab-service/package.json index 8660a0f62b..5529171a82 100644 --- a/backend/collab-service/package.json +++ b/backend/collab-service/package.json @@ -14,6 +14,8 @@ "license": "ISC", "description": "", "dependencies": { + "@codemirror/collab": "^6.1.1", + "@codemirror/state": "^6.4.1", "axios": "^1.7.7", "body-parser": "^1.20.3", "cors": "^2.8.5", @@ -22,7 +24,9 @@ "redis": "^4.7.0", "socket.io": "^4.8.1", "swagger-ui-express": "^5.0.1", - "yaml": "^2.6.0" + "y-protocols": "^1.0.6", + "yaml": "^2.6.0", + "yjs": "^13.6.20" }, "devDependencies": { "@eslint/js": "^9.13.0", diff --git a/backend/collab-service/src/handlers/websocketHandler.ts b/backend/collab-service/src/handlers/websocketHandler.ts index 5a806a70d4..1c98b93305 100644 --- a/backend/collab-service/src/handlers/websocketHandler.ts +++ b/backend/collab-service/src/handlers/websocketHandler.ts @@ -1,72 +1,170 @@ import { Socket } from "socket.io"; import { io } from "../server"; import redisClient from "../config/redis"; +import { Doc, applyUpdateV2, encodeStateAsUpdateV2 } from "yjs"; enum CollabEvents { // Receive JOIN = "join", - CHANGE = "change", LEAVE = "leave", DISCONNECT = "disconnect", + INIT_DOCUMENT = "init_document", + UPDATE_REQUEST = "update_request", + UPDATE_CURSOR_REQUEST = "update_cursor_request", + RECONNECT_REQUEST = "reconnect_request", // Send - ROOM_FULL = "room_full", - CONNECTED = "connected", - NEW_USER_CONNECTED = "new_user_connected", - CODE_CHANGE = "code_change", - PARTNER_LEFT = "partner_left", - PARTNER_DISCONNECTED = "partner_disconnected", + ROOM_READY = "room_ready", + DOCUMENT_READY = "document_ready", + UPDATE = "updateV2", + UPDATE_CURSOR = "update_cursor", + // PARTNER_LEFT = "partner_left", + // PARTNER_DISCONNECTED = "partner_disconnected", } const EXPIRY_TIME = 3600; +const CONNECTION_DELAY = 3000; // time window to allow for page re-renders / refresh + +const userConnections = new Map(); +const collabSessions = new Map(); +const partnerReadiness = new Map(); export const handleWebsocketCollabEvents = (socket: Socket) => { - socket.on(CollabEvents.JOIN, async ({ roomId }) => { - if (!roomId) { + socket.on(CollabEvents.JOIN, async (uid: string, roomId: string) => { + const connectionKey = `${uid}:${roomId}`; + if (userConnections.has(connectionKey)) { + clearTimeout(userConnections.get(connectionKey)!); return; } + userConnections.set(connectionKey, null); const room = io.sockets.adapter.rooms.get(roomId); - if (room && room.size >= 2) { - socket.emit(CollabEvents.ROOM_FULL); + if (room && room?.size >= 2) { + socket.emit(CollabEvents.ROOM_READY, false); return; } socket.join(roomId); socket.data.roomId = roomId; - // in case of disconnect, send the code to the user when he rejoins - const code = await redisClient.get(`collaboration:${roomId}`); - socket.emit(CollabEvents.CONNECTED, { code: code ? code : "" }); + if ( + io.sockets.adapter.rooms.get(roomId)?.size === 2 && + !collabSessions.has(roomId) + ) { + createCollabSession(roomId); + io.to(roomId).emit(CollabEvents.ROOM_READY, true); + } + }); + + socket.on(CollabEvents.INIT_DOCUMENT, (roomId: string, template: string) => { + const doc = getDocument(roomId); + const isPartnerReady = partnerReadiness.get(roomId); - // inform the other user that a new user has joined - socket.to(roomId).emit(CollabEvents.NEW_USER_CONNECTED); + if (isPartnerReady && doc.getText().length === 0) { + doc.transact(() => { + doc.getText().insert(0, template); + }); + io.to(roomId).emit(CollabEvents.DOCUMENT_READY); + } else { + partnerReadiness.set(roomId, true); + } }); - socket.on(CollabEvents.CHANGE, async ({ roomId, code }) => { - if (!roomId || !code) { - return; + socket.on( + CollabEvents.UPDATE_REQUEST, + (roomId: string, update: Uint8Array) => { + const doc = collabSessions.get(roomId); + if (doc) { + applyUpdateV2(doc, new Uint8Array(update)); + } else { + // TODO: error handling + } } + ); - await redisClient.set(`collaboration:${roomId}`, code, { - EX: EXPIRY_TIME, - }); - socket.to(roomId).emit(CollabEvents.CODE_CHANGE, { code }); - }); + socket.on( + CollabEvents.UPDATE_CURSOR_REQUEST, + ( + roomId: string, + cursor: { uid: string; username: string; from: number; to: number } + ) => { + socket.to(roomId).emit(CollabEvents.UPDATE_CURSOR, cursor); + } + ); - socket.on(CollabEvents.LEAVE, ({ roomId }) => { - if (!roomId) { + socket.on(CollabEvents.LEAVE, (uid: string, roomId: string) => { + const connectionKey = `${uid}:${roomId}`; + if (!userConnections.has(connectionKey)) { return; } - socket.leave(roomId); - socket.to(roomId).emit(CollabEvents.PARTNER_LEFT); + clearTimeout(userConnections.get(connectionKey)!); + + const connectionTimeout = setTimeout(() => { + userConnections.delete(connectionKey); + socket.leave(roomId); + socket.disconnect(); + + const room = io.sockets.adapter.rooms.get(roomId); + if (!room || room.size === 0) { + removeCollabSession(roomId); + } + }, CONNECTION_DELAY); + + userConnections.set(connectionKey, connectionTimeout); }); - socket.on(CollabEvents.DISCONNECT, () => { - const { roomId } = socket.data; - if (roomId) { - socket.to(roomId).emit(CollabEvents.PARTNER_DISCONNECTED); + socket.on(CollabEvents.RECONNECT_REQUEST, async (roomId: string) => { + // TODO: Handle recconnection + socket.join(roomId); + + const doc = getDocument(roomId); + const storeData = await redisClient.get(`collaboration:${roomId}`); + + if (storeData) { + const tempDoc = new Doc(); + const update = Buffer.from(storeData, "base64"); + applyUpdateV2(tempDoc, new Uint8Array(update)); + const tempText = tempDoc.getText().toString(); + + const text = doc.getText(); + doc.transact(() => { + text.delete(0, text.length); + text.insert(0, tempText); + }); } }); }; + +const createCollabSession = (roomId: string) => { + getDocument(roomId); + partnerReadiness.set(roomId, false); +}; + +const removeCollabSession = (roomId: string) => { + collabSessions.get(roomId)?.destroy(); + collabSessions.delete(roomId); + partnerReadiness.delete(roomId); +}; + +const getDocument = (roomId: string) => { + let doc = collabSessions.get(roomId); + if (!doc) { + doc = new Doc(); + doc.on(CollabEvents.UPDATE, (_update) => { + saveDocument(roomId, doc!); + io.to(roomId).emit(CollabEvents.UPDATE, encodeStateAsUpdateV2(doc!)); + }); + collabSessions.set(roomId, doc); + } + + return doc; +}; + +const saveDocument = async (roomId: string, doc: Doc) => { + const docState = encodeStateAsUpdateV2(doc); + const docAsString = Buffer.from(docState).toString("base64"); + await redisClient.set(`collaboration:${roomId}`, docAsString, { + EX: EXPIRY_TIME, + }); +}; diff --git a/backend/collab-service/src/server.ts b/backend/collab-service/src/server.ts index d16a00c6ec..c1d11c7333 100644 --- a/backend/collab-service/src/server.ts +++ b/backend/collab-service/src/server.ts @@ -1,7 +1,7 @@ import http from "http"; import app, { allowedOrigins } from "./app.ts"; import { handleWebsocketCollabEvents } from "./handlers/websocketHandler.ts"; -import { Server } from "socket.io"; +import { Server, Socket } from "socket.io"; import { connectRedis } from "./config/redis.ts"; const server = http.createServer(app); @@ -13,7 +13,7 @@ export const io = new Server(server, { connectionStateRecovery: {}, }); -io.on("connection", (socket) => { +io.on("connection", (socket: Socket) => { handleWebsocketCollabEvents(socket); }); diff --git a/backend/user-service/README.md b/backend/user-service/README.md index 5bd2f732f3..0c04de5047 100644 --- a/backend/user-service/README.md +++ b/backend/user-service/README.md @@ -36,14 +36,6 @@ 5. A default admin account (`email: admin@gmail.com` and `password: Admin@123`) wil be created. If you wish to change the default credentials, update them in `.env`. Alternatively, you can also edit your credentials and user profile after you have created the default account. -6. To view the contents stored in Redis, - - 1. Go to [http://localhost:5540](http://localhost:5540). - - 2. Click on "Add Redis Database". - - 3. Enter `host.internal.docker` as the Host. - ## Running User Service Individually > Make sure you have the cloud MongoDB URI in your .env file and set NODE_ENV to production already. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f685f42ad5..15179d3b10 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,14 +11,20 @@ "@babel/preset-env": "^7.25.4", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", + "@codemirror/collab": "^6.1.1", + "@codemirror/lang-javascript": "^6.2.2", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", "@mui/icons-material": "^6.1.0", "@mui/lab": "^6.0.0-beta.13", "@mui/material": "^6.1.0", + "@uiw/codemirror-extensions-basic-setup": "^4.23.6", + "@uiw/codemirror-extensions-langs": "^4.23.6", + "@uiw/react-codemirror": "^4.23.6", "@uiw/react-md-editor": "^4.0.4", "axios": "^1.7.7", + "codemirror": "^6.0.1", "history": "^5.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -28,7 +34,11 @@ "react-toastify": "^10.0.5", "socket.io-client": "^4.8.0", "uuid": "^11.0.2", - "vite-plugin-svgr": "^4.2.0" + "vite-plugin-svgr": "^4.2.0", + "y-codemirror.next": "^0.3.5", + "y-protocols": "^1.0.6", + "y-webrtc": "^10.3.0", + "yjs": "^13.6.20" }, "devDependencies": { "@eslint/js": "^9.9.0", @@ -1937,6 +1947,392 @@ "dev": true, "license": "MIT" }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.1.tgz", + "integrity": "sha512-iWHdj/B1ethnHRTwZj+C1obmmuCzquH29EbcKr0qIjA9NfDeBDJ7vs+WOHsFeLeflE4o+dHfYndJloMKHUkWUA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/collab": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/collab/-/collab-6.1.1.tgz", + "integrity": "sha512-tkIn9Jguh98ie12dbBuba3lE8LHUkaMrIFuCVeVGhncSczFdKmX25vC12+58+yqQW5AXi3py6jWY0W+jelyglA==", + "dependencies": { + "@codemirror/state": "^6.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.7.1.tgz", + "integrity": "sha512-llTrboQYw5H4THfhN4U3qCnSZ1SOJ60ohhz+SzU0ADGtwlc533DtklQP0vSFaQuCPDn3BPpOd1GbbnUtwNjsrw==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-angular": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-angular/-/lang-angular-0.1.3.tgz", + "integrity": "sha512-xgeWGJQQl1LyStvndWtruUvb4SnBZDAu/gvFH/ZU+c0W25tQR8e5hq7WTwiIY2dNxnf+49mRiGI/9yxIwB6f5w==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.3" + } + }, + "node_modules/@codemirror/lang-cpp": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-cpp/-/lang-cpp-6.0.2.tgz", + "integrity": "sha512-6oYEYUKHvrnacXxWxYa6t4puTlbN3dgV662BDfSH8+MfjQjVmP697/KYTDOqpxgerkvoNm7q5wlFMBeX8ZMocg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/cpp": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.0.tgz", + "integrity": "sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-go": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-go/-/lang-go-6.0.1.tgz", + "integrity": "sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/go": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", + "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "node_modules/@codemirror/lang-java": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-java/-/lang-java-6.0.1.tgz", + "integrity": "sha512-OOnmhH67h97jHzCuFaIEspbmsT98fNdhVhmA3zCxW0cn7l8rChDhZtwiwJ/JOKXgfm4J+ELxQihxaI7bj7mJRg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/java": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.2.tgz", + "integrity": "sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-json": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-json/-/lang-json-6.0.1.tgz", + "integrity": "sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/json": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-less": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-less/-/lang-less-6.0.2.tgz", + "integrity": "sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-lezer": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-lezer/-/lang-lezer-6.0.1.tgz", + "integrity": "sha512-WHwjI7OqKFBEfkunohweqA5B/jIlxaZso6Nl3weVckz8EafYbPZldQEKSDb4QQ9H9BUkle4PVELP4sftKoA0uQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/lezer": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-liquid": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-liquid/-/lang-liquid-6.2.1.tgz", + "integrity": "sha512-J1Mratcm6JLNEiX+U2OlCDTysGuwbHD76XwuL5o5bo9soJtSbz2g6RU3vGHFyS5DC8rgVmFSzi7i6oBftm7tnA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.3.0.tgz", + "integrity": "sha512-lYrI8SdL/vhd0w0aHIEvIRLRecLF7MiiRfzXFZY94dFwHqC9HtgxgagJ8fyYNBldijGatf9wkms60d8SrAj6Nw==", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-php": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-php/-/lang-php-6.0.1.tgz", + "integrity": "sha512-ublojMdw/PNWa7qdN5TMsjmqkNuTBD3k6ndZ4Z0S25SBAiweFGyY68AS3xNcIOlb6DDFDvKlinLQ40vSLqf8xA==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/php": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-python": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/@codemirror/lang-python/-/lang-python-6.1.6.tgz", + "integrity": "sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==", + "dependencies": { + "@codemirror/autocomplete": "^6.3.2", + "@codemirror/language": "^6.8.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/python": "^1.1.4" + } + }, + "node_modules/@codemirror/lang-rust": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-rust/-/lang-rust-6.0.1.tgz", + "integrity": "sha512-344EMWFBzWArHWdZn/NcgkwMvZIWUR1GEBdwG8FEp++6o6vT6KL9V7vGs2ONsKxxFUPXKI0SPcWhyYyl2zPYxQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/rust": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sass": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sass/-/lang-sass-6.0.2.tgz", + "integrity": "sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==", + "dependencies": { + "@codemirror/lang-css": "^6.2.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/sass": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-sql": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.8.0.tgz", + "integrity": "sha512-aGLmY4OwGqN3TdSx3h6QeA1NrvaYtF7kkoWR/+W7/JzB0gQtJ+VJxewlnE3+VImhA4WVlhmkJr109PefOOhjLg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-vue": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-vue/-/lang-vue-0.1.3.tgz", + "integrity": "sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.2", + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.1" + } + }, + "node_modules/@codemirror/lang-wast": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@codemirror/lang-wast/-/lang-wast-6.0.2.tgz", + "integrity": "sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-xml": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-xml/-/lang-xml-6.1.0.tgz", + "integrity": "sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/xml": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-yaml": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-yaml/-/lang-yaml-6.1.1.tgz", + "integrity": "sha512-HV2NzbK9bbVnjWxwObuZh5FuPCowx51mEfoFT9y3y+M37fA3+pbxx4I7uePuygFzDsAmCTwQSc/kXh/flab4uw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.2.0", + "@lezer/yaml": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.3.tgz", + "integrity": "sha512-kDqEU5sCP55Oabl6E7m5N+vZRoc0iWqgDVhEKifcHzPzjqCegcO4amfrYVL9PmPZpl4G0yjkpTpUO/Ui8CzO8A==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/language-data": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@codemirror/language-data/-/language-data-6.5.1.tgz", + "integrity": "sha512-0sWxeUSNlBr6OmkqybUTImADFUP0M3P0IiSde4nc24bz/6jIYzqYSgkOSLS+CBIoW1vU8Q9KUWXscBXeoMVC9w==", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-go": "^6.0.0", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.0", + "@codemirror/lang-liquid": "^6.0.0", + "@codemirror/lang-markdown": "^6.0.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.0.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.0", + "@codemirror/lang-sql": "^6.0.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/lang-yaml": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.4.0" + } + }, + "node_modules/@codemirror/legacy-modes": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/legacy-modes/-/legacy-modes-6.4.1.tgz", + "integrity": "sha512-vdg3XY7OAs5uLDx2Iw+cGfnwtd7kM+Et/eMsqAGTfT/JKiVBQZXosTzjEbWAi/FrY6DcQIz8mQjBozFHZEUWQA==", + "dependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.2.tgz", + "integrity": "sha512-PDFG5DjHxSEjOXk9TQYYVjZDqlZTFaDBfhQixHnQOEVDDNHUbEh/hstAjcQJaA6FQdZTD1hquXTK0rVBLADR1g==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.6", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz", + "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz", + "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==" + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.2.tgz", + "integrity": "sha512-F+sH0X16j/qFLMAfbciKTxVOwkdAS336b7AXTKOZhy8BR3eH/RelsnLgLFINrpST63mmN2OuwUt0W2ndUgYwUA==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.34.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.34.1.tgz", + "integrity": "sha512-t1zK/l9UiRqwUNPm+pdIT0qzJlzuVckbTEMVNFhfWkGiBQClstzg+78vedCvLSX0xJEZ6lwZbPpnljL7L6iwMQ==", + "dependencies": { + "@codemirror/state": "^6.4.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -3498,6 +3894,175 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "node_modules/@lezer/cpp": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/cpp/-/cpp-1.1.2.tgz", + "integrity": "sha512-macwKtyeUO0EW86r3xWQCzOV9/CF8imJLpJlPv3sDY57cPGeUZ8gXWOWNlJr52TVByMV3PayFQCA5SHEERDmVQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/css": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.9.tgz", + "integrity": "sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/go": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@lezer/go/-/go-1.0.0.tgz", + "integrity": "sha512-co9JfT3QqX1YkrMmourYw2Z8meGC50Ko4d54QEcQbEYpvdUvN4yb0NBZdn/9ertgvjsySxHsKzH3lbm3vqJ4Jw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", + "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/java": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@lezer/java/-/java-1.1.3.tgz", + "integrity": "sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.4.19", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.19.tgz", + "integrity": "sha512-j44kbR1QL26l6dMunZ1uhKBFteVGLVCBGNUD2sUaMnic+rbTviVuoK0CD1l9FTW31EueWvFFswCKMH7Z+M3JRA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/json": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/json/-/json-1.0.2.tgz", + "integrity": "sha512-xHT2P4S5eeCYECyKNPhr4cbEL9tc8w83SPwRC373o9uEdrvGKTZoJVAGxpOsZckMlEh9W23Pc72ew918RWQOBQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lezer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@lezer/lezer/-/lezer-1.1.2.tgz", + "integrity": "sha512-O8yw3CxPhzYHB1hvwbdozjnAslhhR8A5BH7vfEMof0xk3p+/DFDfZkA9Tde6J+88WgtwaHy4Sy6ThZSkaI0Evw==", + "dependencies": { + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.3.1.tgz", + "integrity": "sha512-DGlzU/i8DC8k0uz1F+jeePrkATl0jWakauTzftMQOcbaMkHbNSRki/4E2tOzJWsVpoKYhe7iTJ03aepdwVUXUA==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@lezer/php": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/php/-/php-1.0.2.tgz", + "integrity": "sha512-GN7BnqtGRpFyeoKSEqxvGvhJQiI4zkgmYnDk/JIyc7H7Ifc1tkPnUn/R2R8meH3h/aBf5rzjvU8ZQoyiNDtDrA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.1.0" + } + }, + "node_modules/@lezer/python": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@lezer/python/-/python-1.1.14.tgz", + "integrity": "sha512-ykDOb2Ti24n76PJsSa4ZoDF0zH12BSw1LGfQXCYJhJyOGiFTfGaX0Du66Ze72R+u/P35U+O6I9m8TFXov1JzsA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/rust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@lezer/rust/-/rust-1.0.2.tgz", + "integrity": "sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/sass": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@lezer/sass/-/sass-1.0.7.tgz", + "integrity": "sha512-8HLlOkuX/SMHOggI2DAsXUw38TuURe+3eQ5hiuk9QmYOUyC55B1dYEIMkav5A4IELVaW4e1T4P9WRiI5ka4mdw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/xml": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@lezer/xml/-/xml-1.0.5.tgz", + "integrity": "sha512-VFouqOzmUWfIg+tfmpcdV33ewtK+NSwd4ngSe1aG7HFb4BN0ExyY1b8msp+ndFrnlG4V4iC8yXacjFtrwERnaw==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/yaml": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@lezer/yaml/-/yaml-1.0.3.tgz", + "integrity": "sha512-GuBLekbw9jDBDhGur82nuwkxKQ+a3W5H0GfaAthDXcAu+XdpS43VlnxA9E9hllkpSP5ellRDKjLLj7Lu9Wr6xA==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.4.0" + } + }, "node_modules/@mui/base": { "version": "5.0.0-beta.60", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.60.tgz", @@ -3804,6 +4369,23 @@ } } }, + "node_modules/@nextjournal/lang-clojure": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nextjournal/lang-clojure/-/lang-clojure-1.0.0.tgz", + "integrity": "sha512-gOCV71XrYD0DhwGoPMWZmZ0r92/lIHsqQu9QWdpZYYBwiChNwMO4sbVMP7eTuAqffFB2BTtCSC+1skSH9d3bNg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@nextjournal/lezer-clojure": "1.0.0" + } + }, + "node_modules/@nextjournal/lezer-clojure": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nextjournal/lezer-clojure/-/lezer-clojure-1.0.0.tgz", + "integrity": "sha512-VZyuGu4zw5mkTOwQBTaGVNWmsOZAPw5ZRxu1/Knk/Xfs7EDBIogwIs5UXTYkuECX5ZQB8eOB+wKA2pc7VyqaZQ==", + "dependencies": { + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3848,6 +4430,63 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@replit/codemirror-lang-csharp": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-csharp/-/codemirror-lang-csharp-6.2.0.tgz", + "integrity": "sha512-6utbaWkoymhoAXj051mkRp+VIJlpwUgCX9Toevz3YatiZsz512fw3OVCedXQx+WcR0wb6zVHjChnuxqfCLtFVQ==", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@replit/codemirror-lang-nix": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-nix/-/codemirror-lang-nix-6.0.1.tgz", + "integrity": "sha512-lvzjoYn9nfJzBD5qdm3Ut6G3+Or2wEacYIDJ49h9+19WSChVnxv4ojf+rNmQ78ncuxIt/bfbMvDLMeMP0xze6g==", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@replit/codemirror-lang-solidity": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-solidity/-/codemirror-lang-solidity-6.0.2.tgz", + "integrity": "sha512-/dpTVH338KFV6SaDYYSadkB4bI/0B0QRF/bkt1XS3t3QtyR49mn6+2k0OUQhvt2ZSO7kt10J+OPilRAtgbmX0w==", + "dependencies": { + "@lezer/highlight": "^1.2.0" + }, + "peerDependencies": { + "@codemirror/language": "^6.0.0" + } + }, + "node_modules/@replit/codemirror-lang-svelte": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@replit/codemirror-lang-svelte/-/codemirror-lang-svelte-6.0.0.tgz", + "integrity": "sha512-U2OqqgMM6jKelL0GNWbAmqlu1S078zZNoBqlJBW+retTc5M4Mha6/Y2cf4SVg6ddgloJvmcSpt4hHrVoM4ePRA==", + "peerDependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.1", + "@codemirror/lang-html": "^6.2.0", + "@codemirror/lang-javascript": "^6.1.1", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/javascript": "^1.2.0", + "@lezer/lr": "^1.0.0" + } + }, "node_modules/@rollup/pluginutils": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", @@ -5102,6 +5741,73 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.23.6", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.23.6.tgz", + "integrity": "sha512-bvtq8IOvdkLJMhoJBRGPEzU51fMpPDwEhcAHp9xCR05MtbIokQgsnLXrmD1aZm6e7s/3q47H+qdSfAAkR5MkLA==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/codemirror-extensions-langs": { + "version": "4.23.6", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-langs/-/codemirror-extensions-langs-4.23.6.tgz", + "integrity": "sha512-VKWbEXmVq3EFYrJPWXH4Ei1f92zxuAg6dOlo8suSmwjmEc0qjNEP5Ss2CUi9LlzuWMGMmZgdKw56I3L71wYOog==", + "dependencies": { + "@codemirror/lang-angular": "^0.1.0", + "@codemirror/lang-cpp": "^6.0.0", + "@codemirror/lang-css": "^6.2.0", + "@codemirror/lang-html": "^6.4.0", + "@codemirror/lang-java": "^6.0.0", + "@codemirror/lang-javascript": "^6.1.0", + "@codemirror/lang-json": "^6.0.0", + "@codemirror/lang-less": "^6.0.1", + "@codemirror/lang-lezer": "^6.0.0", + "@codemirror/lang-liquid": "^6.0.1", + "@codemirror/lang-markdown": "^6.1.0", + "@codemirror/lang-php": "^6.0.0", + "@codemirror/lang-python": "^6.1.0", + "@codemirror/lang-rust": "^6.0.0", + "@codemirror/lang-sass": "^6.0.1", + "@codemirror/lang-sql": "^6.4.0", + "@codemirror/lang-vue": "^0.1.1", + "@codemirror/lang-wast": "^6.0.0", + "@codemirror/lang-xml": "^6.0.0", + "@codemirror/language-data": ">=6.0.0", + "@codemirror/legacy-modes": ">=6.0.0", + "@nextjournal/lang-clojure": "^1.0.0", + "@replit/codemirror-lang-csharp": "^6.1.0", + "@replit/codemirror-lang-nix": "^6.0.1", + "@replit/codemirror-lang-solidity": "^6.0.1", + "@replit/codemirror-lang-svelte": "^6.0.0", + "codemirror-lang-mermaid": "^0.5.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/language-data": ">=6.0.0", + "@codemirror/legacy-modes": ">=6.0.0" + } + }, "node_modules/@uiw/copy-to-clipboard": { "version": "1.0.17", "resolved": "https://registry.npmjs.org/@uiw/copy-to-clipboard/-/copy-to-clipboard-1.0.17.tgz", @@ -5110,6 +5816,31 @@ "url": "https://jaywcjlove.github.io/#/sponsor" } }, + "node_modules/@uiw/react-codemirror": { + "version": "4.23.6", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.23.6.tgz", + "integrity": "sha512-caYKGV6TfGLRV1HHD3p0G3FiVzKL1go7wes5XT2nWjB0+dTdyzyb81MKRSacptgZcotujfNO6QXn65uhETRAMw==", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.23.6", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@uiw/react-markdown-preview": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/@uiw/react-markdown-preview/-/react-markdown-preview-5.1.3.tgz", @@ -5605,6 +6336,25 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/bcp-47-match": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", @@ -5682,6 +6432,29 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5859,6 +6632,30 @@ "node": ">= 0.12.0" } }, + "node_modules/codemirror": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz", + "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/codemirror-lang-mermaid": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/codemirror-lang-mermaid/-/codemirror-lang-mermaid-0.5.0.tgz", + "integrity": "sha512-Taw/2gPCyNArQJCxIP/HSUif+3zrvD+6Ugt7KJZ2dUKou/8r3ZhcfG8krNTZfV2iu8AuGnymKuo7bLPFyqsh/A==", + "dependencies": { + "@codemirror/language": "^6.9.0", + "@lezer/highlight": "^1.1.6", + "@lezer/lr": "^1.3.10" + } + }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -6043,6 +6840,11 @@ "dev": true, "license": "MIT" }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -6377,6 +7179,11 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/err-code": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-3.0.1.tgz", + "integrity": "sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==" + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7051,6 +7858,11 @@ "node": ">=6.9.0" } }, + "node_modules/get-browser-rtc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-browser-rtc/-/get-browser-rtc-1.1.0.tgz", + "integrity": "sha512-MghbMJ61EJrRsDe7w1Bvqt3ZsBuqhce5nrn/XAwgwOXhcsz53/ltdxOse1h/8eKXj5slzxdsz56g5rzOFSGwfQ==" + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7526,6 +8338,25 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7605,7 +8436,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/inline-style-parser": { @@ -7768,6 +8598,15 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", @@ -9732,6 +10571,26 @@ "node": ">= 0.8.0" } }, + "node_modules/lib0": { + "version": "0.2.98", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.98.tgz", + "integrity": "sha512-XteTiNO0qEXqqweWx+b21p/fBnNHUA1NwAtJNJek1oPrewEZs2uiT4gWivHKr9GqCjDPAhchz0UQO8NwU3bBNA==", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -11305,7 +12164,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -11321,6 +12179,14 @@ } ] }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -11685,6 +12551,19 @@ "react-dom": ">=16.6.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -12196,6 +13075,25 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -12260,6 +13158,34 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-peer": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/simple-peer/-/simple-peer-9.11.1.tgz", + "integrity": "sha512-D1SaWpOW8afq1CZGWB8xTfrT3FekjQmPValrqncJMX7QFl8YwhrPTZvMCANLtgBwwdS+7zURyqxDDEmY558tTw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "buffer": "^6.0.3", + "debug": "^4.3.2", + "err-code": "^3.0.1", + "get-browser-rtc": "^1.1.0", + "queue-microtask": "^1.2.3", + "randombytes": "^2.1.0", + "readable-stream": "^3.6.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -12388,6 +13314,14 @@ "node": ">=8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -12487,6 +13421,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, "node_modules/style-to-object": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", @@ -12967,6 +13906,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/uuid": { "version": "11.0.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.2.tgz", @@ -13119,6 +14063,11 @@ "vite": "^2.6.0 || 3 || 4 || 5" } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, "node_modules/w3c-xmlserializer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", @@ -13301,7 +14250,7 @@ "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -13344,6 +14293,68 @@ "node": ">=0.4.0" } }, + "node_modules/y-codemirror.next": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/y-codemirror.next/-/y-codemirror.next-0.3.5.tgz", + "integrity": "sha512-VluNu3e5HfEXybnypnsGwKAj+fKLd4iAnR7JuX1Sfyydmn1jCBS5wwEL/uS04Ch2ib0DnMAOF6ZRR/8kK3wyGw==", + "dependencies": { + "lib0": "^0.2.42" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "yjs": "^13.5.6" + } + }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, + "node_modules/y-webrtc": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/y-webrtc/-/y-webrtc-10.3.0.tgz", + "integrity": "sha512-KalJr7dCgUgyVFxoG3CQYbpS0O2qybegD0vI4bYnYHI0MOwoVbucED3RZ5f2o1a5HZb1qEssUKS0H/Upc6p1lA==", + "dependencies": { + "lib0": "^0.2.42", + "simple-peer": "^9.11.0", + "y-protocols": "^1.0.6" + }, + "bin": { + "y-webrtc-signaling": "bin/server.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "optionalDependencies": { + "ws": "^8.14.2" + }, + "peerDependencies": { + "yjs": "^13.6.8" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -13396,6 +14407,22 @@ "node": ">=12" } }, + "node_modules/yjs": { + "version": "13.6.20", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.20.tgz", + "integrity": "sha512-Z2YZI+SYqK7XdWlloI3lhMiKnCdFCVC4PchpdO+mCYwtiTwncjUbnRK9R1JmkNfdmHyDXuWN3ibJAt0wsqTbLQ==", + "dependencies": { + "lib0": "^0.2.98" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 7bd11112c0..a49a949121 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,14 +15,20 @@ "@babel/preset-env": "^7.25.4", "@babel/preset-react": "^7.24.7", "@babel/preset-typescript": "^7.24.7", + "@codemirror/collab": "^6.1.1", + "@codemirror/lang-javascript": "^6.2.2", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.1.0", "@mui/icons-material": "^6.1.0", "@mui/lab": "^6.0.0-beta.13", "@mui/material": "^6.1.0", + "@uiw/codemirror-extensions-basic-setup": "^4.23.6", + "@uiw/codemirror-extensions-langs": "^4.23.6", + "@uiw/react-codemirror": "^4.23.6", "@uiw/react-md-editor": "^4.0.4", "axios": "^1.7.7", + "codemirror": "^6.0.1", "history": "^5.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -32,7 +38,11 @@ "react-toastify": "^10.0.5", "socket.io-client": "^4.8.0", "uuid": "^11.0.2", - "vite-plugin-svgr": "^4.2.0" + "vite-plugin-svgr": "^4.2.0", + "y-codemirror.next": "^0.3.5", + "y-protocols": "^1.0.6", + "y-webrtc": "^10.3.0", + "yjs": "^13.6.20" }, "devDependencies": { "@eslint/js": "^9.9.0", diff --git a/frontend/src/components/Chat/index.tsx b/frontend/src/components/Chat/index.tsx index 45a20f1a2d..791e45b8b8 100644 --- a/frontend/src/components/Chat/index.tsx +++ b/frontend/src/components/Chat/index.tsx @@ -71,7 +71,7 @@ const Chat: React.FC = ({ isActive }) => { }, []); useEffect(() => { - // initliase listerner for incoming messages + // initialize listener for incoming messages communicationSocket.on( CommunicationEvents.USER_JOINED, (message: Message) => { diff --git a/frontend/src/components/CodeEditor/index.tsx b/frontend/src/components/CodeEditor/index.tsx new file mode 100644 index 0000000000..77f4910a00 --- /dev/null +++ b/frontend/src/components/CodeEditor/index.tsx @@ -0,0 +1,87 @@ +import CodeMirror, { ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { langs } from "@uiw/codemirror-extensions-langs"; +import { basicSetup } from "@uiw/codemirror-extensions-basic-setup"; +import { EditorView } from "@codemirror/view"; +import { EditorState } from "@codemirror/state"; +import { useEffect, useState } from "react"; +import { initDocument } from "../../utils/collabSocket"; +import { cursorExtension } from "../../utils/collabCursor"; +import { yCollab } from "y-codemirror.next"; +import { Text } from "yjs"; +import { Awareness } from "y-protocols/awareness"; + +interface CodeEditorProps { + editorState?: { text: Text; awareness: Awareness }; + uid?: string; + username?: string; + language: string; + template?: string; + roomId?: string; + isReadOnly?: boolean; +} + +const languageSupport = { + Python: langs.python(), + Java: langs.java(), + C: langs.c(), +}; + +const CodeEditor: React.FC = (props) => { + const { + editorState, + uid = "", + username = "", + language, + template = "", + roomId = "", + isReadOnly = false, + } = props; + + const [isEditorReady, setIsEditorReady] = useState(false); + const [isDocumentLoaded, setIsDocumentLoaded] = useState(false); + + const onEditorReady = (editor: ReactCodeMirrorRef) => { + if (!isEditorReady && editor?.editor && editor?.state && editor?.view) { + setIsEditorReady(true); + } + }; + + useEffect(() => { + if (isReadOnly || !isEditorReady) { + return; + } + + const loadTemplate = async () => { + await initDocument(uid, roomId, template); + setIsDocumentLoaded(true); + }; + loadTemplate(); + }, [isReadOnly, isEditorReady]); + + return ( + + ); +}; + +export default CodeEditor; diff --git a/frontend/src/components/CollabSessionControls/index.tsx b/frontend/src/components/CollabSessionControls/index.tsx index 3f261e0077..cbd3fd1299 100644 --- a/frontend/src/components/CollabSessionControls/index.tsx +++ b/frontend/src/components/CollabSessionControls/index.tsx @@ -8,6 +8,7 @@ import { extractMinutesFromTime, extractSecondsFromTime, } from "../../utils/sessionTime"; +import { getDocumentContent } from "../../utils/collabSocket"; const CollabSessionControls: React.FC = () => { const [time, setTime] = useState(0); @@ -45,6 +46,7 @@ const CollabSessionControls: React.FC = () => { time )} mins ${extractSecondsFromTime(time)} secs` ); + console.log(`Code: ${getDocumentContent()}`); }} // TODO: implement submit function with time taken pop-up > Submit diff --git a/frontend/src/contexts/MatchContext.tsx b/frontend/src/contexts/MatchContext.tsx index 3bd84c5da1..2610792492 100644 --- a/frontend/src/contexts/MatchContext.tsx +++ b/frontend/src/contexts/MatchContext.tsx @@ -509,7 +509,7 @@ const MatchProvider: React.FC<{ children?: React.ReactNode }> = (props) => { const handleEndSessionClick = () => { setIsEndSessionModalOpen(true); - } + }; const handleRejectEndSession = () => { setIsEndSessionModalOpen(false); diff --git a/frontend/src/pages/CollabSandbox/index.tsx b/frontend/src/pages/CollabSandbox/index.tsx index 9d2bf9afb4..c1a72e3906 100644 --- a/frontend/src/pages/CollabSandbox/index.tsx +++ b/frontend/src/pages/CollabSandbox/index.tsx @@ -26,6 +26,8 @@ import { Navigate } from "react-router-dom"; import Chat from "../../components/Chat"; import TabPanel from "../../components/TabPanel"; import TestCase from "../../components/TestCase"; +import CodeEditor from "../../components/CodeEditor"; +import { CollabSessionData, join, leave } from "../../utils/collabSocket"; // hardcode for now... @@ -53,6 +55,9 @@ const testcases: TestCase[] = [ const CollabSandbox: React.FC = () => { const [showErrorScreen, setShowErrorScreen] = useState(false); + const [editorState, setEditorState] = useState( + null + ); const match = useMatch(); if (!match) { @@ -64,7 +69,9 @@ const CollabSandbox: React.FC = () => { getMatchId, handleRejectEndSession, handleConfirmEndSession, + matchUser, partner, + matchCriteria, loading, isEndSessionModalOpen, questionId, @@ -86,9 +93,27 @@ const CollabSandbox: React.FC = () => { } getQuestionById(questionId, dispatch); - // TODO - // use getMatchId() as the room id in the collab service - console.log(getMatchId()); + const matchId = getMatchId(); + if (!matchUser || !matchId) { + return; + } + + const connectToCollabSession = async () => { + try { + const editorState = await join(matchUser.id, matchId); + if (editorState.ready) { + setEditorState(editorState); + } else { + setShowErrorScreen(true); + } + } catch (error) { + console.error("Error connecting to collab session: ", error); + } + }; + + connectToCollabSession(); + + return () => leave(matchUser.id, matchId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -111,20 +136,20 @@ const CollabSandbox: React.FC = () => { return ; } - if (!partner) { + if (!matchUser || !partner || !matchCriteria || !getMatchId()) { return ; } if (showErrorScreen) { return ( ); } - if (!selectedQuestion) { + if (!selectedQuestion || !editorState) { return ; } @@ -193,7 +218,32 @@ const CollabSandbox: React.FC = () => { }} size={6} > - Code Editor + ({ + flex: 1, + width: "100%", + maxHeight: "50vh", + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + })} + > + + { const match = useMatch(); diff --git a/frontend/src/pages/QuestionHistoryDetail/index.tsx b/frontend/src/pages/QuestionHistoryDetail/index.tsx index e3bc00cbaa..f7cf7c041e 100644 --- a/frontend/src/pages/QuestionHistoryDetail/index.tsx +++ b/frontend/src/pages/QuestionHistoryDetail/index.tsx @@ -7,18 +7,36 @@ import reducer, { getQuestionById, initialState, } from "../../reducers/questionReducer"; -import qnHistoryReducer, { getQnHistoryById, initialQHState, setSelectedQnHistoryError } from "../../reducers/qnHistoryReducer"; -import { Box, IconButton, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from "@mui/material"; +import qnHistoryReducer, { + getQnHistoryById, + initialQHState, + setSelectedQnHistoryError, +} from "../../reducers/qnHistoryReducer"; +import { + Box, + IconButton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from "@mui/material"; import ArrowBack from "@mui/icons-material/ArrowBack"; import { grey } from "@mui/material/colors"; import { convertDateString } from "../../utils/sessionTime"; import { useAuth } from "../../contexts/AuthContext"; import { USE_AUTH_ERROR_MESSAGE } from "../../utils/constants"; import Loader from "../../components/Loader"; +import CodeEditor from "../../components/CodeEditor"; const QuestionHistoryDetail: React.FC = () => { const { qnHistoryId } = useParams<{ qnHistoryId: string }>(); - const [qnhistState, qnhistDispatch] = useReducer(qnHistoryReducer, initialQHState); + const [qnhistState, qnhistDispatch] = useReducer( + qnHistoryReducer, + initialQHState + ); const [qnState, qnDispatch] = useReducer(reducer, initialState); const [loading, setLoading] = useState(true); const navigate = useNavigate(); @@ -30,11 +48,14 @@ const QuestionHistoryDetail: React.FC = () => { const { user } = auth; - const tableHeaders = ["Status", "Date submitted", "Time taken", "Partner"] + const tableHeaders = ["Status", "Date submitted", "Time taken", "Partner"]; useEffect(() => { if (!qnHistoryId) { - setSelectedQnHistoryError("Unable to fetch question history.", qnhistDispatch); + setSelectedQnHistoryError( + "Unable to fetch question history.", + qnhistDispatch + ); return; } @@ -47,8 +68,7 @@ const QuestionHistoryDetail: React.FC = () => { getQuestionById(qnhistState.selectedQnHistory.questionId, qnDispatch); } setTimeout(() => setLoading(false), 500); - }, [qnhistState]) - + }, [qnhistState]); const getPartnerId = (userIds: string[], currUserId: string): string => { if (currUserId == userIds[0]) { @@ -56,12 +76,12 @@ const QuestionHistoryDetail: React.FC = () => { } else { return userIds[0]; } - } + }; if (loading) { return ; } - + if (!qnhistState.selectedQnHistory) { if (qnhistState.selectedQnHistoryError) { return ( @@ -75,116 +95,139 @@ const QuestionHistoryDetail: React.FC = () => { } } - const partnerId = user && qnhistState.selectedQnHistory && getPartnerId(qnhistState.selectedQnHistory.userIds, user.id); + const partnerId = + user && + qnhistState.selectedQnHistory && + getPartnerId(qnhistState.selectedQnHistory.userIds, user.id); return ( - navigate(`/profile/${user?.id}`)}> + navigate(`/profile/${user?.id}`)} + > - Latest submission details - { user && qnhistState.selectedQnHistory && - - ({ - "& .MuiTableCell-root": { padding: theme.spacing(1.2) }, - whiteSpace: "nowrap", - })} - > - - - {tableHeaders.map((header) => ( - - - {header} + + Latest submission details + + {user && qnhistState.selectedQnHistory && ( + +
({ + "& .MuiTableCell-root": { padding: theme.spacing(1.2) }, + whiteSpace: "nowrap", + })} + > + + + {tableHeaders.map((header) => ( + + + {header} + + + ))} + + + + + + + {qnhistState.selectedQnHistory.submissionStatus} - ))} - - - - - - - {qnhistState.selectedQnHistory.submissionStatus} - - - - - {convertDateString(qnhistState.selectedQnHistory.dateAttempted)} - - - - + {convertDateString( + qnhistState.selectedQnHistory.dateAttempted + )} + + + - {`${qnhistState.selectedQnHistory.timeTaken} mins`} - - - - + {`${qnhistState.selectedQnHistory.timeTaken} mins`} + + + navigate(`/profile/${partnerId}`)} > - {"Go to partner profile"} - - - - -
-
- } + navigate(`/profile/${partnerId}`)} + > + {"Go to partner profile"} + + + + + + + )} ({ flex: 1, marginRight: theme.spacing(2) })}> - {qnState.selectedQuestion - ? - : - } + {qnState.selectedQuestion ? ( + + ) : ( + + )} ({ flex: 1, marginLeft: theme.spacing(2) })}> - Code editor + ({ + flex: 1, + width: "100%", + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + })} + > + + diff --git a/frontend/src/utils/collabCursor.ts b/frontend/src/utils/collabCursor.ts new file mode 100644 index 0000000000..5de6bbf76c --- /dev/null +++ b/frontend/src/utils/collabCursor.ts @@ -0,0 +1,159 @@ +import { + EditorView, + Decoration, + DecorationSet, + WidgetType, +} from "@codemirror/view"; +import { StateField, StateEffect } from "@codemirror/state"; +import { receiveCursorUpdate, sendCursorUpdate } from "./collabSocket"; + +// Adapted from https://github.com/BjornTheProgrammer/react-codemirror-collab-sockets + +export interface Cursor { + uid: string; + username: string; + from: number; + to: number; +} + +class CursorWidget extends WidgetType { + private username: string; + + constructor(username: string) { + super(); + this.username = username; + } + + toDOM() { + const cursorRoot = document.createElement("div"); + cursorRoot.className = "cm-cursor-root"; + + const cursor = document.createElement("div"); + cursor.className = `cm-cursor-display cm-cursor-color`; + cursorRoot.appendChild(cursor); + + const cursorLabel = document.createElement("div"); + cursorLabel.className = `cm-cursor-label cm-cursor-color`; + cursorLabel.textContent = this.username; + cursorRoot.appendChild(cursorLabel); + + let labelTimeout = setTimeout(() => { + cursorLabel.style.display = "none"; + }, 2000); + + cursor.addEventListener("mouseenter", () => { + clearTimeout(labelTimeout); + cursorLabel.style.display = "block"; + }); + + cursor.addEventListener("mouseleave", () => { + labelTimeout = setTimeout(() => { + cursorLabel.style.display = "none"; + }, 2000); + }); + + return cursorRoot; + } +} + +export const updateCursor = StateEffect.define(); + +const cursorStateField = (uid: string): StateField => { + return StateField.define({ + create: () => Decoration.none, + update: (prevCursorState, transaction) => { + let cursorTransactions = prevCursorState.map(transaction.changes); + for (const effect of transaction.effects) { + // check for partner's cursor updates + if (effect.is(updateCursor) && effect.value.uid !== uid) { + const cursorUpdates = []; + + if (effect.value.from !== effect.value.to) { + // highlight selected text + cursorUpdates.push( + Decoration.mark({ + class: "cm-highlight-color", + uid: effect.value.uid, + }).range(effect.value.from, effect.value.to) + ); + } + + cursorUpdates.push( + Decoration.widget({ + widget: new CursorWidget(effect.value.username), + uid: effect.value.uid, + }).range(effect.value.to) + ); + + // ensure only the latest cursor position and/or selection is displayed + cursorTransactions = cursorTransactions.update({ + add: cursorUpdates, + filter: (_from, _to, value) => value.spec.uid !== effect.value.uid, + }); + } + } + return cursorTransactions; + }, + provide: (field) => EditorView.decorations.from(field), + }); +}; + +const cursorBaseTheme = EditorView.baseTheme({ + ".cm-cursor-root": { + display: "inline-block", + width: "0px", + height: "0px", + }, + ".cm-cursor-display": { + border: "none", + width: "0.5px", + height: "18.5px", + position: "absolute", + marginTop: "-14.5px", + marginLeft: "0px", + }, + ".cm-cursor-label": { + color: "white", + borderRadius: "4px 4px 4px 0px", + padding: "2px 4px", + fontSize: "12px", + position: "absolute", + marginTop: "-35px", + marginLeft: "0px", + whiteSpace: "nowrap", + }, + ".cm-cursor-color": { + backgroundColor: "#f6a1a1", + }, + ".cm-highlight-color": { + backgroundColor: "rgba(246, 161, 161, 0.3)", + }, +}); + +export const cursorExtension = ( + roomId: string, + uid: string, + username: string +) => { + return [ + cursorStateField(uid), // handles cursor positions and highlights + cursorBaseTheme, // provides cursor styling + // detects cursor updates + EditorView.updateListener.of((update) => { + update.transactions.forEach((transaction) => { + if (transaction.selection) { + const cursor: Cursor = { + uid: uid, + username: username, + from: transaction.selection.ranges[0].from, + to: transaction.selection.ranges[0].to, + }; + + sendCursorUpdate(roomId, cursor); + } + }); + + receiveCursorUpdate(update.view); + }), + ]; +}; diff --git a/frontend/src/utils/collabSocket.ts b/frontend/src/utils/collabSocket.ts new file mode 100644 index 0000000000..f52bc24819 --- /dev/null +++ b/frontend/src/utils/collabSocket.ts @@ -0,0 +1,144 @@ +import { EditorView } from "@codemirror/view"; +import { io } from "socket.io-client"; +import { updateCursor, Cursor } from "./collabCursor"; +import { Doc, Text, applyUpdateV2 } from "yjs"; +import { Awareness } from "y-protocols/awareness"; + +enum CollabEvents { + // Send + JOIN = "join", + LEAVE = "leave", + INIT_DOCUMENT = "init_document", + UPDATE_REQUEST = "update_request", + UPDATE_CURSOR_REQUEST = "update_cursor_request", + RECONNECT_REQUEST = "reconnect_request", + + // Receive + ROOM_READY = "room_ready", + DOCUMENT_READY = "document_ready", + UPDATE = "updateV2", + UPDATE_CURSOR = "update_cursor", + SOCKET_DISCONNECT = "disconnect", + SOCKET_CLIENT_DISCONNECT = "io client disconnect", + SOCKET_SERVER_DISCONNECT = "io server disconnect", + SOCKET_RECONNECT_SUCCESS = "reconnect", + SOCKET_RECONNECT_FAILED = "reconnect_failed", +} + +export type CollabSessionData = { + ready: boolean; + text: Text; + awareness: Awareness; +}; + +const COLLAB_SOCKET_URL = "http://localhost:3003"; +const collabSocket = io(COLLAB_SOCKET_URL, { + reconnectionAttempts: 3, + autoConnect: false, +}); + +let doc: Doc; +let text: Text; +let awareness: Awareness; + +export const join = ( + uid: string, + roomId: string +): Promise => { + collabSocket.connect(); + initConnectionStatusListeners(roomId); + + doc = new Doc(); + text = doc.getText(); + awareness = new Awareness(doc); + + doc.on(CollabEvents.UPDATE, (update, origin) => { + if (origin != uid) { + collabSocket.emit(CollabEvents.UPDATE_REQUEST, roomId, update); + } + }); + + collabSocket.on(CollabEvents.UPDATE, (update) => { + applyUpdateV2(doc, new Uint8Array(update), uid); + }); + + collabSocket.emit(CollabEvents.JOIN, uid, roomId); + + return new Promise((resolve) => { + collabSocket.once(CollabEvents.ROOM_READY, (ready: boolean) => { + resolve({ ready: ready, text: text, awareness: awareness }); + }); + }); +}; + +export const initDocument = (uid: string, roomId: string, template: string) => { + collabSocket.emit(CollabEvents.INIT_DOCUMENT, roomId, template); + + return new Promise((resolve) => { + collabSocket.once(CollabEvents.UPDATE, (update) => { + applyUpdateV2(doc, new Uint8Array(update), uid); + resolve(); + }); + }); +}; + +export const leave = (uid: string, roomId: string) => { + collabSocket.removeAllListeners(); + collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_SUCCESS); + collabSocket.io.removeListener(CollabEvents.SOCKET_RECONNECT_FAILED); + collabSocket.emit(CollabEvents.LEAVE, uid, roomId); + doc.destroy(); +}; + +export const sendCursorUpdate = (roomId: string, cursor: Cursor) => { + collabSocket.emit(CollabEvents.UPDATE_CURSOR_REQUEST, roomId, cursor); +}; + +export const receiveCursorUpdate = (view: EditorView) => { + if (collabSocket.hasListeners(CollabEvents.UPDATE_CURSOR)) { + return; + } + + collabSocket.on(CollabEvents.UPDATE_CURSOR, (cursor: Cursor) => { + view.dispatch({ + effects: updateCursor.of(cursor), + }); + }); +}; + +export const reconnectRequest = (roomId: string) => { + collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); +}; + +const initConnectionStatusListeners = (roomId: string) => { + if (!collabSocket.hasListeners(CollabEvents.SOCKET_DISCONNECT)) { + collabSocket.on(CollabEvents.SOCKET_DISCONNECT, (reason) => { + if ( + reason !== CollabEvents.SOCKET_CLIENT_DISCONNECT && + reason !== CollabEvents.SOCKET_SERVER_DISCONNECT + ) { + // TODO: Handle socket disconnection + } + }); + } + + if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_SUCCESS)) { + collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_SUCCESS, () => { + console.log("reconnect request"); + collabSocket.emit(CollabEvents.RECONNECT_REQUEST, roomId); + }); + } + + if (!collabSocket.io.hasListeners(CollabEvents.SOCKET_RECONNECT_FAILED)) { + collabSocket.io.on(CollabEvents.SOCKET_RECONNECT_FAILED, () => { + console.log("reconnect failed"); + }); + } +}; + +export const getDocumentContent = () => { + if (!doc.isDestroyed) { + return text.toString(); + } + return ""; +};