Architecture
Overview of CodeMirror 6's architecture: functional core, immutable state, transaction-based updates, and the extension system.
Architecture
CodeMirror 6 is a ground-up rewrite that separates concerns into a functional core and an imperative shell. This page covers the fundamental design decisions that shape how you work with the editor.
Functional Core and Imperative Shell
The architecture splits into two layers:
EditorState(from@codemirror/state) — the functional core. It holds the document, selection, and all configured state. It is immutable; you never mutate it directly.EditorView(from@codemirror/view) — the imperative shell. It owns the DOM, handles rendering, and coordinates user interaction. It holds a reference to the currentEditorStateand updates it through transactions.
This separation means that state logic can be tested and reasoned about without touching the DOM.
Immutable State Model
EditorState is immutable. To change anything — the document content, the selection, a state field — you create a transaction that describes the change. Applying that transaction produces a new EditorState. The old state remains unchanged.
import { EditorState } from "@codemirror/state";
let state = EditorState.create({ doc: "Hello" });
let tr = state.update({ changes: { from: 5, insert: " World" } });
let newState = tr.state;
console.log(state.doc.toString()); // "Hello"
console.log(newState.doc.toString()); // "Hello World"
This is fundamentally different from CodeMirror 5, which used a mutable model where calling methods like setValue() or replaceRange() directly mutated the editor’s internal state.
Transaction-Based Updates
Every state change goes through a transaction. A transaction can contain:
- Document changes (insertions, deletions, replacements)
- Selection updates
- Annotations (metadata attached to the transaction)
- State effects (signals for state fields and other extensions)
When working with EditorView, you call view.dispatch(transaction) to apply the transaction, which updates both the state and the DOM.
// Dispatch a change through the view
view.dispatch({
changes: { from: 0, insert: "// " },
});
Transactions are described in detail on the Transactions page.
Extension System
CodeMirror 6 has no built-in behavior beyond rendering text. Everything else — line numbers, syntax highlighting, keybindings, bracket matching — is provided through extensions.
An extension is a value (or array of values) that you pass to EditorState.create(). Extensions compose by flattening into a single array, so you can group related extensions into bundles and combine them freely.
import { EditorState } from "@codemirror/state";
import { EditorView, keymap, lineNumbers } from "@codemirror/view";
import { defaultKeymap } from "@codemirror/commands";
let state = EditorState.create({
doc: "Hello",
extensions: [lineNumbers(), keymap.of(defaultKeymap)],
});
The extension system is covered on the Extensions page.
Facets and State Fields
Two core mechanisms make extensions work:
Facets
A facet is a named extension point that collects values from multiple providers and combines them. Facets are used for configuration that needs to merge contributions from different extensions.
For example, EditorState.tabSize is a facet. Multiple extensions could theoretically provide a tab size, but the facet’s combining function determines the final value. Some facets combine by taking the first provided value; others collect all values into an array.
import { EditorState } from "@codemirror/state";
let state = EditorState.create({
extensions: [EditorState.tabSize.of(4)],
});
console.log(state.facet(EditorState.tabSize)); // 4
State Fields
A state field holds a piece of computed state that updates with each transaction. Unlike facets, state fields maintain their own value across transactions using create and update functions.
import { StateField } from "@codemirror/state";
let wordCount = StateField.define({
create(state) {
return state.doc.toString().split(/\s+/).filter(Boolean).length;
},
update(value, tr) {
if (tr.docChanged) {
return tr.state.doc.toString().split(/\s+/).filter(Boolean).length;
}
return value;
},
});
How Extensions Compose
Extensions are flat arrays. When you pass nested arrays, they get flattened:
let myBundle = [lineNumbers(), keymap.of(defaultKeymap)];
let state = EditorState.create({
extensions: [myBundle, someOtherExtension],
// Equivalent to: [lineNumbers(), keymap.of(defaultKeymap), someOtherExtension]
});
This makes it straightforward to create reusable bundles. The basicSetup package is itself just a large array of extensions bundled together.
When extensions conflict (for example, two keymaps binding the same key), the order matters: earlier extensions in the array take precedence by default. You can override this with Prec.highest, Prec.high, Prec.low, and Prec.lowest.
Comparison with CodeMirror 5
| Aspect | CodeMirror 5 | CodeMirror 6 |
|---|---|---|
| State model | Mutable, in-place updates | Immutable, transaction-based |
| DOM | Single textarea or contentEditable | Custom rendering with EditorView |
| Configuration | Options object with setOption() | Extensions, facets, and compartments |
| Extending | Event listeners, overlays, addons | Facets, state fields, view plugins |
| Bundling | Single monolithic script | Modular packages (@codemirror/*) |
| TypeScript | Community type definitions | Written in TypeScript, types included |
The modular design means you only bundle the features you use, and the immutable state model makes it possible to reason about changes more predictably than CM5’s event-driven mutation approach.