Real-time collaboration: Improve collaboration within the same rich text#75703
Conversation
…sition has changed, avoid recursing document
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
152e50a to
86ed959
Compare
| end.type === YSelectionType.BlockSelection | ||
| ) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
I thought this might be an issue, but you can't select half of a paragraph and then a block after that. Extending you selection down to a block at the end causes the entire paragraph to be selected. So it seems you can't half end.type be BlockSelection and start.type not be BlockSelection
There was a problem hiding this comment.
Block-level selections overall are funky, so thank you for checking.
…ext (#75703) * Recalculate absolute position in getPostChangesFromCRDTDoc() and apply with content changes * Add tests for selection movement * Fix types in tests * Only use the latest selection history item when dispatching cursor recalculation * Optimization: Only recalculate cursor when the calculated relative position has changed, avoid recursing document * Remove unused export
|
I just cherry-picked this PR to the wp/7.0 branch to get it included in the next release: 3b5bca0 |
…ext (#75703) * Recalculate absolute position in getPostChangesFromCRDTDoc() and apply with content changes * Add tests for selection movement * Fix types in tests * Only use the latest selection history item when dispatching cursor recalculation * Optimization: Only recalculate cursor when the calculated relative position has changed, avoid recursing document * Remove unused export
CI run: #11059. See #64595. --- I've included a log of the Gutenberg changes with the following command: git log --reverse --format="- %s" 23b566c72e9c4a36219ef5d6e62890f05551f6cb..022d8dd3d461f91b15c1f0410649d3ebb027207f | sed 's|#\([0-9][0-9]*\)|https://github.com/WordPress/gutenberg/pull/\1|g; /github\.com\/WordPress\/gutenberg\/pull/!d' | pbcopy - Pattern Editing: Fix nested patterns/sections (WordPress/gutenberg#75772) - QuickEdit: rename status label and remove extra labels in popup (WordPress/gutenberg#75824) - Fix block editing modes not recomputing when isolated editor value changes (WordPress/gutenberg#75821) - Synced patterns: Fix block editing mode of synced pattern content when nested in an unsynced pattern (WordPress/gutenberg#75818) - Block Support: Fix custom CSS not saved when style schema is not defined (WordPress/gutenberg#75797) - Gallery: Fixes keyboard focus escaping the lightbox overlay when navigating a gallery with Tab/Shift+Tab. (WordPress/gutenberg#75852) - Navigation Overlay Close: Set Close as default text, rather than using a placeholder (WordPress/gutenberg#75692) - RTC: Fix entity save call / initial persistence. (WordPress/gutenberg#75841) - Real-time collaboration: Improve collaboration within the same rich text (WordPress/gutenberg#75703) - Client Side Media: Add device/browser capability detection (WordPress/gutenberg#75863) - Navigation editing: simplify edit/view buttons (WordPress/gutenberg#75819) - Add core/icon block to theme.json schema (WordPress/gutenberg#75813) - Fix error when undoing newly added pattern (WordPress/gutenberg#75850) - Page List Item: Replace RawHTML with dangerouslySetInnerHTML for label and title (WordPress/gutenberg#75890) - REST API: Make filter_wp_unique_filename() static to match core, plus avoid duplicate routes (WordPress/gutenberg#75782) - RichText: useAnchor: Fix TypeError in virtual element (WordPress/gutenberg#75900) - DataViews: Remove menu divider again. (WordPress/gutenberg#75908) - Theme: Add build plugins to inject design token fallbacks (WordPress/gutenberg#75901) - Theme: Remove global stylesheet (WordPress/gutenberg#75879) - Real-time collaboration: Remove ghost awareness state explicitly when refreshing (WordPress/gutenberg#75883) - Real-time collaboration: Expand mergeCrdtBlocks() automated testing (WordPress/gutenberg#75923) - Fix client-side media file naming (WordPress/gutenberg#75817) - Add: Connectors screen (WordPress/gutenberg#75833) - Merge document meta into state map (WordPress/gutenberg#75830) - Move WordPress meta key from sync package to core-data (WordPress/gutenberg#75846) - Bugfix: Fix casing of getPersistedCRDTDoc (WordPress/gutenberg#75922) - Add debug logging to SyncManager (WordPress/gutenberg#75924) - DataForm: fix label colors (WordPress/gutenberg#75730) - DataViews: minimize padding for primary action buttons (WordPress/gutenberg#75721) (WordPress/gutenberg#75947) - Connectors: Add `_ai_` prefix to connector setting names and fix naming inconsistencies (WordPress/gutenberg#75948) - Connectors: Unhook Core callbacks in Gutenberg coexistence (WordPress/gutenberg#75935) - Unsynced patterns: Rename 'Disconnect pattern' to 'Detach pattern' in context menu (WordPress/gutenberg#75807) - Editor: Remove View dropdown and pinned items from revisions header (WordPress/gutenberg#75951) - Fix: Template revisions infinite spinner (WordPress/gutenberg#75953) - Backport: Avoid flickering while refreshing (WordPress/gutenberg#74572) (WordPress/gutenberg#75952) - Add wp_ prefix to real time collaberation option. (WordPress/gutenberg#75837) git-svn-id: https://develop.svn.wordpress.org/trunk@61750 602fd350-edb4-49c9-b593-d223f7449a82
CI run: WordPress/wordpress-develop#11059. See #64595. --- I've included a log of the Gutenberg changes with the following command: git log --reverse --format="- %s" 23b566c72e9c4a36219ef5d6e62890f05551f6cb..022d8dd3d461f91b15c1f0410649d3ebb027207f | sed 's|#\([0-9][0-9]*\)|https://github.com/WordPress/gutenberg/pull/\1|g; /github\.com\/WordPress\/gutenberg\/pull/!d' | pbcopy - Pattern Editing: Fix nested patterns/sections (WordPress/gutenberg#75772) - QuickEdit: rename status label and remove extra labels in popup (WordPress/gutenberg#75824) - Fix block editing modes not recomputing when isolated editor value changes (WordPress/gutenberg#75821) - Synced patterns: Fix block editing mode of synced pattern content when nested in an unsynced pattern (WordPress/gutenberg#75818) - Block Support: Fix custom CSS not saved when style schema is not defined (WordPress/gutenberg#75797) - Gallery: Fixes keyboard focus escaping the lightbox overlay when navigating a gallery with Tab/Shift+Tab. (WordPress/gutenberg#75852) - Navigation Overlay Close: Set Close as default text, rather than using a placeholder (WordPress/gutenberg#75692) - RTC: Fix entity save call / initial persistence. (WordPress/gutenberg#75841) - Real-time collaboration: Improve collaboration within the same rich text (WordPress/gutenberg#75703) - Client Side Media: Add device/browser capability detection (WordPress/gutenberg#75863) - Navigation editing: simplify edit/view buttons (WordPress/gutenberg#75819) - Add core/icon block to theme.json schema (WordPress/gutenberg#75813) - Fix error when undoing newly added pattern (WordPress/gutenberg#75850) - Page List Item: Replace RawHTML with dangerouslySetInnerHTML for label and title (WordPress/gutenberg#75890) - REST API: Make filter_wp_unique_filename() static to match core, plus avoid duplicate routes (WordPress/gutenberg#75782) - RichText: useAnchor: Fix TypeError in virtual element (WordPress/gutenberg#75900) - DataViews: Remove menu divider again. (WordPress/gutenberg#75908) - Theme: Add build plugins to inject design token fallbacks (WordPress/gutenberg#75901) - Theme: Remove global stylesheet (WordPress/gutenberg#75879) - Real-time collaboration: Remove ghost awareness state explicitly when refreshing (WordPress/gutenberg#75883) - Real-time collaboration: Expand mergeCrdtBlocks() automated testing (WordPress/gutenberg#75923) - Fix client-side media file naming (WordPress/gutenberg#75817) - Add: Connectors screen (WordPress/gutenberg#75833) - Merge document meta into state map (WordPress/gutenberg#75830) - Move WordPress meta key from sync package to core-data (WordPress/gutenberg#75846) - Bugfix: Fix casing of getPersistedCRDTDoc (WordPress/gutenberg#75922) - Add debug logging to SyncManager (WordPress/gutenberg#75924) - DataForm: fix label colors (WordPress/gutenberg#75730) - DataViews: minimize padding for primary action buttons (WordPress/gutenberg#75721) (WordPress/gutenberg#75947) - Connectors: Add `_ai_` prefix to connector setting names and fix naming inconsistencies (WordPress/gutenberg#75948) - Connectors: Unhook Core callbacks in Gutenberg coexistence (WordPress/gutenberg#75935) - Unsynced patterns: Rename 'Disconnect pattern' to 'Detach pattern' in context menu (WordPress/gutenberg#75807) - Editor: Remove View dropdown and pinned items from revisions header (WordPress/gutenberg#75951) - Fix: Template revisions infinite spinner (WordPress/gutenberg#75953) - Backport: Avoid flickering while refreshing (WordPress/gutenberg#74572) (WordPress/gutenberg#75952) - Add wp_ prefix to real time collaberation option. (WordPress/gutenberg#75837) Built from https://develop.svn.wordpress.org/trunk@61750 git-svn-id: http://core.svn.wordpress.org/trunk@61056 1a063a9b-81f0-0310-95a4-ce76da25c4cd
What?
Allow two users to type at the same time within a block.
Fixes #74563, which demonstrates the behavior before this PR:
This PR addresses these issues and allows fluid selection updates with CRDT changes.
Why?
Users expect editing within the same block to work, and cursors to adjust to real-time changes in the same text.
How?
The cursor drift issue happens because the editor stores cursor positions as absolute offsets (e.g. "index
5in the paragraph"), but when a remote peer inserts or deletes text before that position, the offset becomes stale. The cursor stays at index 5 even though the text it was anchored to has shifted.The fix utilizes existing stored relative position data and updates selection to match when text changes occur.
When a user interacts with the editor, their cursor position is saved to a selection history as a
Y.RelativePosition. These relative positions automatically account for text mutations and the relative position converted back to absolute will return the corrected offset. The main change is ingetPostChangesFromCRDTDoc(), which extracts changes from the CRDT document when remote updates arrive. After extracting content changes, it now also recalculates the local user's cursor by converting their stored relative position back to an absolute offset, and includes this recalculated selection in the returned changes.Because the recalculated selection is bundled into the same
EDIT_ENTITY_RECORDdispatch as the content changes, both arrive in the store atomically.Limitations
This strategy of recalculating absolute position doesn't work in two scenarios:
Limitation 1: Current block is split before the cursor
Screen.Recording.2026-02-23.at.1.13.22.PM.mov
User 1 has their cursor near the front of the block, and user 2 is typing later in the block. When user 1 hits enter and the block breaks into two, user 2's cursor stays within the same block. Ideally, the cursor should stay with the same logical text and move into the second paragraph.
This is tricky to solve. When the block is broken, a new
Y.Textobject is created for the new block's content which is now a separate data structure from the block the user was typing in. The anchor text is gone, so the absolute position just resolves to the end of the first block.Limitation 2: Current block is merged into another block
Screen.Recording.2026-02-23.at.1.14.55.PM.mov
User 1 has their cursor at the beginning of the block and user 2 is typing later in the block. When user 1 hits backspace and the block is merged into the previous block, user 2's cursor disappears. Ideally, the cursor should stay in the same logical text and move into the previous block with the rest of the content.
Similar to above, this is difficult because the second block's structure is destroyed when the block is merged and the Y.Text in the prior block is changed. Making this work would require understanding where text content has been merged into other blocks and recalculating position.
Both of these are real issues, but will require more intensive tree-watching, changing our
mergeCrdtBlockslogic to be more intelligent about block additions/deletions, or listening for specific Gutenberg signals that a block split or merge has occurred. In any case, I think this PR represents a huge improvement over current behavior and can be merged without addressing these cases yet.Testing Instructions