Dynamic Configuration
Using Compartments to dynamically swap extensions at runtime in CodeMirror 6: toggling features, switching themes, and runtime reconfiguration.
Compartments
In CodeMirror 6, extensions are normally fixed at the time you create the EditorState. A Compartment lets you change extensions after the editor is already running. You wrap an extension in a compartment, then later dispatch an effect to replace its contents.
import { Compartment } from "@codemirror/state";
const myCompartment = new Compartment();Creating a Compartment
A compartment wraps an initial extension value using its of() method. You pass the compartment's of() result as part of the extensions array when creating the state.
import { Compartment } from "@codemirror/state";
import { EditorState } from "@codemirror/state";
import { lineNumbers } from "@codemirror/view";
const lineNumberCompartment = new Compartment();
const state = EditorState.create({
doc: "Hello",
extensions: [
lineNumberCompartment.of(lineNumbers()),
],
});Reconfiguring a Compartment
To change the extension inside a compartment at runtime, dispatch a transaction with a reconfigure effect:
// Remove the extension (pass an empty array)
view.dispatch({
effects: lineNumberCompartment.reconfigure([]),
});
// Re-add the extension
view.dispatch({
effects: lineNumberCompartment.reconfigure(lineNumbers()),
});Passing an empty array [] effectively disables the extension. Passing a new extension replaces whatever was there before.
Example: Toggling Line Numbers
import { Compartment } from "@codemirror/state";
import { EditorState } from "@codemirror/state";
import { EditorView, lineNumbers } from "@codemirror/view";
const lineNumberCompartment = new Compartment();
let lineNumbersEnabled = true;
const state = EditorState.create({
doc: "Toggle line numbers on and off.",
extensions: [
lineNumberCompartment.of(lineNumbers()),
],
});
const view = new EditorView({ state, parent: document.getElementById("editor") });
function toggleLineNumbers() {
lineNumbersEnabled = !lineNumbersEnabled;
view.dispatch({
effects: lineNumberCompartment.reconfigure(
lineNumbersEnabled ? lineNumbers() : [],
),
});
}Try toggling line numbers on and off using the button below:
Example: Switching Between Themes
import { Compartment } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
const themeCompartment = new Compartment();
const lightTheme = EditorView.theme({
"&": { backgroundColor: "#ffffff", color: "#333333" },
".cm-gutters": { backgroundColor: "#f0f0f0" },
});
const darkTheme = EditorView.theme({
"&": { backgroundColor: "#1e1e1e", color: "#d4d4d4" },
".cm-gutters": { backgroundColor: "#252526" },
}, { dark: true });
const state = EditorState.create({
doc: "Switch between light and dark.",
extensions: [
themeCompartment.of(lightTheme),
],
});
const view = new EditorView({ state, parent: document.getElementById("editor") });
function setTheme(dark: boolean) {
view.dispatch({
effects: themeCompartment.reconfigure(dark ? darkTheme : lightTheme),
});
}Click the "Toggle Theme" button to switch between a light theme and the oneDark theme at runtime using a compartment:
Example: Toggling Vim Mode
The @replit/codemirror-vim package provides vim keybindings. Wrapping it in a compartment lets users enable or disable vim mode at runtime.
import { Compartment } from "@codemirror/state";
import { vim } from "@replit/codemirror-vim";
const vimCompartment = new Compartment();
let vimEnabled = false;
// Initial setup: vim off
const extensions = [
vimCompartment.of([]),
];
function toggleVim() {
vimEnabled = !vimEnabled;
view.dispatch({
effects: vimCompartment.reconfigure(
vimEnabled ? vim() : [],
),
});
}Warning
When toggling vim mode, the vim extension should be placed early in the extensions array so it can intercept key events before other keymaps.
Example: Toggling Line Wrapping
import { Compartment } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
const wrapCompartment = new Compartment();
// Initial setup with wrapping enabled
const extensions = [
wrapCompartment.of(EditorView.lineWrapping),
];
// Toggle wrapping off
view.dispatch({
effects: wrapCompartment.reconfigure([]),
});
// Toggle wrapping on
view.dispatch({
effects: wrapCompartment.reconfigure(EditorView.lineWrapping),
});Multiple Compartments
You can use as many compartments as you need. Each one manages its own extension independently.
import { Compartment } from "@codemirror/state";
import { EditorView, lineNumbers } from "@codemirror/view";
const lineNumberCompartment = new Compartment();
const wrapCompartment = new Compartment();
const themeCompartment = new Compartment();
const state = EditorState.create({
doc: "",
extensions: [
lineNumberCompartment.of(lineNumbers()),
wrapCompartment.of(EditorView.lineWrapping),
themeCompartment.of(myTheme),
],
});You can reconfigure multiple compartments in a single dispatch by passing an array of effects:
view.dispatch({
effects: [
themeCompartment.reconfigure(darkTheme),
wrapCompartment.reconfigure([]),
],
});Checking Current Compartment Value
You can check what extension a compartment currently holds using compartment.get():
const currentExtension = lineNumberCompartment.get(view.state);This returns the current extension value, which can be useful for determining the current state before toggling.
When to Use Compartments
Compartments are the right tool when you need to:
Toggle features on and off at runtime (line numbers, wrapping, vim mode)
Switch between themes or highlight styles
Change language support when the user switches files
Update configuration based on user preferences
For state that changes with every transaction (like a word count), use a StateField instead. Compartments are for swapping entire extensions, not for fine-grained state updates.