Просмотр исходного кода

WIP: update original doc with collaborative changes

Yuki Takei 1 год назад
Родитель
Сommit
ff0222e00c

+ 78 - 4
packages/editor/src/client/services-internal/unified-merge-view/use-unified-merge-view.ts

@@ -1,8 +1,47 @@
 import { useEffect } from 'react';
 
-import { unifiedMergeView } from '@codemirror/merge';
+import {
+  unifiedMergeView,
+  originalDocChangeEffect,
+  getOriginalDoc,
+} from '@codemirror/merge';
+import { ChangeSet, type ChangeSpec, StateField } from '@codemirror/state';
 
+import type { Delta } from '../../../interfaces';
 import type { UseCodeMirrorEditor } from '../../services';
+import { collaborativeChange } from '../../stores/use-collaborative-editor-mode';
+
+
+function deltaToChangeSet(delta: Delta, docLength: number): ChangeSet {
+  const changes: ChangeSpec[] = [];
+  let pos = 0;
+
+  for (const op of delta) {
+    if (op.retain != null) {
+      pos += op.retain;
+    }
+
+    if (op.delete != null) {
+      changes.push({
+        from: pos,
+        to: pos + op.delete,
+      });
+    }
+
+    if (op.insert != null) {
+      changes.push({
+        from: pos,
+        insert: typeof op.insert === 'string' ? op.insert : '',
+      });
+      if (typeof op.insert === 'string') {
+        pos += op.insert.length;
+      }
+    }
+  }
+
+  return ChangeSet.of(changes, docLength);
+}
+
 
 export const useUnifiedMergeView = (
     unifiedMergeViewEnabled?: boolean,
@@ -10,17 +49,52 @@ export const useUnifiedMergeView = (
 ): void => {
 
   useEffect(() => {
-    if (unifiedMergeViewEnabled == null) {
+    if (unifiedMergeViewEnabled == null || !codeMirrorEditor) {
       return;
     }
+
     const extension = unifiedMergeViewEnabled ? [
       unifiedMergeView({
-        original: codeMirrorEditor?.getDoc() ?? '',
+        original: codeMirrorEditor.getDoc(),
       }),
     ] : [];
 
-    const cleanupFunction = codeMirrorEditor?.appendExtensions?.(extension);
+    const cleanupFunction = codeMirrorEditor.appendExtensions(extension);
     return cleanupFunction;
   }, [codeMirrorEditor, unifiedMergeViewEnabled]);
 
+  useEffect(() => {
+    if (!unifiedMergeViewEnabled || codeMirrorEditor == null) {
+      return;
+    }
+
+    const extension = unifiedMergeViewEnabled ? [
+      // collaborative changes を追跡し、original document を更新する
+      StateField.define({
+        create: () => null,
+        update(value, tr) {
+          if (codeMirrorEditor.view?.state == null) {
+            return value;
+          }
+
+          for (const e of tr.effects) {
+            if (e.is(collaborativeChange)) {
+              // original document を更新
+              const changeSet = deltaToChangeSet(e.value, getOriginalDoc(codeMirrorEditor.view.state).length);
+              const effect = originalDocChangeEffect(tr.state, changeSet);
+              setTimeout(() => {
+                codeMirrorEditor.view?.dispatch({
+                  effects: effect,
+                });
+              }, 0);
+            }
+          }
+          return value;
+        },
+      }),
+    ] : [];
+
+    const cleanupFunction = codeMirrorEditor.appendExtensions(extension);
+    return cleanupFunction;
+  }, [codeMirrorEditor, unifiedMergeViewEnabled]);
 };

+ 20 - 9
packages/editor/src/client/stores/use-collaborative-editor-mode.ts

@@ -1,5 +1,6 @@
 import { useEffect, useState } from 'react';
 
+import { StateEffect } from '@codemirror/state';
 import { keymap } from '@codemirror/view';
 import type { IUserHasId } from '@growi/core/dist/interfaces';
 import { useGlobalSocket } from '@growi/core/dist/swr';
@@ -8,6 +9,7 @@ import { SocketIOProvider } from 'y-socket.io';
 import * as Y from 'yjs';
 
 import { userColor } from '../../consts';
+import type { Delta } from '../../interfaces';
 import type { UseCodeMirrorEditor } from '../services';
 
 type UserLocalState = {
@@ -17,6 +19,9 @@ type UserLocalState = {
   colorLight: string;
 }
 
+// collaborative changesを通知するための StateEffect
+export const collaborativeChange = StateEffect.define<Delta>();
+
 export const useCollaborativeEditorMode = (
     isEnabled: boolean,
     user?: IUserHasId,
@@ -120,19 +125,25 @@ export const useCollaborativeEditorMode = (
     const ytext = ydoc.getText('codemirror');
     const undoManager = new Y.UndoManager(ytext);
 
-    codeMirrorEditor.initDoc(ytext.toString());
-
-    const cleanupYUndoManagerKeymap = codeMirrorEditor.appendExtensions([
+    const extensions = [
       keymap.of(yUndoManagerKeymap),
-    ]);
-    const cleanupYCollab = codeMirrorEditor.appendExtensions([
       yCollab(ytext, provider.awareness, { undoManager }),
-    ]);
+    ];
+
+    // Setup observer for collaborative changes
+    ytext.observe((event) => {
+      if (event.transaction.local) return;
+
+      // 外部からの変更があったことを通知
+      codeMirrorEditor.view?.dispatch({
+        effects: collaborativeChange.of(event.delta),
+      });
+    });
+
+    const cleanupFunctions = extensions.map(ext => codeMirrorEditor.appendExtensions([ext]));
 
     return () => {
-      cleanupYUndoManagerKeymap?.();
-      cleanupYCollab?.();
-      // clean up editor
+      cleanupFunctions.forEach(cleanup => cleanup?.());
       codeMirrorEditor.initDoc('');
     };
   }, [codeMirrorEditor, provider, ydoc]);