From e0b76cd2f432aabab49509e93d94191185d3a94d Mon Sep 17 00:00:00 2001
From: Kevin Jahns <kevin.jahns@pm.me>
Date: Thu, 11 Mar 2021 18:52:35 +0100
Subject: [PATCH] [UndoManager] stop tracking unrelated insertions -
 yjs/y-monaco#10

---
 README.md                |  5 ++-
 src/utils/UndoManager.js | 84 ++++++++++++++++------------------------
 2 files changed, 36 insertions(+), 53 deletions(-)

diff --git a/README.md b/README.md
index f33c138c..2579b6d6 100644
--- a/README.md
+++ b/README.md
@@ -614,8 +614,9 @@ parameter that is stored on <code>transaction.origin</code> and
   <b><code>toJSON():any</code></b>
   <dd>
 Deprecated: It is recommended to call toJSON directly on the shared types.
-Converts the entire document into a js object, recursively traversing each yjs type. Doesn't
-log types that have not been defined (using <code>ydoc.getType(..)</code>). 
+Converts the entire document into a js object, recursively traversing each yjs
+type. Doesn't log types that have not been defined (using
+<code>ydoc.getType(..)</code>).
   </dd>
   <b><code>get(string, Y.[TypeClass]):[Type]</code></b>
   <dd>Define a shared type.</dd>
diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js
index 3a01000b..022d8985 100644
--- a/src/utils/UndoManager.js
+++ b/src/utils/UndoManager.js
@@ -5,11 +5,11 @@ import {
   transact,
   createID,
   redoItem,
-  iterateStructs,
   isParentOf,
   followRedone,
   getItemCleanStart,
-  getState,
+  isDeleted,
+  addToDeleteSet,
   Transaction, Doc, Item, GC, DeleteSet, AbstractType, YEvent // eslint-disable-line
 } from '../internals.js'
 
@@ -18,14 +18,12 @@ import { Observable } from 'lib0/observable.js'
 
 class StackItem {
   /**
-   * @param {DeleteSet} ds
-   * @param {Map<number,number>} beforeState
-   * @param {Map<number,number>} afterState
+   * @param {DeleteSet} deletions
+   * @param {DeleteSet} insertions
    */
-  constructor (ds, beforeState, afterState) {
-    this.ds = ds
-    this.beforeState = beforeState
-    this.afterState = afterState
+  constructor (deletions, insertions) {
+    this.insertions = insertions
+    this.deletions = deletions
     /**
      * Use this to save and restore metadata like selection range
      */
@@ -65,48 +63,26 @@ const popStackItem = (undoManager, stack, eventType) => {
        */
       const itemsToDelete = []
       let performedChange = false
-      stackItem.afterState.forEach((endClock, client) => {
-        const startClock = stackItem.beforeState.get(client) || 0
-        const len = endClock - startClock
-        // @todo iterateStructs should not need the structs parameter
-        const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client))
-        if (startClock !== endClock) {
-          // make sure structs don't overlap with the range of created operations [stackItem.start, stackItem.start + stackItem.end)
-          // this must be executed before deleted structs are iterated.
-          getItemCleanStart(transaction, createID(client, startClock))
-          if (endClock < getState(doc.store, client)) {
-            getItemCleanStart(transaction, createID(client, endClock))
-          }
-          iterateStructs(transaction, structs, startClock, len, struct => {
-            if (struct instanceof Item) {
-              if (struct.redone !== null) {
-                let { item, diff } = followRedone(store, struct.id)
-                if (diff > 0) {
-                  item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
-                }
-                if (item.length > len) {
-                  getItemCleanStart(transaction, createID(item.id.client, endClock))
-                }
-                struct = item
-              }
-              if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
-                itemsToDelete.push(struct)
-              }
+      iterateDeletedStructs(transaction, stackItem.insertions, struct => {
+        if (struct instanceof Item) {
+          if (struct.redone !== null) {
+            let { item, diff } = followRedone(store, struct.id)
+            if (diff > 0) {
+              item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff))
             }
-          })
+            struct = item
+          }
+          if (!struct.deleted && scope.some(type => isParentOf(type, /** @type {Item} */ (struct)))) {
+            itemsToDelete.push(struct)
+          }
         }
       })
-      iterateDeletedStructs(transaction, stackItem.ds, struct => {
-        const id = struct.id
-        const clock = id.clock
-        const client = id.client
-        const startClock = stackItem.beforeState.get(client) || 0
-        const endClock = stackItem.afterState.get(client) || 0
+      iterateDeletedStructs(transaction, stackItem.deletions, struct => {
         if (
           struct instanceof Item &&
           scope.some(type => isParentOf(type, struct)) &&
-          // Never redo structs in [stackItem.start, stackItem.start + stackItem.end) because they were created and deleted in the same capture interval.
-          !(clock >= startClock && clock < endClock)
+          // Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
+          !isDeleted(stackItem.insertions, struct.id)
         ) {
           itemsToRedo.add(struct)
         }
@@ -201,17 +177,23 @@ export class UndoManager extends Observable {
         // neither undoing nor redoing: delete redoStack
         this.redoStack = []
       }
-      const beforeState = transaction.beforeState
-      const afterState = transaction.afterState
+      const insertions = new DeleteSet()
+      transaction.afterState.forEach((endClock, client) => {
+        const startClock = transaction.beforeState.get(client) || 0
+        const len = endClock - startClock
+        if (len > 0) {
+          addToDeleteSet(insertions, client, startClock, len)
+        }
+      })
       const now = time.getUnixTime()
       if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
         // append change to last stack op
         const lastOp = stack[stack.length - 1]
-        lastOp.ds = mergeDeleteSets([lastOp.ds, transaction.deleteSet])
-        lastOp.afterState = afterState
+        lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet])
+        lastOp.insertions = mergeDeleteSets([lastOp.insertions, insertions])
       } else {
         // create a new stack op
-        stack.push(new StackItem(transaction.deleteSet, beforeState, afterState))
+        stack.push(new StackItem(transaction.deleteSet, insertions))
       }
       if (!undoing && !redoing) {
         this.lastChange = now
@@ -232,7 +214,7 @@ export class UndoManager extends Observable {
        * @param {StackItem} stackItem
        */
       const clearItem = stackItem => {
-        iterateDeletedStructs(transaction, stackItem.ds, item => {
+        iterateDeletedStructs(transaction, stackItem.deletions, item => {
           if (item instanceof Item && this.scope.some(type => isParentOf(type, item))) {
             keepItem(item, false)
           }