| name | codemirror |
| description | [Applies to: **/*.{js,jsx}] This guide defines best practices for building and maintaining CodeMirror 6-based editors, focusing on modularity, immutable state, extension-driven configuration, and modern JavaScript/TypeScript patterns. |
| source | cursor_mdc |
CodeMirror 6 Best Practices
This document outlines the definitive best practices for developing with CodeMirror. We exclusively target CodeMirror 6 (CM6). Any reference to CodeMirror 5 (CM5) is for historical context only; CM5 must not be used in new development and should be actively migrated away from.
Code Organization and Structure
1. Always Use CodeMirror 6
CM6 is a complete rewrite with a modern, modular architecture. It offers superior performance, extensibility, and maintainability. CM5 is deprecated and unsupported.
❌ BAD: Using CodeMirror 5
var myCodeMirror = CodeMirror(document.body, {
value: "function myScript(){}",
mode: "javascript"
});
✅ GOOD: Using CodeMirror 6
import { EditorState } from "@codemirror/state";
import { EditorView, basicSetup } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";
const view = new EditorView({
state: EditorState.create({
doc: "console.log('Hello CM6!');",
extensions: [basicSetup, javascript()],
}),
parent: document.body,
});
2. Import Only What You Need (Modular Imports)
CM6 is designed for tree-shaking. Only import the specific packages and modules required to minimize bundle size and improve load times.
❌ BAD: Importing monolithic packages (if they existed for CM6)
import { EditorView, EditorState, basicSetup, javascript, ... } from "codemirror-all";
✅ GOOD: Granular, specific imports
import { EditorState } from "@codemirror/state";
import { EditorView, keymap } from "@codemirror/view";
import { defaultKeymap } from "@codemirror/commands";
import { javascript } from "@codemirror/lang-javascript";
3. Configure Exclusively via Extensions
All editor features, from basic setup to complex language services, must be added as extensions to the EditorState. Avoid direct DOM manipulation or non-extension-based configuration.
❌ BAD: Direct property assignment or DOM manipulation
const view = new EditorView({ state: EditorState.create({ doc: "" }) });
view.dom.style.fontSize = "16px";
✅ GOOD: Everything is an extension
import { EditorState } from "@codemirror/state";
import { EditorView, lineNumbers } from "@codemirror/view";
import { oneDark } from "@codemirror/theme-one-dark";
const view = new EditorView({
state: EditorState.create({
doc: "const x = 1;",
extensions: [
lineNumbers(),
oneDark,
EditorView.lineWrapping,
EditorState.tabSize.of(2),
],
}),
parent: document.body,
});
Common Patterns and Anti-patterns
4. Embrace Immutable State Management
EditorState objects are immutable. All changes must be applied by creating a Transaction and dispatching it to the EditorView. This enables reliable undo/redo, time-travel debugging, and predictable behavior.
❌ BAD: Mutating EditorState or Text directly
let state = EditorState.create({ doc: "hello" });
✅ GOOD: Using Transaction and dispatch
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
const startState = EditorState.create({ doc: "Hello World" });
const view = new EditorView({ state: startState, parent: document.body });
const transaction = view.state.update({
changes: { from: 0, to: 5, insert: "Goodbye" },
selection: { anchor: 7 },
});
view.dispatch(transaction);
console.log(view.state.doc.toString());
5. Prioritize TypeScript for New Code
For new CodeMirror components, always use TypeScript. It provides static type checking, improved IDE support, and reduces runtime errors, leading to more robust and maintainable code.
❌ BAD: Writing new complex modules in plain JavaScript
export function myComplexPlugin() {
return ViewPlugin.define(view => ({
update(update) {
if (update.docChanged) {
}
}
}));
}
✅ GOOD: Writing new complex modules in TypeScript
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
import { Extension } from "@codemirror/state";
export function myComplexPlugin(): Extension {
return ViewPlugin.define(view => ({
update(update: ViewUpdate) {
if (update.docChanged) {
console.log("Document changed:", update.changes.toString());
}
},
}));
}
Performance Considerations
6. Batch Editor Updates
Avoid dispatching multiple transactions in quick succession. Group related changes into a single Transaction to minimize DOM updates and improve performance.
❌ BAD: Frequent, unbatched dispatches
for (let i = 0; i < 100; i++) {
view.dispatch(view.state.update({ changes: { from: i, insert: "a" } }));
}
✅ GOOD: Batching changes into a single transaction
let changes = [];
for (let i = 0; i < 100; i++) {
changes.push({ from: i, insert: "a" });
}
view.dispatch(view.state.update({ changes }));
7. Debounce/Throttle External Event Handlers
When external events (e.g., window resize, network responses, user input outside the editor) trigger editor updates or expensive computations, always debounce or throttle them to prevent excessive processing.
import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";
import { javascript } from "@codemirror/lang-javascript";
const view = new EditorView({
state: EditorState.create({
doc: "function expensiveLint() { /* ... */ }",
extensions: [javascript()],
}),
parent: document.body,
});
const debouncedLint = debounce(() => {
console.log("Linting document...");
}, 500);
view.dom.addEventListener("input", debouncedLint);
function debounce(func, delay) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout( func.(context, args), delay);
};
}
Common Pitfalls and Gotchas
8. Never Mix CodeMirror 5 and 6
The architectures are fundamentally different. Attempting to use CM5 add-ons or APIs with a CM6 editor will lead to unpredictable behavior and errors.
❌ BAD: Using CM5 add-ons with CM6
import "codemirror/addon/search/searchcursor";
✅ GOOD: Use CM6-native extensions
import { search, searchKeymap, highlightSelectionMatches } from "@codemirror/search";
import { EditorState } from "@codemirror/state";
import { EditorView, keymap } from "@codemirror/view";
const view = new EditorView({
state: EditorState.create({
doc: "search me",
extensions: [
search(),
keymap.of(searchKeymap),
highlightSelectionMatches(),
],
}),
parent: document.body,
});
9. Understand Extension Precedence
When multiple extensions affect the same aspect of the editor (e.g., keymaps, decorations), their order in the extensions array and explicit Prec modifiers determine precedence. Debug carefully if an extension isn't behaving as expected.
import { EditorState, Prec } from "@codemirror/state";
import { EditorView, keymap } from "@codemirror/view";
import { defaultKeymap } from "@codemirror/commands";
const myCustomKeymap = keymap.of([
{
key: "Mod-s",
run: (view) => {
console.log("Custom save action!");
return true;
},
},
]);
const view = new EditorView({
state: EditorState.create({
doc: "Press Cmd-S",
extensions: [
defaultKeymap,
Prec.highest(myCustomKeymap),
],
}),
parent: document.body,
});
Testing Approaches
10. Unit Test EditorState Logic
Isolate and unit test functions that operate on or produce EditorState objects. Since EditorState is immutable, these functions are pure and easy to test without a browser environment.
import { EditorState } from "@codemirror/state";
export function addCommentToLine(state, lineNumber, commentText) {
const line = state.doc.line(lineNumber);
return state.update({
changes: { from: line.from, insert: `// ${commentText}\n` + line.text },
});
}
import { EditorState } from "@codemirror/state";
import { addCommentToLine } from "./my-state-transformer";
test("addCommentToLine adds comment correctly", () => {
const initialState = EditorState.create({ doc: "line 1\nline 2" });
const newState = addCommentToLine(initialState, 2, "My comment");
expect(newState.doc.toString()).toBe("line 1\n// My comment\nline 2");
});
11. Use End-to-End Tests for EditorView Interactions
For testing UI interactions (typing, selection, scrolling, extension rendering), use browser-based end-to-end testing frameworks like Playwright or Cypress. These simulate real user behavior.
import { test, expect } from "@playwright/test";
test("editor displays line numbers and allows typing", async ({ page }) => {
await page.goto("http://localhost:3000/editor");
const editor = page.locator(".cm-editor");
await expect(editor).toBeVisible();
await expect(page.locator(".cm-lineNumbers")).toBeVisible();
await editor.type("Hello, CodeMirror!");
await expect(editor).toHaveText(/Hello, CodeMirror!/);
});