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
7 changes: 7 additions & 0 deletions .changeset/lexical-y-set-text-word-edges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@lexical/y': patch
---

[fix] Place an insertion or a deletion found in text set without a caret at a word edge, so a collaborator's concurrent edit of the same word does not take a letter of the next one

Verify: `setTextContent` (or a node's text set whole) that deletes or inserts whole words, with no caret in the node, while a collaborator deletes or edits the same words: `t1z t2z t3z` set to `t1z t3z` while a collaborator deletes `t2z` shows `t1z t3z` on every client, not `t1z 3z`.
13 changes: 12 additions & 1 deletion packages/lexical-website/docs/collaboration/y.md
Original file line number Diff line number Diff line change
Expand Up @@ -830,7 +830,18 @@ person's:
(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,
anchored at the caret, which cannot tell where an edit next to equal text
was made without one; a replacement found that way replaces whole words
was made without one. Without a caret, an insertion or a deletion found
that way stays where the diff puts it if both its ends are at word edges;
where that is inside a word, it goes to the first place where both are,
so it changes no word it does not add or remove (`t1z t2z t3z` set to
`t1z t3z` deletes ` t2z`, not `2z t`, and a collaborator's concurrent
delete of `t2z` leaves `t1z t3z`). Text a collaborator types right
against a word deleted that way at the same time, on the side of the
separator deleted with it (a space, a tab, a comma, an emoji), ends up
glued to the word beyond, as it does without this placement on the other
side (`q9z` typed before `t2z` shows as `t1zq9z`; on a line's first
word, which has no separator before it, text typed after it). A
replacement found that way replaces whole words
(what a collaborator types inside the replaced words at the same time
lands in one piece at the start of the new text, and the letters the new
text shares are written again, so a collaborator's formatting of them at
Expand Down
3 changes: 2 additions & 1 deletion packages/lexical-y/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ issues and docs that carry it.
| 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] |
| D30 | **Diffed rewrites snap to whole words**; keystrokes and IME composition keep the exact diff. Scripts without spaces treat each character as a boundary. | Concurrent rewrites otherwise split words. Real word segmentation is a follow-up ([#9467]). | `DECIDED` | 10-26 | [#9400], [#9467] |
| 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] |
| D70 | **No virtual structure.** The editor does not show repairs that are not stored (padded cells for a short table row, two stored lists drawn as one). The one existing case stays: text left without a line marker shows as a paragraph. | It adds another structure on top of Yjs and the Lexical tree, and the complexity that comes with it. | `DECIDED` | 10-25 | [#9400] |
Expand Down Expand Up @@ -201,3 +201,4 @@ issues and docs that carry it.
[#9610]: https://github.com/meta/lexical/issues/9610
[#9621]: https://github.com/meta/lexical/issues/9621
[#9635]: https://github.com/meta/lexical/issues/9635
[#9636]: https://github.com/meta/lexical/issues/9636
Loading
Loading