constructor(textType, codeMirror, awareness = null, {
    yUndoManager = null
  } = {}) {
    super();
    const doc = textType.doc;
    const cmDoc = codeMirror.getDoc();
    this.doc = doc;
    this.type = textType;
    this.cm = codeMirror;
    this.cmDoc = cmDoc;
    this.awareness = awareness || null;
    this.yUndoManager = yUndoManager;

    this._onStackItemAdded = ({
      stackItem,
      changedParentTypes
    }) => {
      // only store metadata if this type was affected
      if (changedParentTypes.has(textType) && this._beforeChangeSelection) {
        stackItem.meta.set(this, this._beforeChangeSelection);
      }
    };

    this._onStackItemPopped = ({
      stackItem
    }) => {
      const sel = stackItem.meta.get(this);

      if (sel) {
        const anchor = Y.createAbsolutePositionFromRelativePosition(sel.anchor, doc).index;
        const head = Y.createAbsolutePositionFromRelativePosition(sel.head, doc).index;
        codeMirror.setSelection(codeMirror.posFromIndex(anchor), codeMirror.posFromIndex(head));

        this._beforeChange();
      }
    };

    if (yUndoManager) {
      yUndoManager.trackedOrigins.add(this); // track changes performed by this editor binding

      const editorUndo = cm => {
        // Keymaps always start with an active operation.
        // End the current operation so that the event is fired at the correct moment.
        // @todo check cm.curOp in typeListener and endOperation always.
        cm.endOperation();
        yUndoManager.undo();
        cm.startOperation();
      };

      const editorRedo = cm => {
        cm.endOperation();
        yUndoManager.redo();
        cm.startOperation();
      };

      codeMirror.addKeyMap({
        // pc
        'Ctrl-Z': editorUndo,
        'Shift-Ctrl-Z': editorRedo,
        'Ctrl-Y': editorRedo,
        // mac
        'Cmd-Z': editorUndo,
        'Shift-Cmd-Z': editorRedo,
        'Cmd-Y': editorRedo
      });
      yUndoManager.on('stack-item-added', this._onStackItemAdded);
      yUndoManager.on('stack-item-popped', this._onStackItemPopped);
    }

    this._mux = (0, _mutex.createMutex)(); // set initial value

    cmDoc.setValue(textType.toString()); // observe type and target

    this._typeObserver = event => typeObserver(this, event);

    this._targetObserver = (instance, changes) => {
      if (instance.getDoc() === cmDoc) {
        targetObserver(this, changes);
      }
    };

    this._cursors = new Map();
    this._changedCursors = new Set();
    this._debounceCursorEvent = eventloop.createDebouncer(10);

    this._awarenessListener = event => {
      if (codeMirror.getDoc() !== cmDoc) {
        return;
      }

      const f = clientId => {
        if (clientId !== doc.clientID) {
          this._changedCursors.add(clientId);
        }
      };

      event.added.forEach(f);
      event.removed.forEach(f);
      event.updated.forEach(f);

      if (this._changedCursors.size > 0) {
        this._debounceCursorEvent(() => {
          this._changedCursors.forEach(clientId => {
            updateRemoteSelection(doc, codeMirror, textType, this._cursors, clientId, awareness);
          });

          this._changedCursors.clear();
        });
      }
    };

    this._pendingCursorEvent = false;

    this._cursorListener = () => {
      if (codeMirror.getDoc() === cmDoc) {
        this._pendingCursorEvent = true;
        setTimeout(() => {
          if (this._pendingCursorEvent) {
            this._pendingCursorEvent = false;
            this.emit('cursorActivity', [codeMirror]);
          }
        }, 0);
      }
    };

    this.on('cursorActivity', () => {
      codemirrorCursorActivity(doc, codeMirror, textType, awareness);
    });

    this._blurListeer = () => awareness.setLocalStateField('cursor', null);

    textType.observe(this._typeObserver); // @ts-ignore

    codeMirror.on('changes', this._targetObserver);
    /**
     * @type {{ anchor: Y.RelativePosition, head: Y.RelativePosition } | null}
     */

    this._beforeChangeSelection = null;

    this._beforeChange = () => {
      // update the the beforeChangeSelection that is stored befor each change to the editor (except when applying remote changes)
      this._mux(() => {
        // store the selection before the change is applied so we can restore it with the undo manager.
        const anchor = Y.createRelativePositionFromTypeIndex(textType, codeMirror.indexFromPos(codeMirror.getCursor('anchor')));
        const head = Y.createRelativePositionFromTypeIndex(textType, codeMirror.indexFromPos(codeMirror.getCursor('head')));
        this._beforeChangeSelection = {
          anchor,
          head
        };
      });
    };

    codeMirror.on('beforeChange', this._beforeChange);

    if (awareness) {
      codeMirror.on('swapDoc', this._blurListeer);
      awareness.on('change', this._awarenessListener); // @ts-ignore

      codeMirror.on('cursorActivity', this._cursorListener);
      codeMirror.on('blur', this._blurListeer);
      codeMirror.on('focus', this._cursorListener);
    }
  }
@param {Y.Text} textType
@param {import('codemirror').Editor} codeMirror
@param {any | null} [awareness]
@param {{ yUndoManager?: Y.UndoManager }} [options]