Track Changes w/ marks

I’ve done a lot of research about how to use ProseMirror to track deletions (e.g. prosemirror-changeset, fiduswriter, etc.), but I haven’t found anything that works how I think track changes should work.

I’ve been trying to use rebaseSteps from prosemirror-collab along with inserted and deleted marks to mark how the state has changed over a series of steps. The code below seems to work fine for changes within a paragraph, but when there is a ReplaceStep that spans paragraphs and lists items, I run into issues like “Inconsistent open depths”.

My thinking was that if looped through the original steps, then I could add additional steps to each of the original steps and then rebase the remaining original steps. This seems similar to how prosemirror-collab works where the extra steps I’m adding are the steps provided in receiveTransaction here:

There must either be something wrong with my logic or the code, but I’m not sure what. Any ideas? Any help would be greatly appreciated!

export type Commit = {
  createdAt: Date
  steps: Record<string, any>[]
  userId: string

class Rebaseable {
  readonly step: Step
  readonly inverted: Step
  readonly origin: any

    step: Step,
    inverted: Step,
    origin: any
  ) {
    this.step = step
    this.inverted = inverted
    this.origin = origin

// : ([Rebaseable], [Step], Transform) → [Rebaseable]
// Undo a given set of steps, apply a set of other steps, and then
// redo them.
export function rebaseSteps(
  steps: Rebaseable[],
  over: Step[],
  transform: Transform
) {
  for (let i = steps.length - 1; i >= 0; i--) transform.step(steps[i].inverted)
  for (let i = 0; i < over.length; i++) transform.step(over[i])
  let result = []
  for (let i = 0, mapFrom = steps.length; i < steps.length; i++) {
    let mapped = steps[i]
    if (mapped && !transform.maybeStep(mapped).failed) {
      // @ts-ignore
      transform.mapping.setMirror(mapFrom, transform.steps.length - 1)
      result.push(new Rebaseable(mapped, mapped.invert([ - 1]), steps[i].origin))
  return result

function unconfirmedFrom(transform: Transform) {
  let result = []
  for (let i = 0; i < transform.steps.length; i++)
    result.push(new Rebaseable(transform.steps[i],
  return result

class Blame {
  readonly commits: Commit[]
  readonly state: EditorState

  constructor (
    initialState: EditorState,
    commits: Commit[]
  ) {
    this.commits = commits

    // Convert commits to steps
    let originalTr =
    for (const commit of commits) {
      for (const json of commit.steps) {
        const step = Step.fromJSON(defaultSchema, json)
    let originalSteps = unconfirmedFrom(originalTr)

    // Make the commits
    this.state = initialState
    while (originalSteps.length) {
      const originalStep = originalSteps.shift()!
      const { step } = originalStep
      console.log('step', step)

      const originalStepTr =
      this.state = this.state.apply(originalStepTr)

      const trackTr =
      if (step instanceof ReplaceStep) { 
        // Mark the insert, if any
        if (step.slice.size) {
          const insertedMark = this.state.doc.type.schema.marks.inserted.create()
          const { from } = step
          const to = from + step.slice.size
          trackTr.addMark(from, to, insertedMark)

        // Re-add/mark the delete, if any
        if (step.from !== {
          const invertedStep = step.invert([0]) as ReplaceStep
          const deleteStep = new ReplaceStep(

          const { from } = deleteStep
          const to = from + deleteStep.slice.size
          const deletedMark = this.state.doc.type.schema.marks.deleted.create()
          trackTr.addMark(from, to, deletedMark)

      if (trackTr.steps.length) {
        originalSteps = rebaseSteps(originalSteps, trackTr.steps,
        this.state = this.state.apply(trackTr)

Here is a glitch that demonstrates where I’m at right now