Extensions
CodeMirror 6 extension system: facets, state fields, view plugins, compartments, precedence, and built-in extensions.
Extensions
CodeMirror 6 has almost no built-in behavior. Line numbers, syntax highlighting, keybindings, undo history -- all of these are extensions. The extension system is the primary way to configure and customize the editor.
What Is an Extension
An extension is a value that you pass to EditorState.create() via the extensions array. It can be:
A facet value (created with
facet.of(value))A state field (created with
StateField.define())A view plugin (created with
ViewPlugin.define()orViewPlugin.fromClass())An array of other extensions (nested arrays are flattened)
A compartment wrapper (created with
compartment.of(extension))
import { EditorState } from "@codemirror/state";
import { EditorView, keymap, lineNumbers } from "@codemirror/view";
import { defaultKeymap } from "@codemirror/commands";
let view = new EditorView({
extensions: [
lineNumbers(),
keymap.of(defaultKeymap),
EditorView.lineWrapping,
EditorState.tabSize.of(2),
],
parent: document.body,
});Extension Arrays
Extensions compose through flat arrays. Nesting is allowed -- CodeMirror flattens all arrays:
let editorSetup = [lineNumbers(), EditorView.lineWrapping];
let keybindings = [keymap.of(defaultKeymap)];
// These are equivalent:
let extensions = [editorSetup, keybindings];
// Flattens to: [lineNumbers(), EditorView.lineWrapping, keymap.of(defaultKeymap)]This makes it natural to create reusable extension bundles as arrays.
Compartments
Compartments allow you to reconfigure extensions after the editor is created. Wrap an extension in a compartment, and later dispatch an effect to replace it.
import { Compartment } from "@codemirror/state";
let language = new Compartment();
let tabSize = new Compartment();
let view = new EditorView({
extensions: [
language.of([]),
tabSize.of(EditorState.tabSize.of(4)),
],
parent: document.body,
});Reconfigure a compartment by dispatching its reconfigure effect:
// Change tab size to 2
view.dispatch({
effects: tabSize.reconfigure(EditorState.tabSize.of(2)),
});
// Load a language
import { javascript } from "@codemirror/lang-javascript";
view.dispatch({
effects: language.reconfigure(javascript()),
});
// Remove the language (pass empty extension)
view.dispatch({
effects: language.reconfigure([]),
});You can read the current content of a compartment with compartment.get():
let currentLang = language.get(view.state);Tip
Compartments are the intended way to toggle extensions on/off or swap configurations dynamically. Do not recreate the entire editor to change settings.
Facets
A facet is a named extension point that collects values from multiple sources and combines them into a single output. Facets are defined with Facet.define().
Built-in Facets
CodeMirror includes several built-in facets:
EditorState.tabSize-- tab character width (default: 4)EditorState.readOnly-- whether the editor is read-onlyEditorState.phrases-- translation stringsEditorState.languageData-- language-specific configurationEditorView.contentAttributes-- HTML attributes on the content elementEditorView.editorAttributes-- HTML attributes on the editor wrapperEditorView.decorations-- decorations provided by extensionsEditorView.updateListener-- callbacks for state updates
Using Facets
Provide a value to a facet with .of():
let ext = EditorState.tabSize.of(2);Read a facet value with state.facet():
let size = state.facet(EditorState.tabSize); // 2Defining Custom Facets
import { Facet } from "@codemirror/state";
// A facet that combines values by taking the first one
let maxLineLength = Facet.define({
combine: (values) => (values.length ? Math.min(...values) : 80),
});
// Provide a value
let ext = maxLineLength.of(120);
// Read the combined value
let max = state.facet(maxLineLength);The combine function receives all provided values and returns a single result. Common combining strategies:
Take the first value (for single-value configuration)
Collect into an array (for multi-value configuration)
Merge or reduce (for aggregated configuration)
Computed Facets
A facet can derive its value from other parts of the state:
let lineCount = Facet.define({
combine: (values) => values[0],
});
let computedLineCount = lineCount.compute(["doc"], (state) => {
return state.doc.lines;
});StateField
A state field holds a value that updates with each transaction. It is the primary way to maintain custom state across updates.
import { StateField } from "@codemirror/state";
let wordCount = StateField.define({
create(state) {
return countWords(state.doc.toString());
},
update(value, tr) {
if (tr.docChanged) {
return countWords(tr.state.doc.toString());
}
return value;
},
});
function countWords(text) {
return text.split(/\s+/).filter(Boolean).length;
}State fields can also provide extensions through their provide option:
let myField = StateField.define({
create() {
return Decoration.none;
},
update(decos, tr) {
// update decorations...
return decos;
},
provide: (field) => EditorView.decorations.from(field),
});ViewPlugin
A view plugin runs code in response to view updates and has access to the DOM. Use it when you need side effects that state fields cannot handle.
import { ViewPlugin, ViewUpdate } from "@codemirror/view";
let charCounter = ViewPlugin.fromClass(
class {
dom;
constructor(view) {
this.dom = document.createElement("div");
this.dom.className = "char-count";
this.dom.textContent = `${view.state.doc.length} chars`;
view.dom.appendChild(this.dom);
}
update(update) {
if (update.docChanged) {
this.dom.textContent = `${update.state.doc.length} chars`;
}
}
destroy() {
this.dom.remove();
}
}
);View plugins can also provide decorations, event handlers, and other view-level values through their spec:
let myPlugin = ViewPlugin.fromClass(MyPluginClass, {
decorations: (plugin) => plugin.decorations,
eventHandlers: {
click(event, view) {
// handle click
return false;
},
},
});Extension Precedence
When multiple extensions provide values for the same facet or keymap, order matters. By default, extensions listed earlier take priority. You can override this with Prec:
import { Prec } from "@codemirror/state";
import { keymap } from "@codemirror/view";
let myKeymap = keymap.of([
{ key: "Ctrl-s", run: () => { console.log("save"); return true; } },
]);
// Ensure this keymap takes priority over others
let highPriority = Prec.high(myKeymap);
// Or guarantee it runs first
let highest = Prec.highest(myKeymap);The precedence levels, from highest to lowest:
Prec.highestPrec.high(default -- no wrapper)
Prec.lowPrec.lowest
Built-in Extensions
CodeMirror provides many extensions across its packages:
From @codemirror/view:
lineNumbers()-- gutter with line numbershighlightActiveLine()-- highlight the line the cursor is onhighlightActiveLineGutter()-- highlight the active line's gutterhighlightSpecialChars()-- render control characters visiblydrawSelection()-- custom selection drawing (required for multiple selections)dropCursor()-- show cursor position during drag-and-droprectangularSelection()-- Alt+drag rectangular selectioncrosshairCursor()-- crosshair cursor during Alt+dragEditorView.lineWrapping-- enable soft line wrappingplaceholder(text)-- placeholder text when the editor is empty
From @codemirror/commands:
history()-- undo/redo supportdefaultKeymap-- standard editing keybindingshistoryKeymap-- Ctrl-Z / Ctrl-Y keybindings
From @codemirror/search:
search()-- search and replace panelsearchKeymap-- Ctrl-F / Ctrl-H keybindingshighlightSelectionMatches()-- highlight text matching the selection
From @codemirror/autocomplete:
autocompletion()-- autocomplete popupcloseBrackets()-- auto-close brackets and quotescloseBracketsKeymap-- related keybindings
From @codemirror/language:
bracketMatching()-- highlight matching bracketsfoldGutter()-- code folding gutterindentOnInput()-- re-indent after typing specific characterssyntaxHighlighting()-- apply highlight styles to syntax tree
From @codemirror/lint:
linter()-- run a linting function and display diagnosticslintGutter()-- show lint markers in the gutter
basicSetup and minimalSetup
The codemirror package exports two convenience bundles:
basicSetup
A comprehensive set of extensions suitable for most use cases:
import { basicSetup } from "codemirror";
import { EditorView } from "@codemirror/view";
let view = new EditorView({
extensions: [basicSetup],
parent: document.body,
});basicSetup includes line numbers, active line highlighting, history, bracket matching, autocompletion, search, and many other common extensions.
minimalSetup
A smaller set for lightweight editors:
import { minimalSetup } from "codemirror";
import { EditorView } from "@codemirror/view";
let view = new EditorView({
extensions: [minimalSetup],
parent: document.body,
});minimalSetup includes just the default keymap, history, draw selection, drop cursor, special character highlighting, undo/redo, and bracket matching.
Note
Both bundles are just arrays of extensions. You can spread them into your own array and add or override specific extensions.
import { basicSetup } from "codemirror";
import { EditorView } from "@codemirror/view";
let view = new EditorView({
extensions: [
basicSetup,
EditorView.lineWrapping,
EditorState.tabSize.of(2),
// Additional extensions...
],
parent: document.body,
});Example: Creating a Custom Extension
Here is a state field that tracks whether the document has been modified from its initial content, paired with a view plugin that shows an indicator in the DOM:
import { StateField, StateEffect } from "@codemirror/state";
import { EditorView, ViewPlugin } from "@codemirror/view";
// Effect to mark the document as "saved"
let markSaved = StateEffect.define();
// State field tracking dirty status
let dirtyField = StateField.define({
create() {
return false;
},
update(isDirty, tr) {
for (let effect of tr.effects) {
if (effect.is(markSaved)) return false;
}
if (tr.docChanged) return true;
return isDirty;
},
});
// View plugin showing dirty indicator
let dirtyIndicator = ViewPlugin.fromClass(
class {
dom;
constructor(view) {
this.dom = document.createElement("div");
this.dom.className = "dirty-indicator";
this.updateDisplay(view.state.field(dirtyField));
view.dom.parentNode?.insertBefore(this.dom, view.dom);
}
update(update) {
let dirty = update.state.field(dirtyField);
if (dirty !== update.startState.field(dirtyField)) {
this.updateDisplay(dirty);
}
}
updateDisplay(isDirty) {
this.dom.textContent = isDirty ? "Unsaved changes" : "Saved";
}
destroy() {
this.dom.remove();
}
}
);
// Bundle into a single extension
function dirtyTracking() {
return [dirtyField, dirtyIndicator];
}Use it:
let view = new EditorView({
extensions: [basicSetup, dirtyTracking()],
parent: document.body,
});
// Later, mark as saved:
view.dispatch({
effects: markSaved.of(null),
});