Conflicting widget and mark decorations on empty line in CM6

Hey there!

I’m trying to add logic to decorate an empty line but am running into some problems. My initial approach involved using a widget decorator as suggested in this issue: Adding placeholder on active line if active line is empty - #3 by crossvalidator since that’s what view.placeholder does under the hood: view/src/placeholder.ts at main · codemirror/view · GitHub.

For context, my code ended up looking something like this:

function addIndentationMarkers(view: EditorView) {
  const builder = new RangeSetBuilder<Decoration>();

  for (const { from, to } of view.visibleRanges) {
    let pos = from;

    while (pos <= to) {
      const line = view.state.doc.lineAt(pos);
      const { text } = line;

      // Decorate empty line
      if (text.trim().length === 0) {
        const indentationWidget = Decoration.widget({
          widget: new IndentationWidget(),
        });

        builder.add(line.from, line.from, indentationWidget);
      }

      // Move on to next line
      pos = line.to + 1;
    }
  }

  return builder.finish();
}

function createIndentMarkerPlugin() {
  return ViewPlugin.define(
    (view) => ({
      decorations: addIndentationMarkers(view),
      update(update) {
        if (update.docChanged || update.viewportChanged) {
          this.decorations = addIndentationMarkers(update.view);
        }
      },
    }),
    {
      decorations: (v) => v.decorations,
    },
  );
}

However, after implementing this and playing around with it a bit, I discovered that my widget would conflict with the mark decoration that gets produced by the lint plugin, which we use to display lint errors. Specifically, the cm-lintRange mark, rendered by the lint package, wraps my custom widget which ruins the styling and leads to some pretty strange bugs. Here’s what it looks like in the DOM:

Notice my custom widget (parent of cm-indentation-marker) is wrapped by the cm-lintRange element whereas before the lintRange element wouldn’t be rendered at all since this line is empty.

This leads to some pretty strange bugs such as lines seemingly disappearing (being rendered with a zero length height):

cm-bug

It’s unclear to me whether this is a bug or intentional behavior since the docs only specify precedence between a mark component and another mark component:

Nesting order is determined by precedence of the facet or (below the facet-provided decorations) view plugin.

And between multiple widgets:

When multiple widgets sit at the same position, their side values will determine their ordering—those with a lower value come first.

but not between a conflicting widget and mark decoration.

In any case, this made me feel like I was approaching this the wrong way because I don’t want my overlayed content to interfere with the doc’s content like it is here which suggests that they shouldn’t live in the same place in the DOM.

My other attempts at this involved using tooltips (which was slow and interfered with the doc’s content) and mark decorators (which wouldn’t render at all for ranges with zero length). Line decorations don’t work either because the overlayed content depends on adding new elements to the DOM rather than just changing the styling of the line.

Let me know if there’s a way around this or what you would suggest doing to overlay content above an empty line in a way that doesn’t conflict with the doc’s content or decorations produced by other extensions. Thanks so much for your help!

1 Like

Marks will wrap around widgets (and other marks) with higher precedence. So wrapping your extension in a Prec.fallback to make sure it has lower precedence than the lint marks might help in this case.

Weird, I tried that but it still doesn’t work, even when wrapping the lint plugin in a Prec.override.

Worth noting that we load our LSP extensions dynamically via a dispatched transaction, e.g.:

const lspCompartment = new codemirror.state.Compartment();

// initialize extensions...
const extensions = [
  lspCompartment.of([]),
  // ...
];

// Set LSP Extensions after editor is initialized...
function setLspExtensions(extensions: Array<Extension>) {
  view.dispatch({
    effects: lspCompartment.reconfigure(extensions),
  });
},

Not sure if that makes a difference

That might be a bug. Can you reduce it to the simplest setup needed to trigger it? If that doesn’t show where the issue lies, I’ll debug it.

Sure thing! I’ve implemented a minimal repro here:

which is hosted here:

https://codemirror-bug-repro.sergeichestakov.repl.co/

Feel free to fork that repl as it might make it easier to view the code and play around with it.

Basically, this demo loads my extension (which adds widgets to empty lines + marks to non-empty lines) as well as an extension that creates fake LSP diagnostics and dispatches a transaction to add them to the view after the editor loads and after every change to the document. You’ll notice the lint plugin’s indentation markers wrap my custom plugin’s widgets regardless of precedence. Definitely seems like a bug since the Prec.fallback and Prec.override don’t appear to be doing anything here.

Hope that helps!

Hi Sergei, I’m still seeing some 350 lines of code there. I meant ‘simplest’ more in the sense of a plain editor view with two extensions that simply statically add the decorations needed to produce the problem (or if, more steps are needed to reproduce it, a script that simulates those steps).

Hey, sorry about that. I had originally pasted in the extension we were using as is but I just cut down the example I pasted above to only include the bare minimum needed to repro. If you open up the repl I pasted above now it should be much easier to read through. Hope that helps.

Thanks. I see what’s happening now—the lint module uses EditorView.decorations to provide decorations from the state, and you use a view plugin providing them through a plugin field. Plugin decorations currently always have a higher precedence than facet decorations. Which is problematic, I guess. I’ll spend some time thinking about how to better handle that — it seems this is a good illustration of the fact that client code needs full control over the ordering of decoration precedence.

(If one of the ‘strange bugs’ you mention in the initial post is the lines with just a wrapped widget collapsing vertically, that was a separate bug, which I’m fixing.)

Oh interesting. I wouldn’t have expected that to be the case. So it sounds like there’s no easy way around this at the moment? My initial thought would have been to rewrite my extension to use EditorView.decorations rather than a view plugin but that doesn’t allow me to scope these decorations to just the visible viewport (i.e. view.visibleRanges) since there’s no way to access the view from state.

And yes, the bug I’m referring to (besides the fact that the lint marks were wrapping my custom widgets) is that lines with just this widget wrapped in a lint mark have zero height. I think if that’s fixed I can work around the precedence issue so please do keep me posted when that’s patched :pray:

I’ve tagged @codemirror/view 0.19.4 with the fix.

1 Like

Perfect, that did the trick. Thanks so much! :pray:

I have a design for providing better control over precedence, but it’d be hard to pull off in a backwards-compatibly way, so it’ll have to wait until the next major version. See https://github.com/codemirror/codemirror.next/issues/567