diffStart after diffEnd ?!?

Hello,

I have a plugin that adds node decorations to empty nodes, so that I can use CSS to show that they are empty.

To update the decorations, the plugin’s apply method uses findDiffStart / findDiffEnd and then nodesBetween to only look at the nodes that may have changed.

Surprisingly, the value returned by findDiffStart is often larger than those returned by findDiffEnd. I “fixed” it by taking the min and the max before calling nodesBetween but this does not sound right…

Am I doing something wrong?

Here is the code of the plugin:

// nodes that should receive an 'empty' class if they are empty
const emptyNodeTypes = ['paragraph', 'section', 'listing', 'quote', 'example']

function isEmptyNode(node: Node) {
  return emptyNodeTypes.indexOf(node.type.name) >= 0 && node.nodeSize === 2
}

export const emptyPlugin: Plugin = new Plugin({
  state: {
    init(_, {doc}) {
      // go over the entire document and create the initial set of decorations
      let emptyBlocks: Decoration[] = []
      doc.descendants((node, pos, _parent, _index) => {
        if (isEmptyNode(node))
          emptyBlocks.push(Decoration.node(pos, pos+node.nodeSize, { class: 'empty' }))
        return true
      })
      return DecorationSet.create(doc, emptyBlocks)
    },
    apply(tr, set) {
      // update the decorations by going over the part of the document that was changed

      // transform the decorations through the transaction
      set = set.map(tr.mapping, tr.doc)

      // if there is no steps, we're done
      if (tr.docs.length === 0)
        return set

      // find the range of the changes
      let diffStart = tr.docs[0].content.findDiffStart(tr.doc.content)
      let diffEnd = tr.docs[0].content.findDiffEnd(tr.doc.content)
      if (diffStart === null || diffEnd === null) // there was no change
        return set

      // in some cases start is after end!
      const start = Math.min(diffStart, diffEnd.a, diffEnd.b)
      const end = Math.max(diffStart, diffEnd.a, diffEnd.b) +1 // sometimes we miss the last one...

      // go over the nodes in the range and record the decorations to add/remove
      let add: Decoration[] = []
      let remove: Decoration[] = []
      tr.doc.nodesBetween(start, end, (node, pos, _parent, _index) => {
        if (emptyNodeTypes.indexOf(node.type.name) >= 0) {
          const deco = Decoration.node(pos, pos+node.nodeSize, { class: 'empty'})
          if (node.nodeSize === 2)
            add.push(deco)
          else
            remove.push(deco)
        }
        return true
      })
      // update the decoration set and return it
      return set.remove(remove).add(tr.doc, add)
    }
  },
  props: {
    decorations(state) {
      return emptyPlugin.getState(state)
    }
  }
})

findDiffStart and findDiffEnd just scan the document from one side until they find a difference. If your documents are, say, “abab” and “ab”, both scans will cover the entirety of the second document, so findDiffStart will return 2 and findDiffEnd 0. This is an expected feature of this kind of comparing.

When moving one side to compensate for this, you must make sure to also move the other side by the same amount. This code in prosemirror-view could serve as an example.

Thank you @marijn, this explains it.

I was expecting the diff to be based on some sort of unique node id so that when the user creates a new empty paragraph, say in the middle of other empty paragraphs, I would know which one is new (since the others would already have the “empty” decoration, but not the new one).

From what I see from other posts, this is not really possible. My approch of using the min and max of diffStart and diffEnd overestimates the part of the document that has changed, but it seems to work.

I guess if I made “empty” an attribute rather than a decoration, the diff would work better, but I prefer avoiding an attribute for this.