Example: Zebra Stripes

This example defines an extension that styles every Nth line with a background.

To style the stripes in a way that allows themes to override them, we start by defining a base theme. It styles the zebraStripe theme selector (which will show up as the cm-zebraStripe CSS class in the DOM), with different backgrounds for light and dark base themes.

import {EditorView} from "@codemirror/next/view"

const baseTheme = EditorView.baseTheme({
  "zebraStripe@light": {backgroundColor: "#f4fafa"},
  "zebraStripe@dark": {backgroundColor: "#1a2727"}
})

Next, as an excuse for including configuration functionality, we'll allow the caller to configure the distance between the stripes. To store the configured distance in a way that is well-defined even if multiple instances of the extension are added, we'll store it in a facet.

The facet takes any number of step values (input type number, the first type parameter), and takes their minimum, or 2 if no values were provided, as its value (output type, the second type parameter, also number).

import {Facet} from "@codemirror/next/state"

const stepSize = Facet.define<number, number>({
  combine: values => values.length ? Math.min(...values) : 2
})

We'll export a single function, which returns the extension that installs the zebra stripe functionality.

Note that extension values can be individual extensions (such as facet values, created with the of method), or arrays, possibly nested, of extensions. Thus they can be easily composed into bigger extensions.

In this case, the function returns our base theme, a value for the stepSize facet, if a configuration was provided, and showStripes, the view plugin that actually adds the styling, which we'll define in a moment.

import {Extension} from "@codemirror/next/state"

export function zebraStripes(options: {step?: number} = {}): Extension {
  return [
    baseTheme,
    options.step == null ? [] : stepSize.of(options.step),
    showStripes
  ]
}

First, this helper function, given a view, iterates over the visible lines, creating a line decoration for every Nth line.

The plugin will simply recompute its decorations every time something changes. Using a builder, this is not very expensive. In other cases, it can be preferable to preserve decorations (mapping them through document changes) across updates.

themeClass is a helper function that, given a theme selector name, return a (set of) CSS class names to assign to the styled elements.

Note that, because facets are always available on every state, whether they have been added to that state or not, we can simply read the value of stepSize to get the appropriate step size. When no one configured it, it'll have the value 2 (the result of calling its combine function with the empty array).

import {Decoration, themeClass} from "@codemirror/next/view"
import {RangeSetBuilder} from "@codemirror/next/rangeset"

const stripe = Decoration.line({
  attributes: {class: themeClass("zebraStripe")}
})

function stripeDeco(view: EditorView) {
  let step = view.state.facet(stepSize)
  let builder = new RangeSetBuilder<Decoration>()
  for (let {from, to} of view.visibleRanges) {
    for (let pos = from; pos <= to;) {
      let line = view.state.doc.lineAt(pos)
      if ((line.number % step) == 0)
        builder.add(line.from, line.from, stripe)
      pos = line.to + 1
    }
  }
  return builder.finish()
}

The showStripes view plugin, then, only has to advertise that it provides decorations (the .decorations() call), and make sure its decorations property is recomputed when the document or the viewport changes.

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

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

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

  update(update: ViewUpdate) {
    if (update.docChanged || update.viewportChanged)
      this.decorations = stripeDeco(update.view)
  }
}).decorations()

The result looks like this: