Example: Decorations

The DOM structure inside a CodeMirror editor is managed by the editor itself. Inside the cm-content element, any attempt to add attributes or change the structure of nodes will usually just lead to the editor immediately resetting the content back to what it used to be.

So to style content, replace content, or add additional elements in between the content, we have to tell the editor to do so. That is what decorations are for.

Types of Decorations

There are four different types of decorations that you can add to your content.

Calling these functions gives you a Decoration object, which just describes the type of decoration and which you can often reuse between instances of decorations. The range method on these objects gives you an actual decorated range, which holds both the type and a pair of from/to document offsets.

Decoration Sources

Decorations are provided to the editor using the RangeSet data structure, which stores a collection of values (in this case the decorations) with ranges (start and end positions) associated with them. This data structure helps with things like efficiently updating the positions in a big set of decorations when the document changes.

Decorations are provided to the editor view through a facet. There are two ways to provide them—directly, or though a function that will be called with a view instance to produce a set of decorations. Decorations that signficantly change the vertical layout of the editor, for example by replacing line breaks or inserting block widgets, must be provided directly, since indirect decorations are only retrieved after the viewport has been computed.

Indirect decorations are appropriate for things like syntax highlighting or search match highlighting, where you might want to just render the decorations inside the viewport or the current visible ranges, which can help a lot with performance.

Let's start with an example that keeps decorations in the state, and provides them directly.

Underlining Command

Say we want to implement an editor extension that allows the user to underline parts of the document. To do this, we could define a state field that tracks which parts of the document are underlined, and provides mark decoration that draw those underlines.

To keep the code simple, the field stores only the decoration range set. It doesn't do things like joining overlapping underlines, but just dumps any newly underlined region into its set of ranges.

import {EditorView, Decoration, DecorationSet} from "@codemirror/view"
import {StateField, StateEffect} from "@codemirror/state"

const addUnderline = StateEffect.define<{from: number, to: number}>()

const underlineField = StateField.define<DecorationSet>({
  create() {
    return Decoration.none
  },
  update(underlines, tr) {
    underlines = underlines.map(tr.changes)
    for (let e of tr.effects) if (e.is(addUnderline)) {
      underlines = underlines.update({
        add: [underlineMark.range(e.value.from, e.value.to)]
      })
    }
    return underlines
  },
  provide: f => EditorView.decorations.from(f)
})

const underlineMark = Decoration.mark({class: "cm-underline"})

Note that the update method starts by mapping its ranges through the transaction's changes. The old set refers to positions in the old document, and the new state must get a set with positions in the new document, so unless you completely recompute your decoration set, you'll generally want to map it though document changes.

Then it checks if the effect we defined for adding underlines is present in the transaction, and if so, extends the decoration set with more ranges.

Next we define a command that, if any text is selected, adds an underline to it. We'll just make it automatically enable the state field (and a base theme) on demand, so that no further configuration is necessary.

const underlineTheme = EditorView.baseTheme({
  ".cm-underline": { textDecoration: "underline 3px red" }
})

export function underlineSelection(view: EditorView) {
  let effects: StateEffect<unknown>[] = view.state.selection.ranges
    .filter(r => !r.empty)
    .map(({from, to}) => addUnderline.of({from, to}))
  if (!effects.length) return false

  if (!view.state.field(underlineField, false))
    effects.push(StateEffect.appendConfig.of([underlineField,
                                              underlineTheme]))
  view.dispatch({effects})
  return true
}

And finally, this keymap binds that command to Ctrl-h (Cmd-h on macOS). The preventDefault field is there because even when the command doesn't apply, we don't want the browser's default behavior to happen.

import {keymap} from "@codemirror/view"

export const underlineKeymap = keymap.of([{
  key: "Mod-h",
  preventDefault: true,
  run: underlineSelection
}])

Boolean Toggle Widgets

Next, we'll look at a plugin that displays a checkbox widget next to boolean literals, and allows the user to click that to flip the literal.

Widget decorations don't directly contain their widget DOM. Apart from helping keep mutable objects out of the editor state, this additional level of indirection also makes it possible to recreate widgets without redrawing the DOM for them. We'll use that later by simply recreating our decoration set whenever the document changes.

Thus, we must first define a subclass of WidgetType that draws the widget.

import {WidgetType} from "@codemirror/view"

class CheckboxWidget extends WidgetType {
  constructor(readonly checked: boolean) { super() }

  eq(other: CheckboxWidget) { return other.checked == this.checked }

  toDOM() {
    let wrap = document.createElement("span")
    wrap.setAttribute("aria-hidden", "true")
    wrap.className = "cm-boolean-toggle"
    let box = wrap.appendChild(document.createElement("input"))
    box.type = "checkbox"
    box.checked = this.checked
    return wrap
  }

  ignoreEvent() { return false }
}

Decorations contain instances of this class (which are cheap to create). When the view updates itself, if it finds it already has a drawn instance of such a widget in the position where the widget occurs (using the eq method to determine equivalence), it will simply reuse that.

It is also possible to optimize updating of DOM structure for widgets of the same type but with different content by defining an updateDOM method. But that doesn't help much here.

The produced DOM wraps the checkbox in a <span> element, mostly because Firefox handles checkboxes with contenteditable=false poorly (running into browser quirks is common around the edges of contenteditable). We'll also tell screen readers to ignore it since the feature doesn't really work without a pointing device anyway.

Finally, the widget's ignoreEvents method tells the editor to not ignore events that happen in the widget. This is necessary to allow an editor-wide event handler (defined later) to handle interaction with it.

Next, this function uses the editor's syntax tree (assuming the JavaScript language is enabled) to locate boolean literals in the visible parts of the editor and create widgets for them.

import {EditorView, Decoration} from "@codemirror/view"
import {syntaxTree} from "@codemirror/language"

function checkboxes(view: EditorView) {
  let widgets = []
  for (let {from, to} of view.visibleRanges) {
    syntaxTree(view.state).iterate({
      from, to,
      enter: (node) => {
        if (node.name == "BooleanLiteral") {
          let isTrue = view.state.doc.sliceString(node.from, node.to) == "true"
          let deco = Decoration.widget({
            widget: new CheckboxWidget(isTrue),
            side: 1
          })
          widgets.push(deco.range(node.to))
        }
      }
    })
  }
  return Decoration.set(widgets)
}

That function is used by a view plugin that keeps an up-to-date decoration set as the document or viewport changes.

import {ViewUpdate, ViewPlugin, DecorationSet} from "@codemirror/view"

const checkboxPlugin = ViewPlugin.fromClass(class {
  decorations: DecorationSet

  constructor(view: EditorView) {
    this.decorations = checkboxes(view)
  }

  update(update: ViewUpdate) {
    if (update.docChanged || update.viewportChanged)
      this.decorations = checkboxes(update.view)
  }
}, {
  decorations: v => v.decorations,

  eventHandlers: {
    mousedown: (e, view) => {
      let target = e.target as HTMLElement
      if (target.nodeName == "INPUT" &&
          target.parentElement!.classList.contains("cm-boolean-toggle"))
        return toggleBoolean(view, view.posAtDOM(target))
    }
  }
})

The options given to the plugin tell the editor that, firstly, it can get decorations from this plugin, and secondly, that as long as the plugin is active, the given mousedown handler should be registered. The handler checks the event target to recognize clicks on checkboxes, and uses the following helper to actually toggle booleans.

function toggleBoolean(view: EditorView, pos: number) {
  let before = view.state.doc.sliceString(Math.max(0, pos - 5), pos)
  let change
  if (before == "false")
    change = {from: pos - 5, to: pos, insert: "true"}
  else if (before.endsWith("true"))
    change = {from: pos - 4, to: pos, insert: "false"}
  else
    return false
  view.dispatch({changes: change})
  return true
}

After adding the plugin as an extension to a (JavaScript) editor, you get something like this:

To see an example of line decorations, check out the zebra stripe example.