-
Notifications
You must be signed in to change notification settings - Fork 1.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[lexical-yjs] Bug Fix: clean up dangling text after undo in collaboration #6670
Changes from all commits
0d062f6
4fbe7fe
a40b5b9
1d43245
8bc45e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -230,4 +230,161 @@ test.describe('Collaboration', () => { | |
focusPath: [1, 1, 0], | ||
}); | ||
}); | ||
|
||
test('Remove dangling text from YJS when there is no preceding text node', async ({ | ||
isRichText, | ||
page, | ||
isCollab, | ||
browserName, | ||
}) => { | ||
test.skip(!isCollab); | ||
|
||
// Left collaborator types two paragraphs of text | ||
await focusEditor(page); | ||
await page.keyboard.type('Line 1'); | ||
await page.keyboard.press('Enter'); | ||
await sleep(1050); // default merge interval is 1000, add 50ms as overhead due to CI latency. | ||
await page.keyboard.type('This is a test. '); | ||
|
||
// Right collaborator types at the end of paragraph 2 | ||
await sleep(1050); | ||
await page | ||
.frameLocator('iframe[name="right"]') | ||
.locator('[data-lexical-editor="true"]') | ||
.focus(); | ||
await page.keyboard.press('ArrowDown'); // Move caret to end of paragraph 2 | ||
await page.keyboard.press('ArrowDown'); | ||
await page.keyboard.type('Word'); | ||
|
||
await assertHTML( | ||
page, | ||
html` | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span data-lexical-text="true">Line 1</span> | ||
</p> | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span data-lexical-text="true">This is a test. Word</span> | ||
</p> | ||
`, | ||
); | ||
|
||
// Left collaborator undoes their text in the second paragraph. | ||
await sleep(50); | ||
await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click(); | ||
|
||
// The undo also removed the text node from YJS. | ||
// Check that the dangling text from right user was also removed. | ||
await assertHTML( | ||
page, | ||
html` | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span data-lexical-text="true">Line 1</span> | ||
</p> | ||
<p class="PlaygroundEditorTheme__paragraph"><br /></p> | ||
`, | ||
); | ||
|
||
// Left collaborator refreshes their page | ||
await page.evaluate(() => { | ||
document | ||
.querySelector('iframe[name="left"]') | ||
.contentDocument.location.reload(); | ||
}); | ||
|
||
// Page content should be the same as before the refresh | ||
await assertHTML( | ||
page, | ||
html` | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span data-lexical-text="true">Line 1</span> | ||
</p> | ||
<p class="PlaygroundEditorTheme__paragraph"><br /></p> | ||
`, | ||
); | ||
Comment on lines
+301
to
+311
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you comment out the |
||
}); | ||
|
||
test('Merge dangling text into preceding text node', async ({ | ||
isRichText, | ||
page, | ||
isCollab, | ||
browserName, | ||
}) => { | ||
test.skip(!isCollab); | ||
|
||
// Left collaborator types two pieces of text in the same paragraph, but with different styling. | ||
await focusEditor(page); | ||
await page.keyboard.type('normal'); | ||
await sleep(1050); | ||
await toggleBold(page); | ||
await page.keyboard.type('bold'); | ||
|
||
// Right collaborator types at the end of the paragraph. | ||
await sleep(50); | ||
await page | ||
.frameLocator('iframe[name="right"]') | ||
.locator('[data-lexical-editor="true"]') | ||
.focus(); | ||
await page.keyboard.press('ArrowDown'); // Move caret to end of paragraph | ||
await page.keyboard.type('BOLD'); | ||
|
||
await assertHTML( | ||
page, | ||
html` | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span data-lexical-text="true">normal</span> | ||
<strong | ||
class="PlaygroundEditorTheme__textBold" | ||
data-lexical-text="true"> | ||
boldBOLD | ||
</strong> | ||
</p> | ||
`, | ||
); | ||
|
||
// Left collaborator undoes their bold text. | ||
await sleep(50); | ||
await page.frameLocator('iframe[name="left"]').getByLabel('Undo').click(); | ||
|
||
// The undo also removed bold the text node from YJS. | ||
// Check that the dangling text from right user was merged into the preceding text node. | ||
await assertHTML( | ||
page, | ||
html` | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span data-lexical-text="true">normalBOLD</span> | ||
</p> | ||
`, | ||
); | ||
Comment on lines
+360
to
+369
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fails on main with:
The |
||
|
||
// Left collaborator refreshes their page | ||
await page.evaluate(() => { | ||
document | ||
.querySelector('iframe[name="left"]') | ||
.contentDocument.location.reload(); | ||
}); | ||
|
||
// Page content should be the same as before the refresh | ||
await assertHTML( | ||
page, | ||
html` | ||
<p | ||
class="PlaygroundEditorTheme__paragraph PlaygroundEditorTheme__ltr" | ||
dir="ltr"> | ||
<span data-lexical-text="true">normalBOLD</span> | ||
</p> | ||
`, | ||
); | ||
Comment on lines
+379
to
+388
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On main, this |
||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -157,21 +157,25 @@ export class CollabElementNode { | |
nodeIndex !== 0 ? children[nodeIndex - 1] : null; | ||
const nodeSize = node.getSize(); | ||
|
||
if ( | ||
offset === 0 && | ||
delCount === 1 && | ||
nodeIndex > 0 && | ||
prevCollabNode instanceof CollabTextNode && | ||
length === nodeSize && | ||
// If the node has no keys, it's been deleted | ||
Array.from(node._map.keys()).length === 0 | ||
) { | ||
// Merge the text node with previous. | ||
prevCollabNode._text += node._text; | ||
children.splice(nodeIndex, 1); | ||
} else if (offset === 0 && delCount === nodeSize) { | ||
// The entire thing needs removing | ||
if (offset === 0 && length === nodeSize) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Our observation is that |
||
// Text node has been deleted. | ||
children.splice(nodeIndex, 1); | ||
// If this was caused by an undo from YJS, there could be dangling text. | ||
const danglingText = spliceString( | ||
node._text, | ||
offset, | ||
delCount - 1, | ||
'', | ||
); | ||
if (danglingText.length > 0) { | ||
if (prevCollabNode instanceof CollabTextNode) { | ||
// Merge the text node with previous. | ||
prevCollabNode._text += danglingText; | ||
} else { | ||
// No previous text node to merge into, just delete the text. | ||
this._xmlText.delete(offset, danglingText.length); | ||
} | ||
} | ||
} else { | ||
node._text = spliceString(node._text, offset, delCount, ''); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fails on
main
with:The text is
ord
because neither of the twoif
checks pass, so Lexical ends up innode._text = spliceString(node._text, offset, delCount, '');
.delCount
is the length of the string from the left user, plus 1 for the deletedYMap
, resulting in theW
also being removed.