Skip to content

[lexical-website][lexical-y] Documentation Update: Say what the word edit cap of text made again counts, how repeated words are kept, and drop the 30 ms figure - #9652

Merged
zurfyx merged 1 commit into
mainfrom
zurfyx/remade-text-docs-followup
Oct 31, 2026

Conversation

@zurfyx

@zurfyx zurfyx commented Oct 31, 2026

Copy link
Copy Markdown
Member

Description

Follow-up to #9625's review notes on its docs (y.md "Edits made by code", D29, the EXACT_EDITS comment):

  • The cap of 2,048 word edits does not count words only added or only removed between a start and an end the shared words have in common (no search is needed there), so such a line keeps its words past the cap; the docs said only the start and end are kept past it.
  • "About 30 ms for a 5,000-word line" holds for short words only (5,000 words of 40 characters take longer); the docs now say the comparison stays linear in the line's length.
  • Where a word repeats next to a change, the docs now say which occurrence is kept and where a collaborator's concurrent edit of the old one lands (aa bb made again as aa aa bb keeps the first aa).

No code change.

Test plan

Before

The past-cap claim, against the code (all 33 shared words are kept although the shared words take 2,049 edits, all added between a shared start and end):

$ vitest --run --project unit <scratch check: keptStretches on that line, expecting 0 kept words>
AssertionError: expected 33 to be +0

After

The new wording matches that behavior; the repeated-word example, checked with the test harness (a collaborator types X in the old aa while aa bb is made again as aa aa bb, both id orders):

TIE [ 1, 2 ] aXa aa bb
TIE [ 2, 1 ] aXa aa bb

$ prettier --check packages/lexical-website/docs/collaboration/y.md packages/lexical-y/DECISIONS.md packages/lexical-y/src/internal/Inline.ts
All matched files use Prettier code style!

@vercel

vercel Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
lexical Ready Ready Preview Oct 31, 2026 3:38am UTC
lexical-playground Ready Ready Preview Oct 31, 2026 3:38am UTC

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 31, 2026
@meta-code-review

meta-code-review Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 31, 2026, 3:45 AM UTC

@pkg-pr-new

pkg-pr-new Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
lexical

npm i https://pkg.pr.new/lexical@9652

@lexical/a11y

npm i https://pkg.pr.new/@lexical/a11y@9652

@lexical/clipboard

npm i https://pkg.pr.new/@lexical/clipboard@9652

@lexical/code

npm i https://pkg.pr.new/@lexical/code@9652

@lexical/code-core

npm i https://pkg.pr.new/@lexical/code-core@9652

@lexical/code-prism

npm i https://pkg.pr.new/@lexical/code-prism@9652

@lexical/code-shiki

npm i https://pkg.pr.new/@lexical/code-shiki@9652

@lexical/compiler

npm i https://pkg.pr.new/@lexical/compiler@9652

@lexical/devtools-core

npm i https://pkg.pr.new/@lexical/devtools-core@9652

@lexical/dragon

npm i https://pkg.pr.new/@lexical/dragon@9652

@lexical/eslint-plugin

npm i https://pkg.pr.new/@lexical/eslint-plugin@9652

@lexical/extension

npm i https://pkg.pr.new/@lexical/extension@9652

@lexical/fast-check

npm i https://pkg.pr.new/@lexical/fast-check@9652

@lexical/file

npm i https://pkg.pr.new/@lexical/file@9652

@lexical/hashtag

npm i https://pkg.pr.new/@lexical/hashtag@9652

@lexical/headless

npm i https://pkg.pr.new/@lexical/headless@9652

@lexical/history

npm i https://pkg.pr.new/@lexical/history@9652

@lexical/html

npm i https://pkg.pr.new/@lexical/html@9652

@lexical/internal

npm i https://pkg.pr.new/@lexical/internal@9652

@lexical/link

npm i https://pkg.pr.new/@lexical/link@9652

@lexical/list

npm i https://pkg.pr.new/@lexical/list@9652

@lexical/mark

npm i https://pkg.pr.new/@lexical/mark@9652

@lexical/markdown

npm i https://pkg.pr.new/@lexical/markdown@9652

@lexical/mdast

npm i https://pkg.pr.new/@lexical/mdast@9652

@lexical/offset

npm i https://pkg.pr.new/@lexical/offset@9652

@lexical/overflow

npm i https://pkg.pr.new/@lexical/overflow@9652

@lexical/plain-text

npm i https://pkg.pr.new/@lexical/plain-text@9652

@lexical/react

npm i https://pkg.pr.new/@lexical/react@9652

@lexical/rich-text

npm i https://pkg.pr.new/@lexical/rich-text@9652

@lexical/selection

npm i https://pkg.pr.new/@lexical/selection@9652

@lexical/table

npm i https://pkg.pr.new/@lexical/table@9652

@lexical/tailwind

npm i https://pkg.pr.new/@lexical/tailwind@9652

@lexical/text

npm i https://pkg.pr.new/@lexical/text@9652

@lexical/utils

npm i https://pkg.pr.new/@lexical/utils@9652

@lexical/y

npm i https://pkg.pr.new/@lexical/y@9652

@lexical/yjs

npm i https://pkg.pr.new/@lexical/yjs@9652

commit: 166cd96

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks, this looks good. The corrected cap wording matches the code: a line whose shared words gain one run of 2,049 repeats between a shared start and end keeps all of them, as the new text says. Replacing the 30 ms figure with the linear bound is also more accurate. The suggestions below are optional and non-blocking, all about wording:

  1. The aa bb example depends on the caret (packages/lexical-website/docs/collaboration/y.md lines 825-828; D29 in packages/lexical-y/DECISIONS.md). Text of nodes made again is anchored at the caret (the stretch comment in TextEdits.ts; diffStrings in Inline.ts lines 503-511). With the caret right after the first aa of the new text (offset 3), the new word is placed before the old one, and a collaborator's concurrent X in the old aa ends up as aa aXa bb on both peers, in both client-id orders. With no caret, or with the caret at 0, 4, 6 or 8, the result is aXa aa bb, as documented. Adding something like "unless the caret is right after the first aa " would cover it.
  2. D29 credits the edit script for this example. For aa bb made again as aa aa bb, keptEnds keeps aa as the common start, and keptStretches returns at Inline.ts:1003 before commonWords runs. t1z aa bb t2z made again as n1z aa aa bb n2z is decided by the edit script and keeps the first aa at any caret, so it would illustrate both D29 and y.md.
  3. "Only added, or only removed" exempts more than the code does (y.md lines 822-823, the EXACT_EDITS comment at Inline.ts:828-830, D29). exactWords strips the shared words' common start and end once and then counts every added or removed word against the cap, so only one run in one place is free. For example, w0z w1z w2z made again with 1,100 repeats of w2z before w1z and 1,100 of w0z after it (nothing removed) keeps none of the three shared words, while 1,000 in each place keeps all three. Wording like "(one run of words added, or removed, in one place between the start and end those words share takes none)" would match the code.

Minor: the test plan's Before command is a placeholder for a scratch check. AGENTS.md's Test plan section asks for the command that was actually run.

Comment on lines +822 to +823
edits among the words both texts have (words only added, or only
removed, between a start and an end those words share take none), so

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: this reads as exempting any line whose shared words were only added or only removed, but exactWords strips the shared words' common start and end once and then counts every added or removed word against the cap (Inline.ts:916-929, :858). Only one run in one place is free: w0z w1z w2z made again with 1,100 repeats of w2z before w1z and 1,100 of w0z after it keeps none of the three shared words; 1,000 in each place keeps all three. Maybe "(one run of words added, or removed, in one place between the start and end those words share takes none)", here, in the EXACT_EDITS comment and in D29.

Comment on lines +825 to +828
repeats next to a change, which of its occurrences is kept can differ
from what the update meant: `aa bb` made again as `aa aa bb` keeps the
first `aa`, so a collaborator's concurrent edit of the old `aa` lands on
the first of the two. Beyond 2,048 edits (a line made again with most of

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: this holds with no caret in the made text, but made text is anchored at the caret. With the caret right after the first aa of the new text (offset 3), the new word goes before the old one, and a collaborator's concurrent X in the old aa ends up as aa aXa bb on both peers, in both client-id orders. With no caret, or at 0, 4, 6 or 8, the result is aXa aa bb. Consider "unless the caret is right after the first aa ", or an example the word edit script decides at any caret, such as t1z aa bb t2z made again as n1z aa aa bb n2z.

| D27 | **Moves are a delete and a re-insert** at launch; the binding rewrites the cheaper side. Real moves within a container ([#9449], records after Yjs's unmerged `ContentMove` design) are paused and land last, backward compatible: a new layout version that older clients open read only. Move records leave tombstones, as every Yjs binding does. Moves across containers stay copies. | Every Yjs binding turns moves into delete plus insert; Yjs dropped its own move for complexity. Reliability of the rest came first. | `DECIDED` | 10-25 | [#9449], [#9455], [y.md: Limits][limits] |
| D28 | **Text identity is fixed at the source.** While a binding is attached, core records text edits (`spliceText`, insert and remove, split and merge), and text entity plugins record a grow as a move and a remake as a retype. Text only `setTextContent` or the DOM changed is diffed. The binding adds no per-case heuristics. | No binding can recover intent an API dropped; special-case rules were fragile and regressed neighbouring cases. | `DECIDED` | 10-27 | [#9483], [y.md: Edits made by code][y-code] |
| D29 | **Remade text keeps its common start and end by character**, whatever the node boundaries (y-prosemirror's `updateYText` rule), **and between two changes every run of unchanged whole words** (the longest common run of words, by a linear-space shortest edit script over the words both texts have, up to 2,048 edits, about 30 ms for a 5,000-word line; past it, only the start and end; a word the update moved is written again; a word changed only in part is written whole, so a collaborator's undone delete of its unchanged part shows twice, [#9635]), and never brings back a collaborator's delete. It replaces the word matching of [#9565]. | It is the industry-standard behaviour, and word matching glued words together and merged numbers oddly. Keeping only the start and end wrote everything between two changed words again, so a collaborator's delete there came back, and showed twice after the collaborator's undo ([#9621]). | `DECIDED` | 10-30 | [#9565], [#9598], [#9621] |
| D29 | **Remade text keeps its common start and end by character**, whatever the node boundaries (y-prosemirror's `updateYText` rule), **and between two changes every run of unchanged whole words** (the longest common run of words, by a linear-space shortest edit script over the words both texts have, up to 2,048 edits, words only added or only removed between a shared start and end taking none, linear in the line's length; past it, only the start and end; where a word repeats next to a change, which occurrence is kept follows that edit script (`aa bb` made again as `aa aa bb` keeps the first `aa`); a word the update moved is written again; a word changed only in part is written whole, so a collaborator's undone delete of its unchanged part shows twice, [#9635]), and never brings back a collaborator's delete. It replaces the word matching of [#9565]. | It is the industry-standard behaviour, and word matching glued words together and merged numbers oddly. Keeping only the start and end wrote everything between two changed words again, so a collaborator's delete there came back, and showed twice after the collaborator's undo ([#9621]). | `DECIDED` | 10-30 | [#9565], [#9598], [#9621] |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: for aa bb made again as aa aa bb, the first aa is kept as the common start by character (keptEnds); keptStretches returns at Inline.ts:1003 before the edit script runs, which is also why a caret at offset 3 keeps the second one instead. t1z aa bb t2z made again as n1z aa aa bb n2z is decided by the edit script and keeps the first aa at any caret, so it would fit "follows that edit script".

…edit cap of text made again counts, how repeated words are kept, and drop the 30 ms figure

Follow-up to #9625's review notes on its docs (y.md "Edits made by code", D29, the `EXACT_EDITS` comment):

- The cap of 2,048 word edits does not count words only added or only removed between a start and an end the shared words have in common (no search is needed there), so such a line keeps its words past the cap; the docs said only the start and end are kept past it.
- "About 30 ms for a 5,000-word line" holds for short words only (5,000 words of 40 characters take longer); the docs now say the comparison stays linear in the line's length.
- Where a word repeats next to a change, the docs now say which occurrence is kept and where a collaborator's concurrent edit of the old one lands (`aa bb` made again as `aa aa bb` keeps the first `aa`).

No code change.

The past-cap claim, against the code (all 33 shared words are kept although the shared words take 2,049 edits, all added between a shared start and end):

```
$ vitest --run --project unit <scratch check: keptStretches on that line, expecting 0 kept words>
AssertionError: expected 33 to be +0
```

The new wording matches that behavior; the repeated-word example, checked with the test harness (a collaborator types `X` in the old `aa` while `aa bb` is made again as `aa aa bb`, both id orders):

```
TIE [ 1, 2 ] aXa aa bb
TIE [ 2, 1 ] aXa aa bb

$ prettier --check packages/lexical-website/docs/collaboration/y.md packages/lexical-y/DECISIONS.md packages/lexical-y/src/internal/Inline.ts
All matched files use Prettier code style!
```

@meta-code-review meta-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks, this looks good. The corrected cap wording matches the code: a line whose shared words gain one run of 2,049 repeats between a shared start and end keeps all of them, as the new text says, and replacing the 30 ms figure with the linear bound is more accurate. The suggestions below are optional and non-blocking, all about wording:

  1. The aa bb example depends on the caret (packages/lexical-website/docs/collaboration/y.md lines 826-828; D29 in packages/lexical-y/DECISIONS.md). Text of nodes made again is anchored at the caret (the stretch comment in TextEdits.ts; diffStrings in Inline.ts lines 513-521). With the caret right after the first aa of the new text (offset 3), the new word is placed before the old one, and a collaborator's concurrent X in the old aa ends up as aa aXa bb on both peers, in both client-id orders. With no caret, or with the caret at any other offset, the result is aXa aa bb, as documented. Adding something like "unless the caret is right after the first aa " would cover it.
  2. D29 credits the edit script for this example. For aa bb made again as aa aa bb, keptEnds keeps aa as the common start, and keptStretches returns at Inline.ts:1094 before commonWords runs. t1z aa bb t2z made again as n1z aa aa bb n2z is decided by the edit script and keeps the first aa at any caret, so it would illustrate both D29 and y.md.
  3. "Only added, or only removed" exempts more than the code does (y.md lines 822-823, the EXACT_EDITS comment at Inline.ts:919-921, D29). exactWords strips the shared words' common start and end once and then counts every added or removed word against the cap, so only one run in one place is free. For example, 33 shared words with 1,100 repeats added before the 17th and 1,100 after it (nothing removed) keep none of them, while 1,000 in each place keep all 33. Wording like "(one run of words added, or removed, in one place between the start and end those words share takes none)" would match the code.
  4. Limits still has the old cap wording (y.md lines 1033-1035): it says everything between the first and the last change is written again where the shared words need more than 2,048 edits, with no exemption. The same qualifier there would keep the two paragraphs consistent.

Minor: the test plan's Before command is a placeholder for a scratch check. The Commits and Pull Requests section of AGENTS.md asks the Test plan to carry the command that was actually run and its output.

Comment on lines +826 to +828
from what the update meant: `aa bb` made again as `aa aa bb` keeps the
first `aa`, so a collaborator's concurrent edit of the old `aa` lands on
the first of the two. Beyond 2,048 edits (a line made again with most of

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: this holds without a caret, but text of nodes made again is anchored at the caret. With the caret right after the first aa (offset 3 of the new text), a collaborator's concurrent X in the old aa ends up as aa aXa bb on both peers. Consider "unless the caret is right after the first aa ", or t1z aa bb t2z made again as n1z aa aa bb n2z, which keeps the first aa at any caret.

Comment on lines +822 to +823
edits among the words both texts have (words only added, or only
removed, between a start and an end those words share take none), so

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: exactWords strips the shared words' common start and end once, so only one run in one place is free. 1,100 repeats added in each of two places (nothing removed) keep none of 33 shared words. Something like "(one run of words added, or removed, in one place between the start and end those words share takes none)" would match the code.

| D27 | **Moves are a delete and a re-insert** at launch; the binding rewrites the cheaper side. Real moves within a container ([#9449], records after Yjs's unmerged `ContentMove` design) are paused and land last, backward compatible: a new layout version that older clients open read only. Move records leave tombstones, as every Yjs binding does. Moves across containers stay copies. | Every Yjs binding turns moves into delete plus insert; Yjs dropped its own move for complexity. Reliability of the rest came first. | `DECIDED` | 10-25 | [#9449], [#9455], [y.md: Limits][limits] |
| D28 | **Text identity is fixed at the source.** While a binding is attached, core records text edits (`spliceText`, insert and remove, split and merge), and text entity plugins record a grow as a move and a remake as a retype. Text only `setTextContent` or the DOM changed is diffed. The binding adds no per-case heuristics. | No binding can recover intent an API dropped; special-case rules were fragile and regressed neighbouring cases. | `DECIDED` | 10-27 | [#9483], [y.md: Edits made by code][y-code] |
| D29 | **Remade text keeps its common start and end by character**, whatever the node boundaries (y-prosemirror's `updateYText` rule), **and between two changes every run of unchanged whole words** (the longest common run of words, by a linear-space shortest edit script over the words both texts have, up to 2,048 edits, about 30 ms for a 5,000-word line; past it, only the start and end; a word the update moved is written again; a word changed only in part is written whole, so a collaborator's undone delete of its unchanged part shows twice, [#9635]), and never brings back a collaborator's delete. It replaces the word matching of [#9565]. | It is the industry-standard behaviour, and word matching glued words together and merged numbers oddly. Keeping only the start and end wrote everything between two changed words again, so a collaborator's delete there came back, and showed twice after the collaborator's undo ([#9621]). | `DECIDED` | 10-30 | [#9565], [#9598], [#9621] |
| D29 | **Remade text keeps its common start and end by character**, whatever the node boundaries (y-prosemirror's `updateYText` rule), **and between two changes every run of unchanged whole words** (the longest common run of words, by a linear-space shortest edit script over the words both texts have, up to 2,048 edits, words only added or only removed between a shared start and end taking none, linear in the line's length; past it, only the start and end; where a word repeats next to a change, which occurrence is kept follows that edit script (`aa bb` made again as `aa aa bb` keeps the first `aa`); a word the update moved is written again; a word changed only in part is written whole, so a collaborator's undone delete of its unchanged part shows twice, [#9635]), and never brings back a collaborator's delete. It replaces the word matching of [#9565]. | It is the industry-standard behaviour, and word matching glued words together and merged numbers oddly. Keeping only the start and end wrote everything between two changed words again, so a collaborator's delete there came back, and showed twice after the collaborator's undo ([#9621]). | `DECIDED` | 10-30 | [#9565], [#9598], [#9621] |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional: for aa bb made again as aa aa bb, keptEnds keeps aa as the common start and keptStretches returns before commonWords runs, so the edit script does not decide this example (and the caret can move it). t1z aa bb t2z made again as n1z aa aa bb n2z is decided by the edit script.

@zurfyx
zurfyx added this pull request to the merge queue Oct 31, 2026
Merged via the queue into main with commit c363dd9 Oct 31, 2026
60 of 63 checks passed
zurfyx added a commit that referenced this pull request Oct 31, 2026
## Description

Merges main at c0c7e42 (#9640, #9600, #9652, #9659, #9648 and #9657). The one conflict is in the link list of `packages/lexical-y/DECISIONS.md`, where both sides added a reference (#9597 here, #9606 on main); both are kept.

## Test plan

### Before

The branch was on main f59f74d.

### After

`pnpm run ci-check` and `pnpm exec vitest run --project unit --project scripts-unit` pass on the merged tree (outputs in the pull request).

This branch was successfully deployed

2 active deployments
Preview – lexical — 166cd96f Deployed Oct 31, 2026 by vercel[bot]
Preview – lexical-playground — 166cd96f Deployed Oct 31, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant