codemirror — Cursor rule: CodeMirror 6 best practices
A Cursor rule that sets how to build CodeMirror 6 editors: modular imports, immutable state, extension-driven config.
.cursor/rules/codemirror.mdcWhat codemirror does
The rule covers CodeMirror 6 and bans CodeMirror 5 in new code. Existing CM5 code should be migrated away. Each requirement comes with a bad and a good example: how to import packages, how to change the document, how to add editor features.
Configuration goes through EditorState extensions only. Direct DOM edits such as setting view.dom.style are called out as wrong. State is immutable, so changes go through a Transaction dispatched with view.dispatch rather than mutating EditorState. For new modules the rule recommends TypeScript so parameters like ViewUpdate carry types.
Performance gets its own section: batch several edits into one Transaction, and wrap external handlers such as window resize or network responses in debounce or throttle. The file matches **/*.{js,jsx}, so it also fires in plain JS files that have nothing to do with CodeMirror. Worth narrowing the globs for your project.
What it helps with
- migrate an editor from CodeMirror 5 to 6
- configure the editor without touching the DOM
- apply a document change through a Transaction
- shrink the bundle with granular imports
- group several edits into one dispatch
Example prompts for the agent
- Add a CodeMirror 6 code editor to this component
- Rewrite this file from CM5 to CodeMirror 6
- Why does my dispatch not change the document in CodeMirror?
How to install codemirror
Manually: put the files in place
.cursor/rules/codemirror.mdcFrontmatter: description, globs, alwaysApply
Manually: put the files in place
.claude/rules/codemirror.mdThe paths frontmatter field limits the files: move globs from the source into paths
AGENTS.md
Codex reads AGENTS.md: paste the rule text there
Manually: put the files in place
.github/instructions/codemirror.instructions.mdThe applyTo field sets which files it applies to: move globs from the source into applyTo
Manually: put the files in place
.devin/rules/codemirror.mdManually: put the files in place
.clinerules/codemirror.mdStep by step, with commands for each platform: Cursor rules: .mdc files, four modes and what replaced .cursorrules
Questions about codemirror
How do I install the codemirror rule in Cursor?
Create .cursor/rules/codemirror.mdc in your project and paste the rule into it. Cursor picks it up automatically.
What does the codemirror rule do?
It sets standards for CodeMirror 6 editors: modular imports, extension-based configuration, immutable state and batched updates.
Can I use CodeMirror 5 with this rule?
No. The rule requires CM6 for new code and tells you to migrate existing CM5 code.
Source text
codemirror.mdc · CC0-1.0
---
description: 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.
globs: **/*.{js,jsx}
---
# 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**
```javascript
// Old CM5 global API
var myCodeMirror = CodeMirror(document.body, {
value: "function myScript(){}",
mode: "javascript"
});
```
✅ **GOOD: Using CodeMirror 6**
```javascript
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)**
```javascript
// Hypothetical bad example: importing a giant 'codemirror-all' package
import { EditorView, EditorState, basicSetup, javascript, ... } from "codemirror-all";
```
✅ **GOOD: Granular, specific imports**
```javascript
import { EditorState } from "@codemirror/state"; // Core state management
import { EditorView, keymap } from "@codemirror/view"; // Core view rendering
import { defaultKeymap } from "@codemirror/commands"; // Standard editing commands
import { javascript } from "@codemirror/lang-javascript"; // Language support
```
### 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**
```javascript
const view = new EditorView({ state: EditorState.create({ doc: "" }) });
// Don't do this: Bypassing the extension system
view.dom.style.fontSize = "16px";
```
✅ **GOOD: Everything is an extension**
```javascript
import { EditorState } from "@codemirror/state";
import { EditorView, lineNumbers } from "@codemirror/view";
import { oneDark } from "@codemirror/theme-one-dark"; // A theme extension
const view = new EditorView({
state: EditorState.create({
doc: "const x = 1;",
extensions: [
lineNumbers(), // Line numbers extension
oneDark, // Theme extension
EditorView.lineWrapping, // Line wrapping extension
EditorState.tabSize.of(2), // Configuration option as an extension
],
}),
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**
```javascript
let state = EditorState.create({ doc: "hello" });
// This will NOT work as expected and breaks CM6's core design
// state.doc = Text.of("world");
// console.log(state.doc.toString()); // Still "hello"
```
✅ **GOOD: Using `Transaction` and `dispatch`**
```javascript
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 });
// Create a transaction to change the document
const transaction = view.state.update({
changes: { from: 0, to: 5, insert: "Goodbye" }, // Change "Hello" to "Goodbye"
selection: { anchor: 7 }, // Move cursor
// Add other state changes like effects or annotations here
});
view.dispatch(transaction); // Apply the transaction
console.log(view.state.doc.toString()); // "Goodbye World"
```
### 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**
```javascript
// my-complex-cm-plugin.js
export function myComplexPlugin() {
// No type safety, easy to introduce bugs
return ViewPlugin.define(view => ({
update(update) {
if (update.docChanged) {
// What type is update.docChanged?
}
}
}));
}
```
✅ **GOOD: Writing new complex modules in TypeScript**
```typescript
// my-complex-cm-plugin.ts
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
import { Extension } from "@codemirror/state";
export function myComplexPlugin(): Extension {
return ViewPlugin.define(view => ({
// Explicitly typed parameters for clarity and safety
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**
```javascript
// In a loop or event handler
for (let i = 0; i < 100; i++) {
view.dispatch(view.state.update({ changes: { from: i, insert: "a" } }));
}
```
✅ **GOOD: Batching changes into a single transaction**
```javascript
let changes = [];
for (let i = 0; i < 100; i++) {
changes.push({ from: i, insert: "a" });
}
view.dispatch(view.state.update({ changes })); // Single dispatch
```
### 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.
```javascript
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,
});
// Assume `lintDocument` is an expensive operation
const debouncedLint = debounce(() => {
// Trigger linting based on current editor state
console.log("Linting document...");
}, 500);
// Listen for editor state changes
view.dom.addEventListener("input", debouncedLint);
function debounce(func, delay) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(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**
```javascript
// This will NOT work with CM6
import "codemirror/addon/search/searchcursor";
```
✅ **GOOD: Use CM6-native extensions**
```javascript
// For search, use CM6's @codemirror/search
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.
```javascript
import { EditorState, Prec } from "@codemirror/state";
import { EditorView, keymap } from "@codemirror/view";
import { defaultKeymap } from "@codemirror/commands";
// A custom keymap that should override default behavior
const myCustomKeymap = keymap.of([
{
key: "Mod-s",
run: (view) => {
console.log("Custom save action!");
return true; // Indicate that the event was handled
},
},
]);
const view = new EditorView({
state: EditorState.create({
doc: "Press Cmd-S",
extensions: [
defaultKeymap, // Default keymap is defined first
Prec.highest(myCustomKeymap), // Custom keymap with highest precedence will override
],
}),
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.
```javascript
// my-state-transformer.js
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 },
});
}
// my-state-transformer.test.js
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.
```javascript
// playwright.test.js (conceptual example)
import { test, expect } from "@playwright/test";
test("editor displays line numbers and allows typing", async ({ page }) => {
await page.goto("http://localhost:3000/editor"); // Your app's editor page
const editor = page.locator(".cm-editor");
await expect(editor).toBeVisible();
// Check if line numbers are visible
await expect(page.locator(".cm-lineNumbers")).toBeVisible();
// Type into the editor
await editor.type("Hello, CodeMirror!");
await expect(editor).toHaveText(/Hello, CodeMirror!/);
});
```
© sanjeed5 · CC0-1.0 · sanjeed5/awesome-cursor-rules-mdc