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
cm-zebraStripe
class, with different backgrounds for light and dark
base themes.
import {EditorView} from "@codemirror/view"
const baseTheme = EditorView.baseTheme({
"&light .cm-zebraStripe": {backgroundColor: "#d4fafa"},
"&dark .cm-zebraStripe": {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/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/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.
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} from "@codemirror/view"
import {RangeSetBuilder} from "@codemirror/state"
const stripe = Decoration.line({
attributes: {class: "cm-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
option),
and make sure its decorations
property is recomputed when the
document or the viewport changes.
import {ViewPlugin, DecorationSet, ViewUpdate} from "@codemirror/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: v => v.decorations
})
The result looks like this: