Example: Gutters

The view module provides functionality for adding gutters (vertical bars in front of the code) to your editor. The simplest use of gutters is to simply dump lineNumbers() into your configuration to get a line number gutter. But the module also helps when you want to define your own gutters and show custom controls in them.

Adding a Gutter

Conceptually, the editor displays a collection of gutters next to each other, each of which has its own style and content (though you'll often want to keep their default style so that they blend in with the others, looking like a single big gutter). For each line, each gutter may display something. The line number gutter will show a line number—obviously.

To add a gutter, call the gutter function and include the result in your state configuration. The position of this extension relative to other gutter extensions determines the ordering of the gutters. So this, for example, will put our gutter after the line numbers:

extensions: [lineNumbers(), gutter({class: "cm-mygutter"})]

Unless the cm-mygutter CSS class sets some minimum width, you won't see such a gutter though—it'll just be an empty element (in a CSS flexbox), which the browser will collapse.

To put content into the gutter, we can use either the lineMarker option, which will be called for each visible line to determine what to show there, or the markers option, which allows you to build a persistent set of markers (using the same range set data structure used in decorations) to show in your gutter.

As with decorations, gutter markers are represented by lightweight immutable values that know how to render themselves to DOM nodes, in order to allow updates to be represented in a declarative way without recreating a lot of DOM nodes on every transaction. Gutter markers can also add a CSS class to a gutter element.

This code defines two gutters, one that shows an ø sign on every empty line, and one that allows you to toggle a 'breakpoint' marker per line by clicking on that gutter. The first is easy:

import {EditorView, gutter, GutterMarker} from "@codemirror/view"

const emptyMarker = new class extends GutterMarker {
  toDOM() { return document.createTextNode("ø") }
}

const emptyLineGutter = gutter({
  lineMarker(view, line) {
    return line.from == line.to ? emptyMarker : null
  },
  initialSpacer: () => emptyMarker
})

(The new class construct creates an anonymous class and then initializes a single instance of it. Since there's only one type of empty-line marker, we use this to get our GutterMarker instance.)

To avoid the problem with empty gutters not showing up at all, gutters allow you to configure a 'spacer' element that is rendered invisibly in the gutter to set its minimal width. This is often easier than setting an explicit with with CSS and making sure it covers the expected content.

The lineMarker option checks if the line is zero-length, and if so, returns our marker.

The breakpoint gutter is a bit more involved. It needs to track state (the position of the breakpoints), for which we use a state field, with a state effect that can update it.

import {StateField, StateEffect, RangeSet} from "@codemirror/state"

const breakpointEffect = StateEffect.define<{pos: number, on: boolean}>({
  map: (val, mapping) => ({pos: mapping.mapPos(val.pos), on: val.on})
})

const breakpointState = StateField.define<RangeSet<GutterMarker>>({
  create() { return RangeSet.empty },
  update(set, transaction) {
    set = set.map(transaction.changes)
    for (let e of transaction.effects) {
      if (e.is(breakpointEffect)) {
        if (e.value.on)
          set = set.update({add: [breakpointMarker.range(e.value.pos)]})
        else
          set = set.update({filter: from => from != e.value.pos})
      }
    }
    return set
  }
})

function toggleBreakpoint(view: EditorView, pos: number) {
  let breakpoints = view.state.field(breakpointState)
  let hasBreakpoint = false
  breakpoints.between(pos, pos, () => {hasBreakpoint = true})
  view.dispatch({
    effects: breakpointEffect.of({pos, on: !hasBreakpoint})
  })
}

The state starts empty, and when a transaction happens, it maps the positions of the breakpoints through the changes (if any), and looks for effects that add or remove breakpoints, adjusting the set of breakpoints as appropriate.

The breakpointGutter extension combines this state field with a gutter and a bit of styling for that gutter.

const breakpointMarker = new class extends GutterMarker {
  toDOM() { return document.createTextNode("💔") }
}

const breakpointGutter = [
  breakpointState,
  gutter({
    class: "cm-breakpoint-gutter",
    markers: v => v.state.field(breakpointState),
    initialSpacer: () => breakpointMarker,
    domEventHandlers: {
      mousedown(view, line) {
        toggleBreakpoint(view, line.from)
        return true
      }
    }
  }),
  EditorView.baseTheme({
    ".cm-breakpoint-gutter .cm-gutterElement": {
      color: "red",
      paddingLeft: "5px",
      cursor: "default"
    }
  })
]

The domEventHandlers option allows you to specify event handlers for this gutter, which we use to set up a mousedown handler to toggle the breakpoint for the line that was clicked.

This is what an editor with the breakpoint gutter before the line numbers and the empty line gutter after it looks like:

Customizing the Line Number Gutter

The lineNumbers function also takes configuration parameters, allowing you to add event handlers or customize the way line numbers are displayed.

const hexLineNumbers = lineNumbers({
  formatNumber: n => n.toString(16)
})

It is also possible to add markers to the line number gutter, which replace the line numbers for affected lines. This is done through the lineNumberMarkers facet, which works a lot like markers on custom gutters, but can be provided by any extension, rather than being configured directly for a single gutter.