Skip to content
Draft
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/y-undo-keeps-last-run-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@lexical/y': patch
---

[fix] An undo of typing inside a comment or a format keeps the value ending where it did when a collaborator deleted the rest of the run, instead of letting it cover everything after it

Verify: comment or format a few words, type plain text inside them, let a collaborator delete the text after yours up to past the run's end, then undo your typing: the value ends where the deleted text began on every client.
2 changes: 2 additions & 0 deletions packages/lexical-y/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ issues and docs that carry it.
| D24 | **Yjs's format cleanup is off** in `@lexical/y` containers and on every relay that loads a state (`createYDoc`). A delete keeps the ends of each deleted formatted run, within a bound per format key. Not reported upstream. | The cleanup deletes format items a concurrent writer may still rely on. Upstream already knows that skipping it is the app's trade-off. | `DECIDED` | 10-28 | [#9400], [y.md: Servers][y-servers] |
| D25 | **Undo is Yjs's `UndoManager`**, scoped to the binding's root and this client's own edits (500 ms capture by default). It never deletes a container a collaborator has put content in (its `deleteFilter`). Do not combine it with `HistoryExtension`. | Collaborative undo must not revert other people's work; keeping a container whole once someone else wrote in it matches y-prosemirror's protected nodes and is cheap to maintain. The `UndoManager` was chosen on 10-23; keeping containers whole was confirmed on 10-30. | `DECIDED` | 10-30 | [#9400], [y.md: Limits][limits] |
| 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] |
| D84 | **An undo keeps a null that is the last end of a run still open before it.** Where an undo or redo would delete a null of its change (the null by which typed text was made plain inside a run of a value) and that null is the run's only end left (no later live item of the key ends the run, and content shows after the null), the null is kept, and the run ends there. | A collaborator's delete can take the run's own end and the restore the typing wrote after its text; deleting the null then left the value open over everything after it, on every client ([#9624]). It follows D26 (taking turns) and writes nothing: it keeps an item. Cost: one pass over each container that holds a null the undo would delete. Without the delete's collapse of format items (D24) the run's end stays and this does not arise. | `DECIDED` | 10-30 | [#9624] |

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: D84 settles part of the OPEN D74 (keeping the run ends of text an undo removes from inside a format run). packages/lexical-y/AGENTS.md asks a PR that changes a recorded decision to update its row, so a cross-link between D84 and D74 would help.

| 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 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. | `DECIDED` | 10-30 | [#9565] |
Expand Down Expand Up @@ -198,3 +199,4 @@ issues and docs that carry it.
[#9608]: https://github.com/meta/lexical/issues/9608
[#9609]: https://github.com/meta/lexical/issues/9609
[#9610]: https://github.com/meta/lexical/issues/9610
[#9624]: https://github.com/meta/lexical/issues/9624
Loading
Loading