Dynamic line height

I have a decorations Facet that sets the line height based on a StateField using attributes:

    EditorView.decorations.compute([lineNumberToHeight], (state) => {
      const lineHeights = state.field(lineNumberToHeight);
      return Decoration.set(
          .map((lineHeight, lineNumber) =>
              attributes: {
                style: `height: ${
                  /* views.results.defaultLineHeight */ 28 * lineHeight
          .filter((lineHeight) => lineHeight != null)

This works fine while the editor contents are in view. But once I scroll the content out of view, the “artificially increased” line height is not taken into account, and so the content jumps up.

How can I solve it?

I can’t use a block widget, because I need the line itself to have the given height. Can I hook into codemirror’s line height calculation to let the editor know that some lines are different height?

The height of out-of-view content is modeled by the view’s internal “height map” structure, which understands widgets with estimated heights but not explicitly set heights on lines. What exactly are you implementing here? Would it be a good match for block widgets?

Would it be a good match for block widgets?

I don’t think so (I can’t use a block widget, because I need the line itself to have the given height.)

What exactly are you implementing here?

I’m implementing two editors side by side, where the editor on the right matches the editor on the left. The editor on the left has line wrapping. The editor on the right can have “spilling” content, so needs the right line height to both show its lines at the right scroll position and to limit their content to match the editor on the left.

Current line is highlighted in both editors (and its height needs to match).

I also realized the issue seems to be even slightly more complicated. It looks like codemirror remembers the line height I set via attributes, so scrolling itself doesn’t break the layout. What breaks the layout is resetting the content of the right editor. I guess codemirror throws away the line measurements (which is reasonable as I’m wiping out the whole doc with the first change). So if I’m scrolled down, then rewrite the right editor content, the lines jump.

But I need to be able to rewrite the content. Is there a way to let Codemirror know that a line will have certain height without it being rendered?

So you’re clipping wrapped lines with overflow: hidden? Adding extra space to sync editor heights can be done with widgets (see for example the CodeMirror 5 merge addon), but yeah, that can’t clip content. Still, this seems an awfully specific case to add a library feature for.

Agree this is quite specific. I thought of two other workarounds:

  1. Disable the viewport optimization (on the right side). Would this be easy enough to add?
  2. (and this is probably the best albeit most complex) I need to make sure I only update lines on the right that there are in active viewport.

Although I’m not entirely sure this second approach is bullet proof when multiple cursors are used or search and replace is invoked and the left editor itself actually changes its line heights above the active viewport.

Spending some more time on this problem, I think the only way to get a bulletproof solution with the current “smart” layout I have (where the two editors can take different width based on their content), is to disable the viewport optimization. I read the Codemirror code, and I can’t really see how that can be done. @marijn any ideas on how I could disable the viewport and force Codemirror to render all content? (my documents won’t really be long enough for this to matter, but Codemirror is sufficiently aggressive in applying this that I do run into this line height problem)

That’s not something that CodeMirror 6 currently supports, and a lot of complexity analysis in the system assumes a limited-size viewport, meaning that you’re quite likely to hit performance cliffs when working with even medium-sized (say, a few thousand lines) documents if you were to somehow disable this. So I’m not too keen on adding this option, knowing that people will turn it off and then ask for support when it messes up their user experience.

1 Like