fix suggestion issues with formatting by introducing an option to disable automattic formatting cleanups

This commit is contained in:
Kevin Jahns
2025-06-02 14:12:43 +02:00
parent 4d508918e9
commit c37ee3ee8c
3 changed files with 20 additions and 4 deletions

View File

@@ -387,6 +387,7 @@ const insertText = (transaction, parent, currPos, text, attributes) => {
* @function * @function
*/ */
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => { const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
if (!transaction.doc.cleanupFormatting) return 0
/** /**
* @type {Item|null} * @type {Item|null}
*/ */
@@ -417,6 +418,7 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
if (endFormats.get(key) !== content || startAttrValue === value) { if (endFormats.get(key) !== content || startAttrValue === value) {
// Either this format is overwritten or it is not necessary because the attribute already existed. // Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction) start.delete(transaction)
transaction.cleanUps.add(start.id.client, start.id.clock, start.length)
cleanups++ cleanups++
if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) { if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
if (startAttrValue === null) { if (startAttrValue === null) {
@@ -443,6 +445,7 @@ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAtt
* @param {Item | null} item * @param {Item | null} item
*/ */
const cleanupContextlessFormattingGap = (transaction, item) => { const cleanupContextlessFormattingGap = (transaction, item) => {
if (!transaction.doc.cleanupFormatting) return 0
// iterate until item.right is null or content // iterate until item.right is null or content
while (item && item.right && (item.right.deleted || !item.right.countable)) { while (item && item.right && (item.right.deleted || !item.right.countable)) {
item = item.right item = item.right
@@ -454,6 +457,7 @@ const cleanupContextlessFormattingGap = (transaction, item) => {
const key = /** @type {ContentFormat} */ (item.content).key const key = /** @type {ContentFormat} */ (item.content).key
if (attrs.has(key)) { if (attrs.has(key)) {
item.delete(transaction) item.delete(transaction)
transaction.cleanUps.add(item.id.client, item.id.clock, item.length)
} else { } else {
attrs.add(key) attrs.add(key)
} }
@@ -475,6 +479,7 @@ const cleanupContextlessFormattingGap = (transaction, item) => {
* @return {number} How many formatting attributes have been cleaned up. * @return {number} How many formatting attributes have been cleaned up.
*/ */
export const cleanupYTextFormatting = type => { export const cleanupYTextFormatting = type => {
if (!type.doc?.cleanupFormatting) return 0
let res = 0 let res = 0
transact(/** @type {Doc} */ (type.doc), transaction => { transact(/** @type {Doc} */ (type.doc), transaction => {
let start = /** @type {Item} */ (type._start) let start = /** @type {Item} */ (type._start)

View File

@@ -33,6 +33,9 @@ export const generateNewClientId = random.uint32
* @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well. * @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well.
* @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically. * @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically.
* @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load() * @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load()
* @property {boolean} [DocOpts.isSuggestionDoc] Set to true if this document merely suggests
* changes. If this flag is not set in a suggestion document, automatic formatting changes will be
* displayed as suggestions, which might not be intended.
*/ */
/** /**
@@ -59,13 +62,15 @@ export class Doc extends ObservableV2 {
/** /**
* @param {DocOpts} opts configuration * @param {DocOpts} opts configuration
*/ */
constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) { constructor ({ guid = random.uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true, isSuggestionDoc = true } = {}) {
super() super()
this.gc = gc this.gc = gc
this.gcFilter = gcFilter this.gcFilter = gcFilter
this.clientID = generateNewClientId() this.clientID = generateNewClientId()
this.guid = guid this.guid = guid
this.collectionid = collectionid this.collectionid = collectionid
this.isSuggestionDoc = isSuggestionDoc
this.cleanupFormatting = !isSuggestionDoc
/** /**
* @type {Map<string, AbstractType<YEvent<any>>>} * @type {Map<string, AbstractType<YEvent<any>>>}
*/ */
@@ -350,9 +355,10 @@ export class Doc extends ObservableV2 {
/** /**
* @param {Doc} ydoc * @param {Doc} ydoc
* @param {DocOpts} [opts]
*/ */
export const cloneDoc = ydoc => { export const cloneDoc = (ydoc, opts) => {
const clone = new Doc() const clone = new Doc(opts)
applyUpdate(clone, encodeStateAsUpdate(ydoc)) applyUpdate(clone, encodeStateAsUpdate(ydoc))
return clone return clone
} }

View File

@@ -62,6 +62,11 @@ export class Transaction {
* Describes the set of deleted items by ids * Describes the set of deleted items by ids
*/ */
this.deleteSet = createIdSet() this.deleteSet = createIdSet()
/**
* Describes the set of items that are cleaned up / deleted by ids. It is a subset of
* this.deleteSet
*/
this.cleanUps = createIdSet()
/** /**
* Describes the set of inserted items by ids * Describes the set of inserted items by ids
*/ */
@@ -354,7 +359,7 @@ const cleanupTransactions = (transactionCleanups, i) => {
}) })
fs.push(() => doc.emit('afterTransaction', [transaction, doc])) fs.push(() => doc.emit('afterTransaction', [transaction, doc]))
callAll(fs, []) callAll(fs, [])
if (transaction._needFormattingCleanup) { if (transaction._needFormattingCleanup && doc.cleanupFormatting) {
cleanupYTextAfterTransaction(transaction) cleanupYTextAfterTransaction(transaction)
} }
} finally { } finally {