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.