Preferred way to create multi-line widget?

I am currently tinkering with Codemirror 6 and want to be able to create custom DOM elements that work basically like Decoration.replace(), but with multiple lines (specifically I want to be able to pre-render math equations or Mermaid diagrams), but when simply implementing this according to the documentation, the editor outputs a console error reading “Decorations that replace line breaks may not be specified via plugins”.

The question now is: How else can one specify a decoration that is not a plugin…? For my purposes the WidgetType extension is very useful as I can traverse the tree and look for corresponding NodeTypes and simply replace that range completely without having to reparse any of the actual code, and take the node’s contents to render the equation.

Any advise would be very welcome; thanks in advance!

Directly via EditorView.decorations. Put the decorations in a state field, not a plugin, so that they are available before the viewport is computed.

1 Like

Thank you very much! I figured that out shortly after writing this question, but it’s good to hear the confirmation from you that the way I found is also the preferred way.

For anyone who’s curious and wants to know how to do this properly:

After starting this topic, I cloned the codemirror-view repo and searched for the error message, finding the following lines of code:

if (this.disallowBlockEffectsFor[index] && deco instanceof PointDecoration) {
      if (deco.block)
        throw new RangeError("Block decorations may not be specified via plugins")
      if (to > this.doc.lineAt(this.pos).to)
        throw new RangeError("Decorations that replace line breaks may not be specified via plugins")

This indicated to me that apparently there was a flag set to disallow any type of block widgets to be drawn. This also means that apparently one doesn’t have to explicitly set the block-flag on the widgets, since Codemirror is smart enough to figure out whether the widget spans multiple lines. (Please correct me if I’m wrong)

Then, I returned to the documentation where I found this specific sentence:

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.

This was very unclear to me, but in effect it means the following: To define something “directly” means to define them as a StateField, whereas defining them “through a function” equals defining them as a ViewPlugin. The error in my thinking was that both ways of defining decorations actually have a function that gets called and returns the decorations. In the case of a ViewPlugin it’s decorations: view => view.decorations, whereas in the case of a StateField it’s provide: f => EditorView.decorations.from(f).

In short, depending on your use case the methods are as following:

Inline-only widgets

If you want to render widgets that are guaranteed to be only inline (i.e. Markdown links), then you should define a ViewPlugin. Those get called after the viewport has been created and are therefore much cheaper. Also, you can make use of the view.visibleRanges-property which enables you to only render those widgets for any actually visible ranges (i.e. must be within the viewport and additionally not be folded).

The example for inline-only-widgets is the Boolean Toggle Widgets example.

Inline and block widgets

If you, on the other hand, have widgets that may also be block-level and not just inline you have to, as Marijn said, call StateField.define(). You can – more or less – provide the same logic to compute the widgets, but since StateFields are computed prior to rendering, you can also provide block-level widgets.

The example for the inline and block widgets is the Underline command example. (The trick is to understand that you can also underline pieces of text that span line breaks, even if you don’t underline full lines. To me, for the past weeks, it always seemed to me inline-only just as well.)