Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions packages/lexical-website/docs/collaboration/y.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +822 to +823

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 +822 to +823

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.

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
Comment on lines +825 to +828

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.

Comment on lines +826 to +828

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.

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,
Expand Down
2 changes: 1 addition & 1 deletion packages/lexical-y/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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] |

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".

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.

| 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] |
Expand Down
6 changes: 4 additions & 2 deletions packages/lexical-y/src/internal/Inline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading