From 166cd96f85943b51511228bd3d3cbca6c07968f5 Mon Sep 17 00:00:00 2001 From: Gerard Rovira Date: Fri, 30 Oct 2026 20:07:13 -0700 Subject: [PATCH] [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 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 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! ``` --- .../lexical-website/docs/collaboration/y.md | 20 +++++++++++-------- packages/lexical-y/DECISIONS.md | 2 +- packages/lexical-y/src/internal/Inline.ts | 6 ++++-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/lexical-website/docs/collaboration/y.md b/packages/lexical-website/docs/collaboration/y.md index 548778ffca3..0c34b3f3d82 100644 --- a/packages/lexical-website/docs/collaboration/y.md +++ b/packages/lexical-website/docs/collaboration/y.md @@ -818,14 +818,18 @@ person's: joined to a changed word through an inline node, with no space between, is written again with it, and an inline node made between two changes is new where it is put. The words kept are the longest run of words the two - texts share in the same order (where a word repeats, which of its - occurrences is kept can differ from what the update meant), wherever - that takes at most 2,048 word edits among the words both texts have, so - that the comparison stays within about 30 ms for a 5,000-word line. - Beyond that (a line made again with most of its words moved), only its - common start and end are kept. A word that is not kept is written again: - one the update moved past kept words, and beyond 2,048 edits, every word - between the first and the last change. Prefer + texts share in the same order, wherever that takes at most 2,048 word + 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 + that the comparison stays linear in the line's length. Where a word + 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 + its words moved), only its common start and end are kept. A word that is + not kept is written again: one the update moved past kept words, and + beyond 2,048 edits, every word between the first and the last change. + Prefer these to `setTextContent`, which sets a node's text whole: such a change (and a change the browser makes to the DOM text, as an IME composition does) keeps the start and end its text shares with the node's old text, diff --git a/packages/lexical-y/DECISIONS.md b/packages/lexical-y/DECISIONS.md index 961e0b13c56..c764ffcc48d 100644 --- a/packages/lexical-y/DECISIONS.md +++ b/packages/lexical-y/DECISIONS.md @@ -60,7 +60,7 @@ issues and docs that carry it. | D26 | **Undo favours people taking turns over concurrent races.** Where a fix makes undo correct for sequential edits at the cost of rare races between an undo and a simultaneous edit of the same text, the sequential case wins. | Taking turns is the common case; a race with a simultaneous edit of the same text is rare. Picked on 10-28, confirmed on 10-30. | `DECIDED` | 10-30 | [#9400] | | 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] | | D30 | **Diffed rewrites snap to whole words**; keystrokes and IME composition keep the exact diff. Scripts without spaces treat each character as a boundary. **A diffed insertion or deletion without a caret that the diff would put inside a word goes to the first place in the run of equal text it could be in where both its ends are at word edges** (a word deleted with the separator before it); a caret's placement, and one already at word edges, stay. | Concurrent rewrites otherwise split words. Real word segmentation is a follow-up ([#9467]). Without a caret a diff takes the common start first and put deletions inside words, so a collaborator's concurrent delete of the same word took a letter of the next one ([#9636]); the first edge keeps a word typed after a deleted word apart from the next one, as typing goes (a space, then a word). | `DECIDED` | 10-30 | [#9400], [#9467], [#9636] | | D31 | **A list item's nested list gets an item of its own.** `NodeStructureExtension`'s list rule prevents an item holding a nested list next to other content when it is written; the binding does not repair it afterwards. | That shape lost text in outdents, Backspaces and offline merges. Preventing it at write time follows D20. | `DECIDED` | 10-26 | [#9452], [y.md: Limits][limits] | | D32 | **List merging ignores `start`.** | Existing Lexical behaviour, intended. | `DECIDED` | 10-23 | [#9452] | diff --git a/packages/lexical-y/src/internal/Inline.ts b/packages/lexical-y/src/internal/Inline.ts index 09bc3372c83..7929deab9b3 100644 --- a/packages/lexical-y/src/internal/Inline.ts +++ b/packages/lexical-y/src/internal/Inline.ts @@ -915,8 +915,10 @@ function words(s: string, from: number, to: number): [number, number][] { /** * The most edits the exact diff of a remade line's words looks for, among * the words both texts have (see commonWords). It finds the longest common - * run of words in O((n + m) * EXACT_EDITS) time at most (about 30 ms for a - * 5,000-word line); past it, only the common start and end are kept. + * run of words in O((n + m) * EXACT_EDITS) time at most; past it, only the + * common start and end are kept. Words only added or only removed between + * a start and an end the words both texts have share need no search, and + * count none. */ const EXACT_EDITS = 2048;