⚡ CM6 View Plugins & Decorations: Ultra-Cheat-Sheet
🧩 1. View Plugins Lifecycle
Extend editor behavior, track state, and map decorations.
const myPlugin = ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.computeDecos(view);
}
update(update: ViewUpdate) {
// ⚠️ Keep performance high: check what changed before recalculating
if (update.docChanged || update.viewportChanged) {
this.decorations = this.computeDecos(update.view);
}
}
destroy() {
/* Cleanup listeners/timers */
}
computeDecos(view: EditorView): DecorationSet {
// Return RangeSet of decorations
}
},
{
decorations: v => v.decorations,
eventHandlers: {
mousedown(e, view) { /* Handle event */ }
}
}
);🎨 2. Decoration Types
import { Decoration } from "@codemirror/view";| Type | Target | Usage | Key Options |
|---|---|---|---|
| Mark | Inline Range | Decoration.mark({ class: "my-class" }) | class, attributes, inclusive |
| Widget | Point | Decoration.widget({ widget: new MyWidget(), side: 1 }) | widget: WidgetType, side, block |
| Replace | Range → Widget | Decoration.replace({ widget: new MyWidget() }) | widget, inclusive |
| Line | Line Block | Decoration.line({ attributes: { class: "bg-red" } }) | class, attributes |
🛠️ Implementing a Custom Widget
class MyWidget extends WidgetType {
toDOM(view: EditorView): HTMLElement {
const span = document.createElement("span");
span.textContent = "⭐";
return span;
}
// ⚡ Performance: return true if widget instance is identical to avoid DOM rebuilds
eq(other: MyWidget) { return true; }
// Optional optimizations
updateDOM(dom: HTMLElement) { return false; }
destroy(dom: HTMLElement) {}
}📦 3. Decoration Sets (RangeSet)
Immutable structures holding sorted ranges of decorations.
- Build (Initial):
const builder = new RangeSetBuilder<Decoration>(); // ⚠️ CRITICAL: Ranges must be added in ascending order of start position builder.add(from, to, decorationInstance); const decoSet = builder.finish(); - Update (Mapping over document changes):
const updatedDecos = oldDecos.map(tr.changes);
🔄 4. View Updates & Render Cycles
State Change Propagation (ViewUpdate)
Inside update(update: ViewUpdate):
update.docChanged(bool): Text content modified.update.selectionSet(bool): Cursor/selection moved.update.viewportChanged(bool): Scrolled or resized.update.transactions(Array): Inspect raw transaction metadata.
📐 Measuring DOM Safely (Avoid Layout Thrashing)
Never read layout heights directly in event handlers or updates. Use requestMeasure to batch reads/writes.
view.requestMeasure({
read(view) {
// 🔍 Read DOM (e.g., getBoundingClientRect) safely here
return { height: view.dom.clientHeight };
},
write(measureVal, view) {
// ✍️ Write DOM modifications safely here based on read values
console.log("Height was:", measureVal.height);
}
});