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} + */ + 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} */ (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} */ (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} + * @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.