Concurrent editing
What people see when they delete, undo, split, merge and move the same content at the same time.
Edytor merges concurrent edits without conflicts, and it follows one principle when two people touch the same content: keep what nobody removed, and never duplicate what both did. This page lists the rules users run into, each with a short Alice and Bob example. “At the same time” means before either has seen the other’s edit, whether because of latency or because one of them was offline.
Undo is your own
Undo and redo revert only your own edits. When Alice deletes a block, Bob edits another one, and Alice undoes, her block comes back and Bob’s edit stays. See History for undo steps in the editor.
Concurrent deletes and undo
Each person’s delete of a piece of text is recorded as theirs. A character is visible only while no one’s delete of it is in effect.
| Alice | Bob | |
|---|---|---|
| 1 | Deletes “ world” from “hello world”. | Deletes “ world” from “hello world”. |
| 2 | Undoes. The text stays deleted: Bob’s delete still holds. | |
| 3 | Undoes. “ world” comes back, once. |
Both see hello after step 2 and hello world after step 3, never hello world world.
- A restoration made before a peer’s concurrent delete arrived is hidden as soon as that delete arrives. It becomes part of the peer’s delete, and the peer’s undo brings it back.
- Text typed and deleted within one undo step leaves no trace; no undo can bring it back.
Deleting a block keeps its children
Deleting a block removes only that block. A block selection holds exactly the blocks selected: clicking a block’s handle selects that block, not its children.
| Alice | Bob | |
|---|---|---|
| 1 | Selects the “Tasks” block by its handle and deletes it. | Types in “Buy milk”, a child of “Tasks”. |
| 2 | “Buy milk” and its sibling take the place of “Tasks”, with their own children. | Bob’s typing is kept in “Buy milk”. |
| 3 | Undoes: “Tasks” returns with both children nested under it. |
The same holds for whatever Bob puts under “Tasks” without having seen the delete: nothing that Alice did not delete is hidden with it.
| Bob, at the same time | Result after both sync |
|---|---|
| Presses Enter in the middle of “Buy milk”, which has children | Both halves of “Buy milk” take the place of “Tasks”, in order; the children stay under the second half, as the split put them. |
| Types in the second half after that Enter | The text is kept. |
| Adds a new child under “Tasks”, or moves a block into it | It takes the place of “Tasks” too, among the children, in the order Bob placed it. |
| Deletes “Buy milk” (keeping its children) | Both deletes hold; the children of “Buy milk” take the place of “Tasks”. |
Alice’s undo puts all of it back under “Tasks”.
To delete a whole subtree from code, pass keepChildren: false. Every block in the subtree is deleted; a block Bob adds inside it at the same time takes the subtree’s place:
document.facade.deleteBlock('tasks', { keepChildren: false });
Undoing a new block keeps a peer’s text
Undoing the creation of a block (inserting it, pasting it, pressing Enter at the end of a line) removes your part only. The block stays while it holds someone else’s text or a child.
| Alice | Bob | |
|---|---|---|
| 1 | Presses Enter at the end of “Agenda”, creating an empty line. | |
| 2 | Types “Budget review” in the new line. | |
| 3 | Undoes. The line stays, with Bob’s “Budget review”. |
This holds even when Bob’s text arrives after Alice’s undo. If Bob later deletes his text, the empty line goes away; Alice’s redo brings her part back beside Bob’s.
The exception: undoing a split. When a line is split in two, the second half is text that moved, not text that was typed, so undoing the split joins the halves again, whoever typed in the second half:
| Alice | Bob | |
|---|---|---|
| 1 | Splits “hello world” after “hello”. | |
| 2 | Types “!” at the end of “ world”. | |
| 3 | Undoes. One line: “hello world!”. |
An explicit delete wins over concurrent typing
| Alice | Bob | |
|---|---|---|
| 1 | Deletes the “Draft” block. | Types “ v2” in “Draft”, without having seen the delete. |
| 2 | “Draft” is gone. | “Draft” is gone, with “ v2”; Bob’s caret moves to where the block was. |
This applies to text in the deleted block. A block Bob adds under it, or splits off inside it, is kept in its place (see Deleting a block keeps its children). It also applies to deletes only: undoing a block’s creation is not a delete (see above).
An emptied document shows a virtual paragraph
A document can end up with no blocks, for example when two people delete its last two blocks at the same time. Each view then shows one empty paragraph with the caret in it. Nothing is written for it.
| Alice | Bob | |
|---|---|---|
| 1 | Deletes block A (a block selection). | Deletes block B, the only other block. |
| 2 | Sees an empty paragraph; types “Hi”. | Sees an empty paragraph; types “Hello”. |
| 3 | Two lines: “Hi” and “Hello”. | The same two lines. |
The first edit in the virtual paragraph (typing, pasting, Enter, turning it into a heading) creates a real block in the same update. Nobody writes a placeholder block for having seen an empty document, so no empty paragraphs are duplicated. A read-only view shows the virtual paragraph but cannot create it. In a view, edytor.facade.virtual() names it while the document is empty.
Deleting everything keeps the first block
Selecting all the text and deleting it keeps the first block, emptied, with its id, type and data: a heading stays a heading.
| Alice | Bob | |
|---|---|---|
| 1 | Selects all text and deletes. | Selects all text and deletes. |
| 2 | One empty block. | The same one empty block. |
Two people clearing the document converge on one block, not two. Typing that Bob did in the first block at the same time survives in it; typing in any other block goes with that block. Each person’s delete holds on its own: the first undo leaves the other’s delete in effect, the second brings the document back.
Moves, splits and merges keep identity
A block keeps its id and its CRDT identity when it is moved, nested, split or merged. Edits made concurrently to it land where the block, or its text, ends up.
| Alice | Bob | Both see | |
|---|---|---|---|
| Move | Moves “Notes” to the end. | Types “!” in “Notes”. | “Notes!” at the end. |
| Same block moved twice | Nests “Notes” under “Ideas”. | Moves “Notes” to the top. | One placement, chosen the same way on every replica. “Notes” is never duplicated. |
| Split | Splits “hello world” after “hello”. | Types “!” after “world”. | “hello” and “ world!“. |
| Merge | Joins “world” into the line above, “hello”. | Types “!” after “world”. | “helloworld!”. |
| Same split twice | Splits “hello world” after “hello”. | Splits it at the same place. | Both new lines exist; the text goes to one and the other stays empty. |
| Merge into a deleted line | Joins “item”, which has nested lines, into “hello” above (Backspace at its start). | Deletes “hello”. | “item” is back in its place, its former nested lines right after it, in order. |
mergeFrom into a deleted block |
Calls hello.mergeFrom(item): the nested lines of “item” become the last children of “hello”. |
Deletes “hello”. | “item” is back in its place, with its nested lines still under it. |
Moving a block into its own subtree is refused, and concurrent moves that would form a cycle are resolved the same way on every replica: no block becomes unreachable.
Conflicting type changes
When two people change the same block’s type at the same time, one change wins, chosen the same way on every replica. Undo never leaves a block without a type.
| Alice | Bob | |
|---|---|---|
| 1 | Turns the paragraph “Plan” into a heading. | Turns it into a quote. |
| 2 | Both see the same one: say Alice’s heading won. | |
| 3 | Undoes. “Plan” is a paragraph again, the type Alice replaced, for both. | |
| 4 | Redoes. “Plan” is a heading again. |
If Bob’s quote had won, Alice’s undo changes nothing: the quote stays. The same holds for block data (a to-do’s checked, a heading’s level) and for an inline atom’s data. lastChangedBy follows the same rule: it returns to the author before Alice.
Void blocks hold no children
Turning a block into a void kind (an image, a divider) moves its nested blocks out, right after it, in order: nothing renders a void block’s children. Undo puts them back under the block.
The same holds for whatever Bob puts under the block without having seen the change: a void block never shows children.
| Alice | Bob | |
|---|---|---|
| 1 | Turns “Plan”, which has a child “Step 1”, into a divider. | Presses Enter in the middle of “Step 1”, and nests “Notes” under “Plan”. |
| 2 | After both sync: the divider, then “Step 1”, its second half, and “Notes”, in that order, for both. | |
| 3 | Undoes. “Plan” is a paragraph again, with the three blocks nested under it. |
The rule reads the kind’s role from the document’s semantics, so every replica needs the same roles: a document with no roles for a kind treats it as any other block and shows its children.
Island blocks
An island block (the code plugin’s code block, for example) seals its content: range deletions and merges never cross its boundary, and blocks cannot be moved into or out of it. See the document model.
Deleting or merging away an island turns its children into ordinary blocks: each takes the default child type of its new parent (a paragraph at the top level). A line Bob adds to the island without having seen the change does the same.
| Alice | Bob | |
|---|---|---|
| 1 | Deletes a code block with one line, “let a”. | Adds a line “let b” to the code block. |
| 2 | After both sync: “let a” and “let b” are paragraphs where the code block was, for both. | |
| 3 | Turns “let b” into a heading, moves it, or presses Enter or Tab in it. It stays a heading or a paragraph, never a code line outside a code block. | |
| 4 | Undoes back to the delete. The code block is back with both lines as code lines. |
A code line holds no nested blocks, and a code block holds only code lines. When Alice’s undo brings a code block back while Bob has changed one of its former lines, both rules keep Bob’s work visible:
| Bob, after the delete and before Alice’s undo | After Alice’s undo |
|---|---|
| Nests the paragraph below under “let a” (Tab) | The paragraph shows right after the code block, where it can be edited and moved. |
| Turns “let a” into a heading | “let a” shows as a code line in the code block. If Alice redoes, it is a heading again. |
Nothing merges into a code block from outside it: mergeFrom refuses the merge and canMerge answers false.
Mixed versions
Clients must run compatible edytor versions. A client of another document generation is refused by the providers and the room. Older clients of the same generation ignore the per-person delete and undo records described above and fall back to plain undo; see Limitations.