C.W.K.
Stream
Lesson 01 of 05 · published

The Decoration Mechanism

~12 min · decoration, mark, widget, cm6

Level 0Cold Draft
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"A decoration paints over the source without changing it. That one property is why one mechanism can do everything."

Two Kinds of Decoration

CodeMirror 6 gives you exactly two ways to add visual layers over the source. A mark decoration attaches a style to a range of text — make characters 4–8 bold, underline characters 12–16 in red. A widget decoration inserts a piece of UI at a position — a margin note after a word, a whole proposal block below a line. Marks style what's there; widgets insert what isn't. Between them, they cover every "show something extra" a feature could want.

It Paints Over Source, Never Mutates It

The property that makes decorations powerful is what they don't do: they never change the document. A bold mark doesn't insert formatting into the text; it renders characters that are already there as bold. A widget doesn't write into the file; it paints UI at a point in the view. The source string is untouched. Decorations live in a StateField that recomputes a DecorationSet from the document on every edit:

const decoField = StateField.define<DecorationSet>({
  create: () => Decoration.none,
  update: (deco, tr) => buildDecorations(tr.state), // ranges -> marks + widgets
  provide: (f) => EditorView.decorations.from(f),
});

One Primitive, Any "Show Something Extra" Feature

Here's the whole trick. Because a decoration adds a layer without touching the model, any feature that means "show the user something extra over their text" reduces to the same primitive: compute a set of ranges, attach marks or widgets, let CM6 render them. Live Preview is ranges-to-marks. Voice underline is ranges-to-marks-plus-a-widget. CMD+K is a widget holding a proposal. Three features that look unrelated are, underneath, the same computation with different inputs. That's the seed of the entire track.

Code

Marks style a range; widgets insert UI — both over source·typescript
import { Decoration, DecorationSet, EditorView, WidgetType } from "@codemirror/view";
import { StateField } from "@codemirror/state";

// MARK: style a range of the existing source (render **bold** as bold).
const boldMark = Decoration.mark({ class: "cm-strong" });

// WIDGET: insert UI at a position without changing the text.
class MarginNote extends WidgetType {
  constructor(readonly text: string) { super(); }
  toDOM() {
    const s = document.createElement("span");
    s.className = "cm-margin-note";
    s.textContent = this.text;
    return s;
  }
}

// A StateField holds the current DecorationSet and recomputes it per edit.
const decoField = StateField.define<DecorationSet>({
  create: () => Decoration.none,
  update: (deco, tr) => buildDecorations(tr.state), // ranges -> marks + widgets
  provide: (f) => EditorView.decorations.from(f),
});
// The document is never mutated. Only the painted layer changes.

External links

Exercise

For each of Rekindle's three soul features, decide whether it primarily needs a mark or a widget: (1) rendering **bold** as bold, (2) underlining a voice issue and floating a note beside it, (3) showing an AI rewrite proposal below the selected line before you accept. Then state, in one sentence, why none of the three needs to mutate the document.
Hint
Rendering and underlining style existing characters → marks (the note beside the underline is a small widget). A proposal block is new UI inserted at a position → widget. None mutate the document because a decoration is a view layer painted over source, not an edit to it.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.