diff --git a/frontend/src/components/room/code-editor.tsx b/frontend/src/components/room/code-editor.tsx index 2998fa1a..c0889119 100644 --- a/frontend/src/components/room/code-editor.tsx +++ b/frontend/src/components/room/code-editor.tsx @@ -7,7 +7,7 @@ import { Settings, Play, } from "lucide-react"; -import Editor from "@monaco-editor/react"; +import Editor, { OnMount } from "@monaco-editor/react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; @@ -25,6 +25,7 @@ import { } from "@/components/ui/popover"; import { Card } from "../ui/card"; import { TypographyBody, TypographyBodyHeavy } from "../ui/typography"; +import { editor } from "monaco-editor"; type CodeEditorProps = { theme?: string; @@ -33,7 +34,9 @@ type CodeEditorProps = { defaultValue?: string; className?: string; text: string; + cursor: number; onChange: React.Dispatch>; + onCursorChange: React.Dispatch>; }; const frameworks = [ @@ -66,17 +69,50 @@ export default function CodeEditor({ defaultValue = "#Write your solution here", className, text, + cursor, onChange, + onCursorChange, }: CodeEditorProps) { const [open, setOpen] = React.useState(false); const [value, setValue] = React.useState(""); + const [monacoInstance, setMonacoInstance] = + React.useState(null); + + const editorMount: OnMount = (editorL: editor.IStandaloneCodeEditor) => { + setMonacoInstance(editorL); + }; + + const setCursorPosition = React.useCallback( + (cursor: number) => { + if (!monacoInstance) return; + + const position = monacoInstance.getModel()!.getPositionAt(cursor); + monacoInstance.setPosition(position); + }, + [monacoInstance] + ); + + React.useEffect(() => { + if (cursor !== undefined) { + setCursorPosition(cursor); + } + }, [cursor, setCursorPosition]); + const editorOnChange = React.useCallback( (value: string | undefined) => { + if (!monacoInstance) return; if (value === undefined) return; + + if (monacoInstance.getPosition()) { + const cursor = monacoInstance + .getModel()! + .getOffsetAt(monacoInstance.getPosition()!); + onCursorChange(cursor); + } onChange(value); }, - [onChange] + [onChange, onCursorChange, monacoInstance] ); return ( @@ -142,6 +178,7 @@ export default function CodeEditor({ value={text} theme={theme} onChange={(e) => editorOnChange(e)} + onMount={editorMount} />
diff --git a/frontend/src/hooks/useCollaboration.tsx b/frontend/src/hooks/useCollaboration.tsx index d3ec05c9..919c3e70 100644 --- a/frontend/src/hooks/useCollaboration.tsx +++ b/frontend/src/hooks/useCollaboration.tsx @@ -2,7 +2,7 @@ import { useEffect, useState, useRef } from "react"; import { io, Socket } from "socket.io-client"; import { debounce } from "lodash"; import { - TextOperationSet, + TextOperationSetWithCursor, createTextOpFromTexts, } from "../../../utils/shared-ot"; import { TextOp } from "ot-text-unicode"; @@ -24,7 +24,11 @@ var vers = 0; const useCollaboration = ({ roomId, userId }: UseCollaborationProps) => { const [socket, setSocket] = useState(null); const [text, setText] = useState("#Write your solution here"); + const [cursor, setCursor] = useState( + "#Write your solution here".length + ); const textRef = useRef(text); + const cursorRef = useRef(cursor); const prevTextRef = useRef(text); const awaitingAck = useRef(false); // ack from sending update const awaitingSync = useRef(false); // synced with server @@ -37,7 +41,15 @@ const useCollaboration = ({ roomId, userId }: UseCollaborationProps) => { socketConnection.on( SocketEvents.ROOM_UPDATE, - ({ version, text }: { version: number; text: string }) => { + ({ + version, + text, + cursor, + }: { + version: number; + text: string; + cursor: number | undefined | null; + }) => { console.log("Update vers to " + version); vers = version; @@ -46,6 +58,11 @@ const useCollaboration = ({ roomId, userId }: UseCollaborationProps) => { textRef.current = text; prevTextRef.current = text; setText(text); + if (cursor && cursor > -1) { + console.log("Update cursor to " + cursor); + cursorRef.current = cursor; + setCursor(cursor); + } awaitingSync.current = false; } ); @@ -59,6 +76,10 @@ const useCollaboration = ({ roomId, userId }: UseCollaborationProps) => { textRef.current = text; }, [text]); + useEffect(() => { + cursorRef.current = cursor; + }, [cursor]); + useEffect(() => { if (!socket) return; @@ -80,9 +101,10 @@ const useCollaboration = ({ roomId, userId }: UseCollaborationProps) => { console.log(textOp); - const textOperationSet: TextOperationSet = { + const textOperationSet: TextOperationSetWithCursor = { version: vers, operations: textOp, + cursor: cursorRef.current, }; socket.emit(SocketEvents.ROOM_UPDATE, textOperationSet, () => { @@ -90,7 +112,7 @@ const useCollaboration = ({ roomId, userId }: UseCollaborationProps) => { }); }, [text, socket]); - return { text, setText }; + return { text, setText, cursor, setCursor }; }; export default useCollaboration; diff --git a/frontend/src/pages/room/[id].tsx b/frontend/src/pages/room/[id].tsx index 2a48455e..e85bb4c1 100644 --- a/frontend/src/pages/room/[id].tsx +++ b/frontend/src/pages/room/[id].tsx @@ -12,7 +12,7 @@ export default function Room() { const roomId = router.query.id as string; const userId = "user1"; - const { text, setText } = useCollaboration({ + const { text, setText, cursor, setCursor } = useCollaboration({ roomId: roomId as string, userId, }); @@ -52,7 +52,12 @@ export default function Room() { {question.solution}
- +
diff --git a/utils/shared-ot.ts b/utils/shared-ot.ts index 26c5eca6..a1b4c33e 100644 --- a/utils/shared-ot.ts +++ b/utils/shared-ot.ts @@ -6,6 +6,10 @@ export interface TextOperationSet { operations: TextOp; } +export interface TextOperationSetWithCursor extends TextOperationSet { + cursor?: number; +} + export function createTextOpFromTexts( prevText: string, currentText: string