Skip to content

Real-time collaboration: Improve collaboration within the same rich text#75703

Merged
alecgeatches merged 6 commits intoWordPress:trunkfrom
Automattic:add/rtc-multi-user-rich-text-typing
Feb 24, 2026
Merged

Real-time collaboration: Improve collaboration within the same rich text#75703
alecgeatches merged 6 commits intoWordPress:trunkfrom
Automattic:add/rtc-multi-user-rich-text-typing

Conversation

@alecgeatches
Copy link
Contributor

@alecgeatches alecgeatches commented Feb 18, 2026

What?

Allow two users to type at the same time within a block.

simultaneous-typing

Fixes #74563, which demonstrates the behavior before this PR:

When two users have selections with the same Y.Text (e.g. core/paragraph text), other users' editors do not update selection position when it has changed.

Here are some examples of that behavior:

1. Stationary cursor "moves backward"

Image

Above, my user has a cursor set at the beginning of "second". When another user types earlierin the paragraph, my selection stays in the same location. Because the text is being added before my selection position, my cursor "travels backwards" within the paragraph.

2. Selection range moves

Similarly, if a range is selected, this also stays stationary during text changes:

Image

This is the same problem as above. Because the selectionStart/selectionEnd properties don't adjust to the changes in the underlying text, the selection can move backwords within the logical text.

3. Unexpected selection changes in rich text with line breaks

This problem persists when there are line breaks in a shared rich text object:

Image

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 5 in 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 in getPostChangesFromCRDTDoc(), 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_RECORD dispatch 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.Text object 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 mergeCrdtBlocks logic 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

  1. Check out this PR. Ensure real-time collaboration is enabled via: WordPress Admin -> Settings -> Writing, and check the "Enable real-time collaboration" checkbox. Click the "Save Changes" button.
  2. Open a post in two separate tabs. Ideally, this can be done on a shared site so that another real human can test at the same time.
  3. Within the same block, have one user at the beginning of the block start typing.
  4. Have a second user start typing later in the block.
  5. Verify that both user's text appears in the block, and that the cursor position moves intuitively with the content.
@alecgeatches alecgeatches self-assigned this Feb 18, 2026
@alecgeatches alecgeatches added [Type] Bug An existing feature does not function as intended [Feature] Real-time Collaboration Phase 3 of the Gutenberg roadmap around real-time collaboration labels Feb 18, 2026
@alecgeatches alecgeatches changed the title Add/rtc multi user rich text typing Feb 18, 2026
@github-actions github-actions bot added the [Package] Core data /packages/core-data label Feb 18, 2026
@alecgeatches alecgeatches changed the title Real-time collaboration: Fix collaboration within the same rich text Feb 23, 2026
@alecgeatches alecgeatches marked this pull request as ready for review February 23, 2026 19:52
@alecgeatches alecgeatches requested a review from nerrad as a code owner February 23, 2026 19:52
@github-actions
Copy link

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: alecgeatches <alecgeatches@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions github-actions bot added the [Package] Editor /packages/editor label Feb 23, 2026
@alecgeatches alecgeatches force-pushed the add/rtc-multi-user-rich-text-typing branch 2 times, most recently from 152e50a to 86ed959 Compare February 23, 2026 20:50
@github-actions github-actions bot removed the [Package] Editor /packages/editor label Feb 23, 2026
end.type === YSelectionType.BlockSelection
) {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Block-level selections overall are funky, so thank you for checking.

@chriszarate chriszarate added the Backport to WP 7.0 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta label Feb 24, 2026
@alecgeatches alecgeatches merged commit 0180c41 into WordPress:trunk Feb 24, 2026
47 of 48 checks passed
@alecgeatches alecgeatches deleted the add/rtc-multi-user-rich-text-typing branch February 24, 2026 17:19
@github-actions github-actions bot added this to the Gutenberg 22.7 milestone Feb 24, 2026
gutenbergplugin pushed a commit that referenced this pull request Feb 24, 2026
…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
@github-actions github-actions bot added Backported to WP Core Pull request that has been successfully merged into WP Core and removed Backport to WP 7.0 Beta/RC Pull request that needs to be backported to the WordPress major release that's currently in beta labels Feb 24, 2026
@github-actions
Copy link

I just cherry-picked this PR to the wp/7.0 branch to get it included in the next release: 3b5bca0

adamsilverstein pushed a commit that referenced this pull request Feb 25, 2026
…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
pento pushed a commit to WordPress/wordpress-develop that referenced this pull request Feb 26, 2026
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
markjaquith pushed a commit to markjaquith/WordPress that referenced this pull request Feb 26, 2026
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backported to WP Core Pull request that has been successfully merged into WP Core [Feature] Real-time Collaboration Phase 3 of the Gutenberg roadmap around real-time collaboration [Package] Core data /packages/core-data [Type] Bug An existing feature does not function as intended

3 participants