Transactions
How transactions work in CodeMirror 6: document changes, selection updates, annotations, effects, filters, and dispatch.
Transactions
Transactions are the mechanism for all state changes in CodeMirror 6. Every modification to the document, selection, or extension state goes through a transaction. A transaction describes a set of changes to apply to an EditorState, producing a new state.
What Is a Transaction
A transaction wraps one or more changes and metadata into a single atomic update. You create transactions using state.update() or by passing a transaction spec to view.dispatch().
import { EditorState } from "@codemirror/state";
let state = EditorState.create({ doc: "Hello" });
let tr = state.update({
changes: { from: 5, insert: " World" },
});
console.log(tr.state.doc.toString()); // "Hello World"
The transaction (tr) holds:
tr.state— the new state after applying the changestr.changes— the change set describing what was modifiedtr.selection— the new selection (if it changed)tr.effects— any state effects included in the transactiontr.docChanged— boolean indicating whether the document changed
Creating Changes
Changes describe insertions, deletions, and replacements. They use character positions in the original document.
Insertion
view.dispatch({
changes: { from: 0, insert: "// " },
});
Deletion
view.dispatch({
changes: { from: 0, to: 5 },
});
Replacement
view.dispatch({
changes: { from: 0, to: 5, insert: "Goodbye" },
});
Multiple Changes
You can include multiple changes in a single transaction. All positions refer to the original document — CodeMirror adjusts offsets internally.
view.dispatch({
changes: [
{ from: 0, insert: "// " },
{ from: 10, to: 15, insert: "replacement" },
],
});
⚠️ Warning
When specifying multiple changes, the from positions must be in ascending order. CodeMirror throws an error if they are not.
Selection Changes
Update the selection by including a selection field:
import { EditorSelection } from "@codemirror/state";
// Move cursor to position 10
view.dispatch({
selection: { anchor: 10 },
});
// Select a range
view.dispatch({
selection: EditorSelection.single(0, 10),
});
// Move cursor to end of document
view.dispatch({
selection: { anchor: view.state.doc.length },
});
You can combine document changes with selection changes. When you do, the selection positions refer to the document after the changes:
view.dispatch({
changes: { from: 0, insert: "prefix " },
selection: { anchor: 7 }, // Position in the new document
});
Annotations
Annotations attach metadata to a transaction. They do not change the state, but other extensions can read them to understand the context of a change.
import { Annotation } from "@codemirror/state";
let myAnnotation = Annotation.define();
view.dispatch({
changes: { from: 0, insert: "text" },
annotations: myAnnotation.of("programmatic"),
});
CodeMirror includes a built-in annotation, Transaction.userEvent, that describes the type of user action:
import { Transaction } from "@codemirror/state";
view.dispatch({
changes: { from: 0, insert: "text" },
annotations: Transaction.userEvent.of("input.type"),
});
Extensions like the undo history read Transaction.userEvent to decide whether a change should be undoable, groupable, or ignored.
Another built-in annotation is Transaction.addToHistory. Set it to false to prevent a change from being added to the undo history:
view.dispatch({
changes: { from: 0, insert: "auto-generated" },
annotations: Transaction.addToHistory.of(false),
});
Effects and State Effects
State effects are typed signals that can be included in a transaction. State fields can listen for specific effects to update their state.
import { StateEffect, StateField } from "@codemirror/state";
// Define an effect type
let setLanguage = StateEffect.define();
// Define a state field that responds to the effect
let languageField = StateField.define({
create() {
return "plaintext";
},
update(value, tr) {
for (let effect of tr.effects) {
if (effect.is(setLanguage)) {
return effect.value;
}
}
return value;
},
});
// Dispatch the effect
view.dispatch({
effects: setLanguage.of("javascript"),
});
Effects can carry any value. The define() call optionally takes a map function that adjusts the effect’s value when document changes shift positions:
let addHighlight = StateEffect.define({
map: (value, mapping) => ({
from: mapping.mapPos(value.from),
to: mapping.mapPos(value.to),
}),
});
Dispatching Transactions
When working with EditorView, call view.dispatch():
// Dispatching a transaction spec
view.dispatch({
changes: { from: 0, insert: "new text" },
});
// Dispatching a pre-built transaction
let tr = view.state.update({
changes: { from: 0, insert: "new text" },
});
view.dispatch(tr);
When working with EditorState directly (without a view), use state.update() to get the new state without dispatching:
let tr = state.update({
changes: { from: 0, insert: "text" },
});
let newState = tr.state;
Transaction Filters
Transaction filters let extensions intercept and modify transactions before they are applied.
Change Filters
EditorState.changeFilter can suppress or modify document changes:
import { EditorState } from "@codemirror/state";
// Reject all changes (read-only behavior)
let readOnly = EditorState.changeFilter.of(() => false);
The filter function receives a transaction and returns true (allow all changes), false (reject all changes), or an array of ranges to allow.
Transaction Filters
EditorState.transactionFilter can modify the entire transaction spec:
let forceUpperCase = EditorState.transactionFilter.of((tr) => {
if (!tr.docChanged) return tr;
let changes = [];
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
let upper = inserted.toString().toUpperCase();
changes.push({ from: fromB, to: toB, insert: upper });
});
return [tr, { changes, sequential: true }];
});
Transaction Extender
EditorState.transactionExtender adds extra effects or annotations to transactions:
let timestampAnnotation = Annotation.define();
let addTimestamp = EditorState.transactionExtender.of((tr) => {
if (tr.docChanged) {
return { annotations: timestampAnnotation.of(Date.now()) };
}
return null;
});
Example: Replacing All Text
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: "Completely new content",
},
});
Example: Inserting at Cursor Position
let cursor = view.state.selection.main.head;
view.dispatch({
changes: { from: cursor, insert: "inserted text" },
selection: { anchor: cursor + "inserted text".length },
});
Try the buttons below to see how transactions modify the editor state. “Insert Text” inserts at the cursor, “Replace All” replaces the entire document, and “Undo” reverts the last change:
Example: Wrapping Selected Text
let { from, to } = view.state.selection.main;
let selected = view.state.sliceDoc(from, to);
view.dispatch({
changes: { from, to, insert: `[${selected}]` },
selection: { anchor: from + 1, head: from + 1 + selected.length },
});
Example: Replacing the Selection
let { from, to } = view.state.selection.main;
view.dispatch({
changes: { from, to, insert: "replacement" },
});