Help with hooking up Convergence with React - Ace -Editor

Thank you @alec and @michael for such an amazing product when I saw the project demo code editor I was beyond impressed and ever since I wanted to implement the same

I am trying to Implement collaborative editing but I am running into certain roadblocks.

The problem I am facing is that convergence is straight up not working for me, I am getting errors left-right, and center, and when I don’t get errors there is no syncing of data between the two editor instances

I am using React Ace as a wrapper of Ace editor for react app and I am using refs to access the editor instance.

So I use useEffect to call connect with convergence and bind selection cursor and text

  const editorSetup = (model: RealTimeModel) => {
    const textModel = model.elementAt("text");
    initModel(textModel, AceEditorRef);
    initSharedCursors(textModel, AceEditorRef!.current!.editor);
    initSharedSelection(textModel, AceEditorRef);
  };

  useEffect(() => {
    Convergence.connectAnonymously(CONVERGENCE_URL, username)
      .then((d) => {
        setDomain(d);
        return domain?.models().openAutoCreate({
          collection: "example-ace",
          id: "ace-editor-example",
          ephemeral: true,
          data: {},
        });
      })
      .then((model) => {
        editorSetup(model);
      })
      .catch((error) => {
        console.error("Could not open model ", error);
      });
  }, []);

I followed the example for javascript for the binding methods verbatim – I just added some type definitions

Here is a gist Gist link for the following :

import { RealTimeElement, ModelReference } from "@convergence/convergence";
import {
  AceMultiCursorManager,
  AceMultiSelectionManager,
} from "@convergencelabs/ace-collab-ext";
import AceEditor from "react-ace";
import { Range as AceRange } from "ace-builds";
import { ColorAssigner } from "@convergence/color-assigner";
import { IAceEditor } from "react-ace/lib/types";
type AceEditorInstance = React.RefObject<AceEditor>;

export const initModel = (
  textModel: RealTimeElement<string>,
  AceEditorRef: AceEditorInstance,
  suppressEvents = false
) => {
  const editor = AceEditorRef.current!.editor;
  console.log("intiModel");
  console.log(editor);
  const session = editor.getSession();
  const document = session.getDocument();

  session.setValue(textModel.value());
  textModel.on("insert", (e: Record<string, any>) => {
    const pos = document.indexToPosition(e.index, 0);
    suppressEvents = true;
    document.insert(pos, e.value);
    suppressEvents = false;
  });

  textModel.on("remove", (e: Record<string, any>) => {
    const start = document.indexToPosition(e.index, 0);
    const end = document.indexToPosition(e.index + e.value.length, 0);
    suppressEvents = true;
  document.remove(new AceRange(start.row, start.column, end.row, end.column));
    suppressEvents = false;
  });

  textModel.on("value", function (e: Record<string, any>) {
    suppressEvents = true;
    document.setValue(e.value);
    suppressEvents = false;
  });

  editor.on("change", (delta) => {
    if (suppressEvents) {
      return;
    }

    const pos = document.positionToIndex(delta.start);
    switch (delta.action) {
      case "insert":
        //@ts-ignore
        textModel.insert(pos, delta.lines.join("\n"));
        break;
      case "remove":
        //@ts-ignore
        textModel.remove(pos, delta.lines.join("\n").length);
        break;
      default:
        throw new Error("unknown action: " + delta.action);
    }
  });
};

/////////////////////////////////////////////////////////////////////////////
// Cursor Binding
/////////////////////////////////////////////////////////////////////////////
export const initSharedCursors = (
  textModel: RealTimeElement<string>,
  editor: IAceEditor,
  suppressEvents = false,
  cursorKey = "cursor"
) => {
  console.log("insidde initSharedCursor");
  console.log(editor);
  const session = editor.getSession();
  const document = session.getDocument();

  const cursorManager = new AceMultiCursorManager(editor.getSession());
  //@ts-ignore
  const cursorReference = textModel.indexReference(cursorKey);
  console.log(cursorReference);
  const references = textModel.references({ key: cursorKey });
  references.forEach((reference) => {
    if (!reference.isLocal()) {
      addCursor(editor, reference, cursorManager);
    }
  });

  setLocalCursor(editor, cursorReference);
  cursorReference.share();

  editor
    .getSession()
    .selection.on("changeCursor", () => setLocalCursor(editor, document));

  textModel.on("reference", (e: Record<string, any>) => {
    if (e.reference.key() === cursorKey) {
      addCursor(editor, e.reference, cursorManager);
    }
  });
};

function setLocalCursor(editor: IAceEditor, cursorReference: any) {
  const position = editor.getCursorPosition();
  const document = editor.getSession().getDocument();
  const index = document.positionToIndex(position);
  cursorReference.set(index);
}

function addCursor(
  editor: IAceEditor,
  reference: ModelReference<any>,
  cursorManager: AceMultiCursorManager
) {
  const colorAssigner = new ColorAssigner(ColorAssigner.Palettes.LIGHT_12);
  const color = colorAssigner.getColorAsHex(reference.sessionId());
  const remoteCursorIndex = reference.value();
  cursorManager.addCursor(
    reference.sessionId(),
    "user-name",
    color,
    remoteCursorIndex
  );

  reference.on("cleared", () =>
    cursorManager.clearCursor(reference.sessionId())
  );
  reference.on("disposed", () =>
    cursorManager.removeCursor(reference.sessionId())
  );
  reference.on("set", () => {
    const cursorIndex = reference.value();
    cursorManager.setCursor(reference.sessionId(), cursorIndex);

    // check alert for 0 shizz
    // const document = editor.getSession().getDocument();
    // const cursorRow = document.indexToPosition(cursorIndex, 0).row;

    // if (radarView.hasView(reference.sessionId())) {
    //   radarView.setCursorRow(reference.sessionId(), cursorRow);
    // }
  });
}

/////////////////////////////////////////////////////////////////////////////
// Selection Binding
/////////////////////////////////////////////////////////////////////////////
export const initSharedSelection = (
  textModel: RealTimeElement<any>,
  AceEditorRef: AceEditorInstance,
  suppressEvents = false,
  selectionKey = "selection"
) => {
  const editor = AceEditorRef.current!.editor;
  const session = editor.getSession();
  console.log(editor);
  const selectionManager = new AceMultiSelectionManager(editor.getSession());

  //@ts-ignore
  const selectionReference = textModel.rangeReference(selectionKey);
  setLocalSelection(editor, selectionReference);
  selectionReference.share();

  session.selection.on("changeSelection", () =>
    setLocalSelection(editor, selectionReference)
  );

  const references = textModel.references({ key: selectionKey });
  references.forEach((reference) => {
    if (!reference.isLocal()) {
      addSelection(editor, reference, selectionManager);
    }
  });

  textModel.on("reference", (e: Record<string, any>) => {
    console.log("textSelection");
    console.log(e);
    if (e.reference.key() === selectionKey) {
      addSelection(editor, e.reference, selectionManager);
    }
  });
};

function setLocalSelection(editor: IAceEditor, selectionReference: any) {
  const document = editor.getSession().getDocument();
  if (!editor.selection.isEmpty()) {
    const aceRanges = editor.selection.getAllRanges();
    const indexRanges = aceRanges.map((aceRagne) => {
      const start = document.positionToIndex(aceRagne.start);
      const end = document.positionToIndex(aceRagne.end);
      return { start: start, end: end };
    });

    selectionReference.set(indexRanges);
  } else if (selectionReference.isSet()) {
    selectionReference.clear();
  }
}

function addSelection(
  editor: IAceEditor,
  reference: ModelReference<any>,
  selectionManager: AceMultiSelectionManager
) {
  const colorAssigner = new ColorAssigner(ColorAssigner.Palettes.LIGHT_12);
  const color = colorAssigner.getColorAsHex(reference.sessionId());
  const remoteSelection = reference
    .values()
    .map((range) => toAceRange(editor, range));
  selectionManager.addSelection(
    reference.sessionId(),
    reference.user().username,
    color,
    //@ts-ignore
    remoteSelection
  );

  reference.on("cleared", () =>
    selectionManager.clearSelection(reference.sessionId())
  );
  reference.on("disposed", () =>
    selectionManager.removeSelection(reference.sessionId())
  );
  reference.on("set", () => {
    selectionManager.setSelection(
      reference.sessionId(),
      //@ts-ignore
      reference.values().map((range) => toAceRange(editor, range))
    );
  });
}

function toAceRange(editor: IAceEditor, range: Record<string, number>) {
  if (typeof range !== "object") {
    return null;
  }

  let start = range.start;
  let end = range.end;

  if (start > end) {
    const temp = start;
    start = end;
    end = temp;
  }

  const document = editor.getSession().getDocument();
  const rangeAnchor = document.indexToPosition(start, 0);
  const rangeLead = document.indexToPosition(end, 0);
  return new AceRange(
    rangeAnchor.row,
    rangeAnchor.column,
    rangeLead.row,
    rangeLead.column
  );
}

Any Help would be highly appreciated, and thank you once again :slight_smile:

I solved the issue by changing to Monaco editor which seemed easier to hookup with convergence and react, if someone is interested in looking at the code go to my GitHub.