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 just immutable values, and their updated forms 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.

We'll simply use a single start state in this example—though that isn't necessary, as long as both states have the same document. It might help, if the document can be big, to give both states the same Text instance as starting document, so that most of the document tree structure can be shared.

import {EditorState, basicSetup} from "@codemirror/next/basic-setup"

let startState = EditorState.create({
  doc: "The document\nis\nshared",
  extensions: basicSetup
})

Next comes the code that will be responsible for broadcasting changes between the editors. To work around some cyclic reference issues (the dispatch functions need access to the view, but are passed when initializing the view), the code stores the views in an array and refers to them by index.

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/next/view"
import {Transaction, Annotation} from "@codemirror/next/state"

let views: EditorView[] = []

let syncAnnotation = Annotation.define<boolean>()

function syncDispatch(from: number, to: number) {
  return (tr: Transaction) => {
    views[from].update([tr])
    if (!tr.changes.empty && !tr.annotation(syncAnnotation))
      views[to].dispatch({changes: tr.changes,
                          annotations: syncAnnotation.of(true)})
  }
}

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

views.push(
  new EditorView({
    state: startState,
    parent: document.querySelector("#editor1"),
    dispatch: syncDispatch(0, 1)
  }),
  new EditorView({
    state: startState,
    parent: document.querySelector("#editor2"),
    dispatch: syncDispatch(1, 0)
  })
)

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.