⚡ 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";
TypeTargetUsageKey Options
MarkInline RangeDecoration.mark({ class: "my-class" })class, attributes, inclusive
WidgetPointDecoration.widget({ widget: new MyWidget(), side: 1 })widget: WidgetType, side, block
ReplaceRange → WidgetDecoration.replace({ widget: new MyWidget() })widget, inclusive
LineLine BlockDecoration.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);
  }
});