From ce098d0ac2b617d0f4c5214c95607dee8cfaba87 Mon Sep 17 00:00:00 2001
From: Kevin Jahns <kevin.jahns@protonmail.com>
Date: Thu, 15 Jun 2023 12:40:28 +0200
Subject: [PATCH] refactor #538 (formatting attrs) a bit

---
 src/types/YText.js       | 101 ++++++++++++++++++++-------------------
 src/utils/Transaction.js |  13 +++--
 2 files changed, 57 insertions(+), 57 deletions(-)

diff --git a/src/types/YText.js b/src/types/YText.js
index 95df5391..79430c1a 100644
--- a/src/types/YText.js
+++ b/src/types/YText.js
@@ -476,6 +476,56 @@ export const cleanupYTextFormatting = type => {
   return res
 }
 
+/**
+ * This will be called by the transction once the event handlers are called to potentially cleanup
+ * formatting attributes.
+ *
+ * @param {Transaction} transaction
+ */
+export const cleanupYTextAfterTransaction = transaction => {
+  /**
+   * @type {Set<YText>}
+   */
+  const needFullCleanup = new Set()
+  // check if another formatting item was inserted
+  const doc = transaction.doc
+  for (const [client, afterClock] of transaction.afterState.entries()) {
+    const clock = transaction.beforeState.get(client) || 0
+    if (afterClock === clock) {
+      continue
+    }
+    iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
+      if (
+        !item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
+      ) {
+        needFullCleanup.add(/** @type {any} */ (item).parent)
+      }
+    })
+  }
+  // cleanup in a new transaction
+  transact(doc, (t) => {
+    iterateDeletedStructs(transaction, transaction.deleteSet, item => {
+      if (item instanceof GC || needFullCleanup.has(/** @type {YText} */ (item.parent))) {
+        return
+      }
+      const parent = /** @type {YText} */ (item.parent)
+      if (item.content.constructor === ContentFormat) {
+        needFullCleanup.add(parent)
+      } else {
+        // If no formatting attribute was inserted or deleted, we can make due with contextless
+        // formatting cleanups.
+        // Contextless: it is not necessary to compute currentAttributes for the affected position.
+        cleanupContextlessFormattingGap(t, item)
+      }
+    })
+    // If a formatting item was inserted, we simply clean the whole type.
+    // We need to compute currentAttributes for the current position anyway.
+    for (const yText of needFullCleanup) {
+      cleanupYTextFormatting(yText)
+    }
+  })
+}
+
 /**
  * @param {Transaction} transaction
  * @param {ItemTextListPosition} currPos
@@ -862,59 +912,10 @@ export class YText extends AbstractType {
     callTypeObservers(this, transaction, event)
     // If a remote change happened, we try to cleanup potential formatting duplicates.
     if (!transaction.local) {
-      transaction._yTexts.add(this)
+      transaction._needFormattingCleanup = true
     }
   }
 
-  /**
-   * @param {Transaction} transaction
-   */
-  static _cleanup (transaction) {
-    const withFormattingItems = new Set()
-    // check if another formatting item was inserted
-    const doc = transaction.doc
-    for (const [client, afterClock] of transaction.afterState.entries()) {
-      const clock = transaction.beforeState.get(client) || 0
-      if (afterClock === clock) {
-        continue
-      }
-      iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
-        if (!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && !(item instanceof GC) && transaction._yTexts.has(/** @type YText */ (item.parent))) {
-          withFormattingItems.add(item.parent)
-        }
-      })
-    }
-    iterateDeletedStructs(transaction, transaction.deleteSet, item => {
-      if (item instanceof GC) {
-        return
-      }
-      if (transaction._yTexts.has(/** @type YText */ (item.parent)) && item.content.constructor === ContentFormat) {
-        withFormattingItems.add(item.parent)
-      }
-    })
-    transact(doc, (t) => {
-      for (const yText of transaction._yTexts) {
-        if (withFormattingItems.has(yText)) {
-          // If a formatting item was inserted, we simply clean the whole type.
-          // We need to compute currentAttributes for the current position anyway.
-          cleanupYTextFormatting(yText)
-        } else {
-          // If no formatting attribute was inserted, we can make due with contextless
-          // formatting cleanups.
-          // Contextless: it is not necessary to compute currentAttributes for the affected position.
-          iterateDeletedStructs(t, t.deleteSet, item => {
-            if (item instanceof GC) {
-              return
-            }
-            if (item.parent === yText) {
-              cleanupContextlessFormattingGap(t, item)
-            }
-          })
-        }
-      }
-    })
-  }
-
   /**
    * Returns the unformatted string representation of this YText type.
    *
diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js
index df1c3903..299835ee 100644
--- a/src/utils/Transaction.js
+++ b/src/utils/Transaction.js
@@ -11,7 +11,8 @@ import {
   Item,
   generateNewClientId,
   createID,
-  UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc, YText // eslint-disable-line
+  cleanupYTextAfterTransaction,
+  UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractType, AbstractStruct, YEvent, Doc // eslint-disable-line
 } from '../internals.js'
 
 import * as map from 'lib0/map'
@@ -115,9 +116,9 @@ export class Transaction {
      */
     this.subdocsLoaded = new Set()
     /**
-     * @type {Set<YText>}
+     * @type {boolean}
      */
-    this._yTexts = new Set()
+    this._needFormattingCleanup = false
   }
 }
 
@@ -299,10 +300,8 @@ const cleanupTransactions = (transactionCleanups, i) => {
         fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
       })
       callAll(fs, [])
-      if (transaction._yTexts.size > 0) {
-        transact(doc, () => {
-          YText._cleanup(transaction)
-        })
+      if (transaction._needFormattingCleanup) {
+        cleanupYTextAfterTransaction(transaction)
       }
     } finally {
       // Replace deleted items with ItemDeleted / GC.