diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d97080aa..381be8d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,6 @@ jobs: - name: Install and Build 🔧 run: | - npm ci + npm ci --no-optional npm run build diff --git a/package-lock.json b/package-lock.json index fc87e540..606c048e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,7 @@ "eslint-plugin-react": "^7.21.5", "eslint-plugin-react-hooks": "^4.2.0", "fit-curve": "^0.2.0", + "fsevents": "2.3.2", "history": "^4.9.0", "husky": "^4.2.5", "invert-color": "^2.0.0", @@ -61,14 +62,14 @@ "redux-thunk": "^2.3.0", "sass": "^1.54.3", "typescript": "^3.7.5", - "yorkie-js-sdk": "^0.2.12" - }, - "devDependencies": { - "fsevents": "^2.3.2" + "yorkie-js-sdk": "^0.2.14" }, "engines": { "node": ">=16.0.0", "npm": ">=7.1.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" } }, "node_modules/@ampproject/remapping": { @@ -10189,8 +10190,8 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "devOptional": true, "hasInstallScript": true, + "optional": true, "os": [ "darwin" ], @@ -32837,7 +32838,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "devOptional": true + "optional": true }, "function-bind": { "version": "1.1.1", diff --git a/package.json b/package.json index c39a2068..0d05a933 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "redux-thunk": "^2.3.0", "sass": "^1.54.3", "typescript": "^3.7.5", - "yorkie-js-sdk": "^0.2.12" + "yorkie-js-sdk": "^0.2.14" }, "scripts": { "start": "REACT_APP_GIT_HASH=`git rev-parse --short HEAD` react-scripts start", @@ -100,7 +100,7 @@ "last 1 safari version" ] }, - "devDependencies": { + "optionalDependencies": { "fsevents": "^2.3.2" } } diff --git a/src/components/Editor/CodeEditor/index.tsx b/src/components/Editor/CodeEditor/index.tsx index 086f2b6d..a58a5ef1 100644 --- a/src/components/Editor/CodeEditor/index.tsx +++ b/src/components/Editor/CodeEditor/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useCallback } from 'react'; +import React, { useEffect, useMemo, useRef, useCallback, useState } from 'react'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import { useSelector } from 'react-redux'; import { ActorID, DocEvent, TextChange } from 'yorkie-js-sdk'; @@ -82,6 +82,7 @@ export default function CodeEditor({ forwardedRef }: CodeEditorProps) { const client = useSelector((state: AppState) => state.docState.client); const peers = useSelector((state: AppState) => state.peerState.peers); const cursorMapRef = useRef>(new Map()); + const [editor, setEditor] = useState(null); const connectCursor = useCallback((clientID: ActorID, metadata: Metadata) => { cursorMapRef.current.set(clientID, new Cursor(clientID, metadata)); @@ -94,128 +95,129 @@ export default function CodeEditor({ forwardedRef }: CodeEditorProps) { } }, []); - const getCmInstanceCallback = useCallback( - (editor: CodeMirror.Editor) => { - if (!client || !doc) { - return; - } - - // eslint-disable-next-line no-param-reassign - forwardedRef.current = editor; - - const updateCursor = (clientID: ActorID, pos: CodeMirror.Position) => { - const cursor = cursorMapRef.current.get(clientID); - cursor?.updateCursor(editor, pos); - }; - - const updateLine = (clientID: ActorID, fromPos: CodeMirror.Position, toPos: CodeMirror.Position) => { - const cursor = cursorMapRef.current.get(clientID); - cursor?.updateLine(editor, fromPos, toPos); - }; + const getCmInstanceCallback = useCallback((cm: CodeMirror.Editor) => { + setEditor(cm); + }, []); - let syncText = () => {}; + useEffect(() => { + for (const [id, peer] of Object.entries(peers)) { + if (cursorMapRef.current.has(id) && peer.status === ConnectionStatus.Disconnected) { + disconnectCursor(id); + } else if (!cursorMapRef.current.has(id) && peer.status === ConnectionStatus.Connected) { + connectCursor(id, peer.metadata); + } + } + }, [peers]); - doc.subscribe((event: DocEvent) => { - if (event.type === 'remote-change') { - // display remote cursors - for (const { change } of event.value) { - const actor = change.getID().getActorID()!; - if (actor !== client.getID()) { - if (!cursorMapRef.current.has(actor)) { - return; - } + useEffect(() => { + if (!client || !doc || !editor) { + return; + } - const cursor = cursorMapRef.current.get(actor); - if (cursor!.isActive()) { - return; - } + // eslint-disable-next-line no-param-reassign + forwardedRef.current = editor; + + const updateCursor = (clientID: ActorID, pos: CodeMirror.Position) => { + const cursor = cursorMapRef.current.get(clientID); + cursor?.updateCursor(editor, pos); + }; + + const updateLine = (clientID: ActorID, fromPos: CodeMirror.Position, toPos: CodeMirror.Position) => { + const cursor = cursorMapRef.current.get(clientID); + cursor?.updateLine(editor, fromPos, toPos); + }; + + let syncText = () => {}; + + doc.subscribe((event: DocEvent) => { + if (event.type === 'remote-change') { + // display remote cursors + for (const { change } of event.value) { + const actor = change.getID().getActorID()!; + if (actor !== client.getID()) { + if (!cursorMapRef.current.has(actor)) { + return; + } - updateCursor(actor, editor.posFromIndex(0)); + const cursor = cursorMapRef.current.get(actor); + if (cursor!.isActive()) { + return; } + + updateCursor(actor, editor.posFromIndex(0)); } - } else if (event.type === 'snapshot') { - // re-sync for the new text from the snapshot - syncText(); } - }); + } else if (event.type === 'snapshot') { + // re-sync for the new text from the snapshot + syncText(); + } + }); - // local to remote - editor.on('beforeChange', (instance: CodeMirror.Editor, change: CodeMirror.EditorChange) => { - if (change.origin === 'yorkie' || change.origin === 'setValue') { - return; - } + // local to remote + editor.on('beforeChange', (instance: CodeMirror.Editor, change: CodeMirror.EditorChange) => { + if (change.origin === 'yorkie' || change.origin === 'setValue') { + return; + } - const from = editor.indexFromPos(change.from); - const to = editor.indexFromPos(change.to); - const content = change.text.join('\n'); + const from = editor.indexFromPos(change.from); + const to = editor.indexFromPos(change.to); + const content = change.text.join('\n'); - doc.update((root) => { - root.content.edit(from, to, content); - }); + doc.update((root) => { + root.content.edit(from, to, content); }); + }); - editor.on('beforeSelectionChange', (instance: CodeMirror.Editor, data: CodeMirror.EditorSelectionChange) => { - if (!data.origin) { - return; - } + editor.on('beforeSelectionChange', (instance: CodeMirror.Editor, data: CodeMirror.EditorSelectionChange) => { + if (!data.origin) { + return; + } - const from = editor.indexFromPos(data.ranges[0].anchor); - const to = editor.indexFromPos(data.ranges[0].head); + const from = editor.indexFromPos(data.ranges[0].anchor); + const to = editor.indexFromPos(data.ranges[0].head); - doc.update((root) => { - root.content.select(from, to); - }); + doc.update((root) => { + root.content.select(from, to); }); - - // remote to local - const changeEventHandler = (changes: TextChange[]) => { - changes.forEach((change) => { - const { actor, from, to } = change; - if (change.type === 'content') { - const content = change.content || ''; - if (actor !== client.getID()) { - const fromPos = editor.posFromIndex(from); - const toPos = editor.posFromIndex(to); - editor.replaceRange(content, fromPos, toPos, 'yorkie'); - } - } else if (change.type === 'selection') { - if (actor !== client.getID()) { - let fromPos = editor.posFromIndex(from); - let toPos = editor.posFromIndex(to); - updateCursor(actor, toPos); - if (from > to) { - [toPos, fromPos] = [fromPos, toPos]; - } - updateLine(actor, fromPos, toPos); + }); + + // remote to local + const changeEventHandler = (changes: TextChange[]) => { + changes.forEach((change) => { + const { actor, from, to } = change; + if (change.type === 'content') { + const content = change.content || ''; + if (actor !== client.getID()) { + const fromPos = editor.posFromIndex(from); + const toPos = editor.posFromIndex(to); + editor.replaceRange(content, fromPos, toPos, 'yorkie'); + } + } else if (change.type === 'selection') { + if (actor !== client.getID()) { + let fromPos = editor.posFromIndex(from); + let toPos = editor.posFromIndex(to); + updateCursor(actor, toPos); + if (from > to) { + [toPos, fromPos] = [fromPos, toPos]; } + updateLine(actor, fromPos, toPos); } - }); - }; - - // sync text of document and editor - syncText = () => { - const text = doc.getRoot().content; - text.onChanges(changeEventHandler); - editor.setValue(text.toString()); - }; - syncText(); - editor.addKeyMap(menu.codeKeyMap); - editor.setOption('keyMap', menu.codeKeyMap); - editor.getDoc().clearHistory(); - editor.focus(); - }, - [client, doc, menu], - ); - - useEffect(() => { - for (const [id, peer] of Object.entries(peers)) { - if (cursorMapRef.current.has(id) && peer.status === ConnectionStatus.Disconnected) { - disconnectCursor(id); - } else if (!cursorMapRef.current.has(id) && peer.status === ConnectionStatus.Connected) { - connectCursor(id, peer.metadata); - } - } - }, [peers]); + } + }); + }; + + // sync text of document and editor + syncText = () => { + const text = doc.getRoot().content; + text.onChanges(changeEventHandler); + editor.setValue(text.toString()); + }; + syncText(); + editor.addKeyMap(menu.codeKeyMap); + editor.setOption('keyMap', menu.codeKeyMap); + editor.getDoc().clearHistory(); + editor.focus(); + }, [editor]); const options = useMemo(() => { const opts = {