Suggestion: Stable order or priority configuration for `blockwrapper`

hi @marijn , thank you for providing such a useful feature with blockwrapper—it has been a great help in our development.

I noticed that when multiple blockwrappers are nested at the same position, their rendering order seems unstable. It would be great to have a parameter or configuration to ensure a stable order, for example by allowing explicit priorities.

Here is a small demo illustrating the issue.

import {basicSetup, EditorView} from "codemirror"
import {BlockWrapper, ViewPlugin} from "@codemirror/view"
import {Range} from "@codemirror/state"
import {syntaxTree} from "@codemirror/language"
import {markdown} from "@codemirror/lang-markdown"
import {languages} from "@codemirror/language-data"

let view = new EditorView({
  doc: "Hello\n\n```javascript\nlet x = 'y'\n```\nWorld\n```javascript\nlet x = 'y'\n```",
  extensions: [
    basicSetup,
    markdown({codeLanguages: languages}),
    EditorView.theme({
      ".cm-fenced-code-block-red > .cm-line": {
        backgroundColor: "rgba(255 0 0 / 0.5)"
      },
      ".cm-fenced-code-block-blue > .cm-line": {
        backgroundColor: "rgba(0 0 255 / 0.5)"
      },
      ".cm-fenced-code-block-green > .cm-line": {
        backgroundColor: "rgba(0 255 0 / 0.5)"
      }
    }),
    ViewPlugin.fromClass(class {}, {
      provide: () => EditorView.blockWrappers.of(view => {
        const ranges = []
        for (const {from, to} of view.visibleRanges) {
          syntaxTree(view.state).iterate({
            from, to,
            enter: node => {
              if (node.name === 'FencedCode') {
                ranges.push(BlockWrapper.create({
                  tagName: 'div',
                  attributes: {'class': 'cm-fenced-code-block-red'}
                }).range(node.from, node.to))
                ranges.push(BlockWrapper.create({
                  tagName: 'div',
                  attributes: {'class': 'cm-fenced-code-block-blue'}
                }).range(node.from, node.to))
                 ranges.push(BlockWrapper.create({
                  tagName: 'div',
                  attributes: {'class': 'cm-fenced-code-block-green'}
                }).range(node.from, node.to))
              }
            }
          })
        }
        return BlockWrapper.set(ranges)
      })
    })
  ],
  parent: document.body
})

I didn’t really anticipate people emitting multiple identical wrappers from the same decoration source. If you use multiple sources, the relative precedence of the decoration facet will determine their nesting. Is that maybe a workable way to avoid this issue for you, or is the set of different types of wrappers that you have open, and not something you can encode in a fixed set of sources?

Multiple wrappers are used to achieve the desired CSS styling, as a single wrapper sometimes isn’t sufficient.

Handling the same functionality through multiple decoration facets may not be ideal from a code structure perspective. Of course, for this particular requirement, it is also a viable approach.

All right, it’s easy enough to support something like this. Does this patch look like it’d solve the problem for you?

Yep, that’s it! This is awesome! It just solved a huge problem for me—without it, my solution would’ve been way more complicated!

Great. Released in version 6.43.0.