How to calculate the changed ranges for transactions

I want to calculate the changed ranges for transactions (even with multiple steps).

I’m aware of findDiffStart and findDiffEnd but that’s not enough if a change something at the start and end of the document within one transaction.

let diffStart = oldState.doc.content.findDiffStart(newState.doc.content)
let diffEnd = oldState.doc.content.findDiffEnd(newState.doc.content)

With step.getMap() I can get the changed ranges of each step but these positions depend on the current doc of the step (transaction.docs[stepIndex]).

That’s why I thought I could map these positions forwards and backwards based on its index so that I get the correct positions for the oldState and newState of the whole transaction.

transaction.steps.forEach((step, index) => {

  const upcomingMapping = transaction.mapping
    .slice(index, transaction.mapping.maps.length)

  const pastMapping = transaction.mapping
    .slice(0, index)
    .invert()

  step.getMap().forEach((oldStart, oldEnd, newStart, newEnd) => {
    const oldStart = pastMapping.map(oldStart, -1)
    const oldEnd = pastMapping.map(oldEnd)
    const newStart = upcomingMapping.map(oldStart, -1)
    const newEnd = upcomingMapping.map(oldEnd)
  })

})

That’s working fine for “normal” changes but breaks (out of range) if I undo changes with the history plugin. So I guess it’s not that easy to map the positions :upside_down_face:

Does anyone have any ideas what I can do differently or better?

2 Likes

Depending on how precise you need these to be (and how heavily you use them) it may be worth pulling in prosemirror-changeset as a dependency. Your code seems to mostly have the right idea but you’ll definitely want to slice from index + 1 in upcomingMapping (newStart already points into the document after the step). Maybe that’s what’s causing the problem?

Your code seems to mostly have the right idea but you’ll definitely want to slice from index + 1 in upcomingMapping (newStart already points into the document after the step). Maybe that’s what’s causing the problem?

Yeah you are right, but in my example I’m always calculating based on oldStart and oldEnd.

So basically this:

const upcomingMapping = transaction.mapping
    .slice(index, transaction.mapping.maps.length)

step.getMap().forEach((oldStart, oldEnd, newStart, newEnd) => {
  // using old values here
  const newStart = upcomingMapping.map(oldStart, -1)
  const newEnd = upcomingMapping.map(oldEnd)
})

Should be the same as this, right? (At least it makes no difference in my case.)

const upcomingMapping = transaction.mapping
    .slice(index + 1, transaction.mapping.maps.length)

step.getMap().forEach((oldStart, oldEnd, newStart, newEnd) => {
  // using new values here
  const newStart = upcomingMapping.map(newStart, -1)
  const newEnd = upcomingMapping.map(newEnd)
})

But I have problems with getting the correct values for oldEnd when undoing changes. When starting with an empty document and typing one character I get these values (which are correct):

{oldStart: 1, oldEnd: 1, newStart: 1, newEnd: 2}

When undoing these steps I get these values:

{oldStart: 1, oldEnd: 3, newStart: 1, newEnd: 1}

I don’t get what I’m doing wrong in this most simple change like described above. Any idea?

Got it!

transaction.mapping.maps.forEach((stepMap, index) => {
  stepMap.forEach((from, to) => {
    const newStart = transaction.mapping.slice(index).map(from, -1)
    const newEnd = transaction.mapping.slice(index).map(to)
    const oldStart = transaction.mapping.invert().map(newStart, -1)
    const oldEnd = transaction.mapping.invert().map(newEnd)
  })
})