Repository navigation
Conversation
… own text when a collaborator's text comes in before it
## Description
While a client composes (IME), the reader keeps the node being composed with its key and its text, so the browser goes on composing in its DOM text node. It found that text by string: the first occurrence of the node's old text in the line. When a collaborator typed at the same place at the same time as the composer's own text, and their text is ordered before it, the line reads, for example, `t12dz t18ez t20ez t` where the composed node was `t12dz t18ez t` (the composer's own ` t` at its end). The first occurrence of `t12dz t18ez t` ends in the collaborator's ` t`, so the composed node stood for the collaborator's characters, and the next character the input method wrote at its offset in the node landed inside the collaborator's word: `t12dz t18ez t320ez t`. Nothing was lost or duplicated, but a word was cut. The composition's change is normally held back until it ends; here the collaborator's change also deleted other text of the line, and a change that deletes shown text is read whole.
The reader already knows the composition's character by identity (`composedAt`, the character before the caret). It now places the node around that character: as many characters before and after it as the node had, so the browser's offset in the node's DOM text stays right after the composer's own text. Another occurrence of the node's text elsewhere holds other characters and is not the node. Without a known character (no caret in the node) the node is found by its text as before. One case keeps the offset off by one: where the node's start would fall between the two halves of an astral character a collaborator put there, the node starts before the character (a node never splits a surrogate pair), so the browser's offset is one unit before the composer's character; main misplaces it there too. This also replaces the previous fallback for a node whose text is no longer there whole (a collaborator deleted part of it), which kept its key for the whole run of text holding the character and so moved the browser's offset; the node keeps the text around the character instead.
## Changeset
`.changeset/lexical-y-composition-offset.md` (`@lexical/y`, `[fix]`).
## Test plan
New `CompositionInterleave.test.ts`: A types ` t` at the end of a line and composes after it while B types ` t20ez` at the same place and deletes the line's bold word in the same update; after the input method writes `3` at its offset, both words are whole and the peers converge (both client id orders; the order where B's text comes first failed).
```
pnpm exec vitest run --project unit --reporter=verbose packages/lexical-y/src/__tests__/unit/CompositionInterleave.test.ts
```
### Before
```
✓ |unit| packages/lexical-y/src/__tests__/unit/CompositionInterleave.test.ts > @lexical/y a composition at text a collaborator typed next to at the same time > the composed text stays after the composer's own text (ids 1,2)
× |unit| packages/lexical-y/src/__tests__/unit/CompositionInterleave.test.ts > @lexical/y a composition at text a collaborator typed next to at the same time > the composed text stays after the composer's own text (ids 2,1)
⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
AssertionError: expected [ 't12dz t18ez t20ez t3', …(1) ] to include 't12dz t18ez t320ez t'
Test Files 1 failed (1)
Tests 1 failed | 1 passed (2)
```
### After
```
✓ |unit| packages/lexical-y/src/__tests__/unit/CompositionInterleave.test.ts > @lexical/y a composition at text a collaborator typed next to at the same time > the composed text stays after the composer's own text (ids 1,2)
✓ |unit| packages/lexical-y/src/__tests__/unit/CompositionInterleave.test.ts > @lexical/y a composition at text a collaborator typed next to at the same time > the composed text stays after the composer's own text (ids 2,1)
Test Files 1 passed (1)
Tests 2 passed (2)
```
`@lexical/y` unit suite (including `Composition`, `CompositionHoldBack` and `CompositionHoldBackBlockIds`): 7,486 passed, 46 expected fail, 0 failed. A randomized two-to-twelve-client session harness with compositions, schema versions and agents (12,000 sessions, same seeds with and without this change): the same flag counts on both, no crash. `pnpm run ci-check` passes.
zurfyx
requested review from
acywatson,
etrepum,
ivailop7 and
potatowagon
as code owners
October 29, 2026 23:56
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
lexical
@lexical/a11y
@lexical/clipboard
@lexical/code
@lexical/code-core
@lexical/code-prism
@lexical/code-shiki
@lexical/compiler
@lexical/devtools-core
@lexical/dragon
@lexical/eslint-plugin
@lexical/extension
@lexical/fast-check
@lexical/file
@lexical/hashtag
@lexical/headless
@lexical/history
@lexical/html
@lexical/internal
@lexical/link
@lexical/list
@lexical/mark
@lexical/markdown
@lexical/mdast
@lexical/offset
@lexical/overflow
@lexical/plain-text
@lexical/react
@lexical/rich-text
@lexical/selection
@lexical/table
@lexical/tailwind
@lexical/text
@lexical/utils
@lexical/y
@lexical/yjs
commit: |
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
While a client composes (IME), the reader keeps the node being composed with its key and its text, so the browser goes on composing in its DOM text node. It found that text by string: the first occurrence of the node's old text in the line. When a collaborator typed at the same place at the same time as the composer's own text, and their text is ordered before it, the line reads, for example,
t12dz t18ez t20ez twhere the composed node wast12dz t18ez t(the composer's owntat its end). The first occurrence oft12dz t18ez tends in the collaborator'st, so the composed node stood for the collaborator's characters, and the next character the input method wrote at its offset in the node landed inside the collaborator's word:t12dz t18ez t320ez t. Nothing was lost or duplicated, but a word was cut. The composition's change is normally held back until it ends; here the collaborator's change also deleted other text of the line, and a change that deletes shown text is read whole.The reader already knows the composition's character by identity (
composedAt, the character before the caret). It now places the node around that character: as many characters before and after it as the node had, so the browser's offset in the node's DOM text stays right after the composer's own text. Another occurrence of the node's text elsewhere holds other characters and is not the node. Without a known character (no caret in the node) the node is found by its text as before. One case keeps the offset off by one: where the node's start would fall between the two halves of an astral character a collaborator put there, the node starts before the character (a node never splits a surrogate pair), so the browser's offset is one unit before the composer's character; main misplaces it there too. This also replaces the previous fallback for a node whose text is no longer there whole (a collaborator deleted part of it), which kept its key for the whole run of text holding the character and so moved the browser's offset; the node keeps the text around the character instead.Changeset
.changeset/lexical-y-composition-offset.md(@lexical/y,[fix]).Test plan
New
CompositionInterleave.test.ts: A typestat the end of a line and composes after it while B typest20ezat the same place and deletes the line's bold word in the same update; after the input method writes3at its offset, both words are whole and the peers converge (both client id orders; the order where B's text comes first failed).Before
After
@lexical/yunit suite (includingComposition,CompositionHoldBackandCompositionHoldBackBlockIds): 7,486 passed, 46 expected fail, 0 failed. A randomized two-to-twelve-client session harness with compositions, schema versions and agents (12,000 sessions, same seeds with and without this change): the same flag counts on both, no crash.pnpm run ci-checkpasses.Known residual (not changed here)
A randomized collaboration soak reached one case of an existing class (a composition in a highlighted code line while remote edits arrive) through this change's different trace: when the composition ends, the composer's editor shows the composed word while the stored line holds its characters in another order; the opt-in self-check reports it. The class is the known composition-in-highlighted-code limit; its overall rate went down on the soak with this change (28 sessions only with it vs 67 only without). Not reduced yet; a follow-up.