Compare commits

..

9 Commits

Author SHA1 Message Date
Kevin Jahns
3f34777201 13.5.27 2022-02-04 12:42:59 +01:00
Kevin Jahns
24eddb2d75 fix concurrent formatting / cleanup bug 2022-02-04 12:41:13 +01:00
Kevin Jahns
8ce107bd17 Merge branch 'ja2nicholl-update-attributes-on-delete' 2022-02-04 11:27:07 +01:00
Kevin Jahns
2d1e3fde43 fixed edge formatting case 2022-02-04 11:26:32 +01:00
Kevin Jahns
04009f0d42 Merge branch 'update-attributes-on-delete' of git://github.com/ja2nicholl/yjs into ja2nicholl-update-attributes-on-delete 2022-02-04 10:39:41 +01:00
Kevin Jahns
d69d93f812 13.5.26 2022-02-03 21:38:48 +01:00
Kevin Jahns
931a37a331 only fire stack-item-added when and item was actually added. closes #368 2022-02-03 21:36:54 +01:00
Jeremy Nicholl
84e95f11cb Fix formatting 2022-02-03 15:19:57 -05:00
Jeremy Nicholl
164b38f0cd Avoid copying attribute map when deleting
Calling cleanupFormattingGap should not make a copy of the
attributes because it needs to be able to update them.
2022-01-31 14:49:16 -05:00
5 changed files with 56 additions and 12 deletions

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.5.25", "version": "13.5.27",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.5.25", "version": "13.5.27",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",

View File

@@ -338,14 +338,16 @@ const formatText = (transaction, parent, currPos, length, attributes) => {
* *
* @param {Transaction} transaction * @param {Transaction} transaction
* @param {Item} start * @param {Item} start
* @param {Item|null} end exclusive end, automatically iterates to the next Content Item * @param {Item|null} curr exclusive end, automatically iterates to the next Content Item
* @param {Map<string,any>} startAttributes * @param {Map<string,any>} startAttributes
* @param {Map<string,any>} endAttributes This attribute is modified! * @param {Map<string,any>} currAttributes
* @return {number} The amount of formatting Items deleted. * @return {number} The amount of formatting Items deleted.
* *
* @function * @function
*/ */
const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttributes) => { const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
let end = curr
const endAttributes = map.copy(currAttributes)
while (end && (!end.countable || end.deleted)) { while (end && (!end.countable || end.deleted)) {
if (!end.deleted && end.content.constructor === ContentFormat) { if (!end.deleted && end.content.constructor === ContentFormat) {
updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content)) updateCurrentAttributes(endAttributes, /** @type {ContentFormat} */ (end.content))
@@ -353,7 +355,11 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
end = end.right end = end.right
} }
let cleanups = 0 let cleanups = 0
let reachedEndOfCurr = false
while (start !== end) { while (start !== end) {
if (curr === start) {
reachedEndOfCurr = true
}
if (!start.deleted) { if (!start.deleted) {
const content = start.content const content = start.content
switch (content.constructor) { switch (content.constructor) {
@@ -363,6 +369,9 @@ const cleanupFormattingGap = (transaction, start, end, startAttributes, endAttri
// 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)
cleanups++ cleanups++
if (!reachedEndOfCurr && (currAttributes.get(key) || null) === value && (startAttributes.get(key) || null) !== value) {
currAttributes.delete(key)
}
} }
break break
} }
@@ -465,7 +474,7 @@ const deleteText = (transaction, currPos, length) => {
currPos.forward() currPos.forward()
} }
if (start) { if (start) {
cleanupFormattingGap(transaction, start, currPos.right, startAttrs, map.copy(currPos.currentAttributes)) cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes)
} }
const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent) const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
if (parent._searchMarker) { if (parent._searchMarker) {
@@ -684,7 +693,7 @@ export class YTextEvent extends YEvent {
} else { } else {
attributes[key] = value attributes[key] = value
} }
} else { } else if (value !== null) {
item.delete(transaction) item.delete(transaction)
} }
} }
@@ -710,7 +719,7 @@ export class YTextEvent extends YEvent {
} else { } else {
attributes[key] = value attributes[key] = value
} }
} else { } else if (attr !== null) { // this will be cleaned up automatically by the contextless cleanup function
item.delete(transaction) item.delete(transaction)
} }
} }

View File

@@ -186,6 +186,7 @@ export class UndoManager extends Observable {
} }
}) })
const now = time.getUnixTime() const now = time.getUnixTime()
let didAdd = false
if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) { if (now - this.lastChange < captureTimeout && stack.length > 0 && !undoing && !redoing) {
// append change to last stack op // append change to last stack op
const lastOp = stack[stack.length - 1] const lastOp = stack[stack.length - 1]
@@ -194,6 +195,7 @@ export class UndoManager extends Observable {
} else { } else {
// create a new stack op // create a new stack op
stack.push(new StackItem(transaction.deleteSet, insertions)) stack.push(new StackItem(transaction.deleteSet, insertions))
didAdd = true
} }
if (!undoing && !redoing) { if (!undoing && !redoing) {
this.lastChange = now this.lastChange = now
@@ -204,7 +206,9 @@ export class UndoManager extends Observable {
keepItem(item, true) keepItem(item, true)
} }
}) })
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]) if (didAdd) {
this.emit('stack-item-added', [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this])
}
}) })
} }

View File

@@ -6,6 +6,10 @@ import * as math from 'lib0/math'
const { init, compare } = Y const { init, compare } = Y
/** /**
* In this test we are mainly interested in the cleanup behavior and whether the resulting delta makes sense.
* It is fine if the resulting delta is not minimal. But applying the delta to a rich-text editor should result in a
* synced document.
*
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testDeltaAfterConcurrentFormatting = tc => { export const testDeltaAfterConcurrentFormatting = tc => {
@@ -14,12 +18,17 @@ export const testDeltaAfterConcurrentFormatting = tc => {
testConnector.flushAllMessages() testConnector.flushAllMessages()
text0.format(0, 3, { bold: true }) text0.format(0, 3, { bold: true })
text1.format(2, 2, { bold: true }) text1.format(2, 2, { bold: true })
let delta = null /**
* @type {any}
*/
const deltas = []
text1.observe(event => { text1.observe(event => {
delta = event.delta if (event.delta.length > 0) {
deltas.push(event.delta)
}
}) })
testConnector.flushAllMessages() testConnector.flushAllMessages()
t.compare(delta, []) t.compare(deltas, [[{ retain: 3, attributes: { bold: true } }, { retain: 2, attributes: { bold: null } }]])
} }
/** /**
@@ -138,6 +147,28 @@ export const testNotMergeEmptyLinesFormat = tc => {
]) ])
} }
/**
* @param {t.TestCase} tc
*/
export const testPreserveAttributesThroughDelete = tc => {
const ydoc = new Y.Doc()
const testText = ydoc.getText('test')
testText.applyDelta([
{ insert: 'Text' },
{ insert: '\n', attributes: { title: true } },
{ insert: '\n' }
])
testText.applyDelta([
{ retain: 4 },
{ delete: 1 },
{ retain: 1, attributes: { title: true } }
])
t.compare(testText.toDelta(), [
{ insert: 'Text' },
{ insert: '\n', attributes: { title: true } }
])
}
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */