Example: Split View

Though it is possible to create multiple views from a single editor state, those views will not, by themselves, stay in sync. States are immutable values, and their updated forms held in the different views will simply diverge.

Thus, to keep the content of two views in sync, you'll have to forward changes made in one view to the other. A good place to do this is either an overridden dispatch function, or an update listener. In this example, we'll use the former.

To make sure there's only one undo history, we'll set up one state with the history extension, and one without. The state for the main editor is set up as normal.

import {EditorState} from "@codemirror/state"
import {defaultKeymap, historyKeymap, history} from "@codemirror/commands"
import {drawSelection, keymap, lineNumbers} from "@codemirror/view"

let startState = EditorState.create({
  doc: "The document\nis\nshared",
  extensions: [
    history(),
    drawSelection(),
    lineNumbers(),
    keymap.of([
      ...defaultKeymap,
      ...historyKeymap,
    ])
  ]
})

The state for the second editor doesn't track history state, and binds history-related keys to perform undo/redo in the main editor.

import {undo, redo} from "@codemirror/commands"

let otherState = EditorState.create({
  doc: startState.doc,
  extensions: [
    drawSelection(),
    lineNumbers(),
    keymap.of([
      ...defaultKeymap,
      {key: "Mod-z", run: () => undo(mainView)},
      {key: "Mod-y", mac: "Mod-Shift-z", run: () => redo(mainView)}
    ])
  ]
})

Next comes the code that will be responsible for broadcasting changes between the editors.

In order to be able to distinguish between regular transactions caused by the user and synchronizing transactions from the other editor, we define an annotation that will be used to tag such transactions. Whenever a transaction that makes document changes and isn't a synchronizing transaction comes in, it is also dispatched to the other editor.

import {EditorView} from "@codemirror/view"
import {Transaction, Annotation} from "@codemirror/state"

let syncAnnotation = Annotation.define<boolean>()

function syncDispatch(tr: Transaction, view: EditorView, other: EditorView) {
  view.update([tr])
  if (!tr.changes.empty && !tr.annotation(syncAnnotation)) {
    let annotations: Annotation<any>[] = [syncAnnotation.of(true)]
    let userEvent = tr.annotation(Transaction.userEvent)
    if (userEvent) annotations.push(Transaction.userEvent.of(userEvent))
    other.dispatch({changes: tr.changes, annotations})
  }
}

Now we can create the views, and see them in action.

let mainView = new EditorView({
  state: startState,
  parent: document.querySelector("#editor1"),
  dispatch: tr => syncDispatch(tr, mainView, otherView)
})

let otherView = new EditorView({
  state: otherState,
  parent: document.querySelector("#editor2"),
  dispatch: tr => syncDispatch(tr, otherView, mainView)
})

The first editor:

And the second:

Note that non-document state (like selection) isn't shared between the editors. For most such state, it wouldn't be appropriate to share it. But there might be cases where additional elements (such as, say, breakpoint information) needs to be shared. You'll have to set up your syncing code to forward updates to that shared state (probably as effects) alongside the document changes.