diff --git a/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts b/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts index bda7bf3bc3..1878ace438 100644 --- a/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts +++ b/examples/07-collaboration/14-suggestion-gallery/src/scenarios.ts @@ -1021,7 +1021,7 @@ export const scenarios: SuggestionScenario[] = [ feedback: [ { severity: "low", - note: "Block N1 appears in two places. Previously this concurrency scenario would also not be correctly handled (one of the edits would be dropped).", + note: "N2 is lost: B's indent moves it into the original N1, which A's indent deletes. Versioning shows N2 deleted without an author (B only moved it, and A never saw it there). Before nesting changes diffed in place, N1 appeared twice instead. Needs move support.", }, ], title: "Cascading indents", @@ -1047,7 +1047,7 @@ export const scenarios: SuggestionScenario[] = [ feedback: [ { severity: "high", - note: "Q appears twice: B's indent moves a copy of Q under R, and A's nesting replaces the original Q with another copy holding B1–B3.", + note: "B1–B3 are lost. A's indent writes them into the original Q, which B's indent concurrently deletes (a move inserts a copy of Q, without them). Versioning shows them deleted without an author. Before nesting changes diffed in place, Q appeared twice instead. Needs move support.", }, ], title: "Nest blocks into a block that is moved", @@ -1103,8 +1103,8 @@ export const scenarios: SuggestionScenario[] = [ id: "concurrent-move-into-deleted-block", feedback: [ { - severity: "high", - note: "X is lost: B's move inserts a copy into Parent, which A deletes. Versioning shows X as deleted by B, though B only moved it, and doesn't show Child at all. To be fixed by #3166.", + severity: "info", + note: "X is lost: B's move inserts a copy into Parent, which A deletes. Versioning shows X deleted without an author: B only moved it, and A never saw it there.", }, ], title: "Move a block into a block that is deleted", @@ -1133,8 +1133,8 @@ export const scenarios: SuggestionScenario[] = [ id: "concurrent-delete-parent-vs-child-type", feedback: [ { - severity: "high", - note: "B's type change is lost with Parent. Versioning shows Parent as deleted by A, but doesn't show Child at all. To be fixed by #3166.", + severity: "info", + note: "B's type change is lost with Parent. Versioning shows Parent deleted by A, and Child deleted without an author: B's change replaced Child with a copy that was lost, and the diff can't tell who to credit.", }, ], title: "Delete a parent vs change its child's type", diff --git a/packages/core/src/y/extensions/AttributionExtension.test.ts b/packages/core/src/y/extensions/AttributionExtension.test.ts index ec4ff60329..3a7e28b8f5 100644 --- a/packages/core/src/y/extensions/AttributionExtension.test.ts +++ b/packages/core/src/y/extensions/AttributionExtension.test.ts @@ -297,8 +297,7 @@ describe("AttributionExtension user loading", () => { }); }); - // To be fixed by #3166. - it.fails("does not show a change to a block's id", () => { + it("does not show a change to a block's id", () => { const { editor } = createEditor(); editor.replaceBlocks(editor.document, [ { type: "paragraph", content: "hello" }, diff --git a/packages/core/src/y/extensions/AttributionExtension.ts b/packages/core/src/y/extensions/AttributionExtension.ts index 34e3f214ba..4f47e96a8b 100644 --- a/packages/core/src/y/extensions/AttributionExtension.ts +++ b/packages/core/src/y/extensions/AttributionExtension.ts @@ -230,19 +230,16 @@ export const AttributionExtension = createExtension( .map((id) => userStore.getUser(id)?.username ?? id) .filter((username) => username !== ""); - // A stable identity string for a wrapper (empty if unattributed), used to - // (a) test whether a mark is attributed and (b) group adjacent marks with - // the *same* attribution under one tooltip. It's an internal grouping key, - // not the displayed text — that's composed in the view from `users` and - // the format label — so it's built from raw `data-*` (ids + format keys) - // and stays free of i18n/username resolution. + // A stable identity string for a mark wrapper, used to group nested marks + // with the *same* change under one tooltip. Every wrapper is a change; + // its author may be unknown (e.g. content removed along with a + // concurrently deleted block). It's an internal grouping key, not the + // displayed text, so it's built from raw `data-*` and stays free of + // i18n/username resolution. const attributionIdentity = (wrapper: HTMLElement) => { const ids = parseUserIds(wrapper.dataset["userIds"]); - if (ids.length === 0 && wrapper.dataset["attributes"] === undefined) { - return ""; - } const format = parseFormatKeys(wrapper.dataset["format"]); - return `${wrapper.dataset["attributes"] ?? ""}:${format.join(",")}:${ids.join(",")}`; + return `${wrapper.tagName}:${wrapper.dataset["attributes"] ?? ""}:${format.join(",")}:${ids.join(",")}`; }; // Build the tooltip state from a wrapper's `data-*` attributes. A @@ -302,24 +299,6 @@ export const AttributionExtension = createExtension( store.setState(undefined); }; - // The innermost attributed mark at or above `el`, skipping unattributed - // wrappers so an attributed ancestor still wins. - const innermostAttributed = ( - el: Element | null, - ): HTMLElement | undefined => { - while (el) { - const wrapper = el.closest(ATTRIBUTION_MARK_SELECTOR); - if (!wrapper) { - return undefined; - } - if (attributionIdentity(wrapper)) { - return wrapper; - } - el = wrapper.parentElement; - } - return undefined; - }; - const nodeAttribution = ( target: Element, ): { mark: HTMLElement; preview: Element } | undefined => { @@ -356,9 +335,9 @@ export const AttributionExtension = createExtension( ) { return undefined; } - const mark = Array.from( - owner.querySelectorAll(ATTRIBUTION_MARK_SELECTOR), - ).find(attributionIdentity); + const mark = owner.querySelector( + ATTRIBUTION_MARK_SELECTOR, + ); return mark ? { mark, preview: owner } : undefined; }; @@ -366,7 +345,8 @@ export const AttributionExtension = createExtension( const target = event.target instanceof Element ? event.target : null; const hoveredMark = target && dom.contains(target) - ? innermostAttributed(target) + ? (target.closest(ATTRIBUTION_MARK_SELECTOR) ?? + undefined) : undefined; const fallback = target && !hoveredMark ? nodeAttribution(target) : undefined; @@ -379,8 +359,7 @@ export const AttributionExtension = createExtension( const identity = attributionIdentity(innermost); // Anchor on the outermost ancestor with the *same* attribution so one - // tooltip covers the whole region; a differently-attributed ancestor - // breaks the chain, and unattributed ones are climbed past. + // tooltip covers the whole region; a different ancestor breaks the chain. let anchor = innermost; let el: Element | null = innermost.parentElement; while (el) { @@ -388,12 +367,10 @@ export const AttributionExtension = createExtension( if (!ancestor) { break; } - const ancestorIdentity = attributionIdentity(ancestor); - if (ancestorIdentity === identity) { - anchor = ancestor; - } else if (ancestorIdentity) { + if (attributionIdentity(ancestor) !== identity) { break; } + anchor = ancestor; el = ancestor.parentElement; } diff --git a/packages/core/src/y/extensions/YAttributionMarks.ts b/packages/core/src/y/extensions/YAttributionMarks.ts index 88f5de9d36..253d2e2a5b 100644 --- a/packages/core/src/y/extensions/YAttributionMarks.ts +++ b/packages/core/src/y/extensions/YAttributionMarks.ts @@ -112,6 +112,16 @@ const createAttributionMarkView = ) => ({ mark, inline }: { mark: PMMark; inline: boolean }) => { const editor = options?.editor; + // A block's id isn't content users edit, so a change to it isn't shown + // (e.g. a diff pairing a deleted block with the editor's empty one). + // Inline content may have an `id` prop of its own, which is. + const { id: _id, ...blockChanges } = getAttributeChanges(mark); + const changes = inline ? getAttributeChanges(mark) : blockChanges; + if (type === "attrs" && Object.keys(changes).length === 0) { + const dom = document.createElement(inline ? "span" : "div"); + dom.style.display = "contents"; + return { dom, contentDOM: dom }; + } // ``/`` are semantic elements. The modification mark has no // dedicated element, so it renders as a `` inline or a `
` over a // block, matching its `parseDOM` rules. @@ -131,7 +141,7 @@ const createAttributionMarkView = }); if (type === "attrs") { dom.dataset["type"] = "attributes"; - dom.dataset["attributes"] = JSON.stringify(getAttributeChanges(mark)); + dom.dataset["attributes"] = JSON.stringify(changes); } if (type === "modification") { dom.dataset["type"] = "modification"; diff --git a/packages/core/src/y/extensions/YSync.ts b/packages/core/src/y/extensions/YSync.ts index 9e2f13d06b..280118f370 100644 --- a/packages/core/src/y/extensions/YSync.ts +++ b/packages/core/src/y/extensions/YSync.ts @@ -9,6 +9,7 @@ import { createExtension, } from "../../editor/BlockNoteExtension.js"; import { blockMatchNodes } from "./blockMatchNodes.js"; +import { mergeBlockGroups } from "./mergeBlockGroups.js"; import { docToBlocks } from "../../api/nodeConversions/nodeToBlock.js"; import type { BlockSchema, @@ -178,6 +179,8 @@ export const YSyncExtension = createExtension( // needed; `blockContainer` already whitelists the `y-attributed-*` // marks. See blockMatchNodes.ts. customCompare: blockMatchNodes, + // Nesting changes diff in place: show a block's groups as one group + transformers: [mergeBlockGroups], // Initial-empty gate: a single empty paragraph (any id/props) must // not seed an empty Y fragment — see isInitialBlockNoteDoc above // and "Initial-content gate" in ProsemirrorRdt's doc. diff --git a/packages/core/src/y/extensions/blockMatchNodes.ts b/packages/core/src/y/extensions/blockMatchNodes.ts index 0e79597a3d..f8eec56748 100644 --- a/packages/core/src/y/extensions/blockMatchNodes.ts +++ b/packages/core/src/y/extensions/blockMatchNodes.ts @@ -23,24 +23,6 @@ const firstChild = ( return null; }; -/** - * Whether a `blockContainer` delta carries a child `blockGroup` — i.e. the block - * has nested children. A container's content is `blockContent blockGroup?`, so - * this is what tells a leaf block apart from a parent. - */ -const hasBlockGroup = (d: schema.Unwrap): boolean => { - for (const op of (d as any).children) { - if (delta.$insertOp.check(op)) { - for (const it of op.insert) { - if (delta.$deltaAny.check(it) && it.name === "blockGroup") { - return true; - } - } - } - } - return false; -}; - function getTableDimensions( d: schema.Unwrap, ): { rows: number; cols: number } | null { @@ -155,16 +137,6 @@ export const blockMatchNodes = ( return false; } - // A change in nesting is structural too: if one container gains or loses a - // child `blockGroup`, diffing it in place would insert/delete the blockGroup as - // a sibling of the block content inside a single container — schema-invalid. - // Treat it as different so the whole container is replaced instead, same as a - // content-type change. Keeps concurrent nesting merges (e.g. two users nesting - // a block under the same parent) from producing a lopsided in-place result. - if (hasBlockGroup(a) !== hasBlockGroup(b)) { - return false; - } - if (childA?.name === "table" && childB?.name === "table") { const dimA = getTableDimensions(childA); const dimB = getTableDimensions(childB); diff --git a/packages/core/src/y/extensions/legacyYjsDocBinding.test.ts b/packages/core/src/y/extensions/legacyYjsDocBinding.test.ts index c5e3aa7377..479f714716 100644 --- a/packages/core/src/y/extensions/legacyYjsDocBinding.test.ts +++ b/packages/core/src/y/extensions/legacyYjsDocBinding.test.ts @@ -383,8 +383,7 @@ describe("legacy Yjs document binding", () => { ]); }); - // To be fixed by #3167. - it.fails.each(structuralChanges)( + it.each(structuralChanges)( "diffs $name made with the old binding like one made with the new binding", ({ blocks, change }) => { const legacy = createLegacyEditor(); diff --git a/packages/core/src/y/extensions/mergeBlockGroups.ts b/packages/core/src/y/extensions/mergeBlockGroups.ts new file mode 100644 index 0000000000..77152c13f5 --- /dev/null +++ b/packages/core/src/y/extensions/mergeBlockGroups.ts @@ -0,0 +1,332 @@ +import * as delta from "lib0/delta"; +import * as dt from "lib0/delta/transformer"; + +/** + * Lets nesting changes diff in place instead of replacing the parent block. + * + * A `blockContainer`'s content is `blockContent blockGroup?`, but Y can hold + * several `blockGroup`s in one container (two users giving a block its first + * child concurrently, or a diff rendering an old and a new group side by side), + * and a group can be empty. A `blockGroup` carries no attributes, so the view + * shows one group holding the children of all non-empty groups, and hides the + * group while there are none. Edits to that group are routed back to the group + * each child came from; emptying a group keeps it in Y (only hidden), so a child + * another user adds to it concurrently is not lost with it. + * + * Assumes the groups follow the block content, as the schema orders them. + */ + +type Slot = { group: false } | { group: true; len: number }; +type GroupSlot = Extract; +// lib0's delta builders are untyped at this level of genericity. +type AnyDelta = any; + +const GROUP = "blockGroup"; + +function isGroup(el: unknown): boolean { + return delta.$deltaAny.check(el) && (el as AnyDelta).name === GROUP; +} + +function isVisible(slots: Slot[]): boolean { + return slots.some((slot) => slot.group && slot.len > 0); +} + +/** Positions an op covers before the change, and after it. */ +function oldLength(op: AnyDelta): number { + return delta.$insertOp.check(op) || delta.$textOp.check(op) ? 0 : op.length; +} +function newLength(op: AnyDelta): number { + return delta.$deleteOp.check(op) ? 0 : op.length; +} +function written(d: AnyDelta, side: "old" | "new"): number { + let n = 0; + for (const op of d.children) { + n += side === "old" ? oldLength(op) : newLength(op); + } + return n; +} +function isEmpty(d: AnyDelta): boolean { + return d.children.start == null; +} + +/** Copy `len` positions of `op` (all of it by default) into `target`. */ +function copyOp(target: AnyDelta, op: AnyDelta, len: number = op.length) { + if (delta.$retainOp.check(op)) { + target.retain(len, op.format, op.attribution); + } else if (delta.$insertOp.check(op) || delta.$textOp.check(op)) { + target.insert(op.insert, op.format, op.attribution); + } else if (delta.$deleteOp.check(op)) { + target.delete(len); + } else if (delta.$modifyOp.check(op)) { + target.modify(delta.clone(op.value), op.format, op.attribution); + } +} + +/** + * Insert the children of an inserted group into `target`. Each child keeps its + * own attribution; the group's applies where a child has none. + */ +function insertChildren(target: AnyDelta, group: AnyDelta, groupOp: AnyDelta) { + for (const op of group.children) { + if (delta.$insertOp.check(op)) { + target.insert( + op.insert, + op.format ?? groupOp.format, + op.attribution ?? groupOp.attribution, + ); + } + } +} + +class MergeBlockGroupsTransformer extends dt.Transformer { + /** One entry per child on the Y side. Groups follow the block content. */ + slots: Slot[] = []; + + /** Y-side change -> view-side change. */ + override applyA(d: AnyDelta) { + const out: AnyDelta = delta.cloneShallow(d); + const oldSlots = this.slots; + const newSlots: Slot[] = []; + // The change to the shown group's children. + const shown: AnyDelta = delta.create(); + let oi = 0; + + for (const op of d.children) { + if (delta.$insertOp.check(op)) { + for (const el of op.insert) { + if (isGroup(el)) { + newSlots.push({ group: true, len: el.childCnt }); + insertChildren(shown, el, op); + } else { + newSlots.push({ group: false }); + out.insert([el], op.format, op.attribution); + } + } + continue; + } + if (delta.$textOp.check(op)) { + out.insert(op.insert, op.format, op.attribution); + continue; + } + for (let n = 0; n < op.length && oi < oldSlots.length; n++) { + const slot = oldSlots[oi++]; + if (!slot.group) { + copyOp(out, op, 1); + if (!delta.$deleteOp.check(op)) { + newSlots.push(slot); + } + } else if (delta.$retainOp.check(op)) { + if (slot.len > 0) { + shown.retain(slot.len, op.format, op.attribution); + } + newSlots.push(slot); + } else if (delta.$deleteOp.check(op)) { + if (slot.len > 0) { + shown.delete(slot.len); + } + } else { + // replay the group's own child ops, then step over the rest of it + let consumed = 0; + let len = slot.len; + for (const child of op.value.children) { + copyOp(shown, child); + consumed += oldLength(child); + len += newLength(child) - oldLength(child); + } + if (slot.len > consumed) { + shown.retain(slot.len - consumed); + } + newSlots.push({ group: true, len }); + } + } + } + newSlots.push(...oldSlots.slice(oi)); + this.slots = newSlots; + + const wasShown = isVisible(oldSlots); + const isShown = isVisible(newSlots); + const position = newSlots.filter((s) => !s.group).length; + const gap = position - written(out, "new"); + if (wasShown && isShown && !isEmpty(shown)) { + out.retain(gap); + out.modify(shown.done(false)); + } else if (wasShown && !isShown) { + out.retain(gap); + out.delete(1); + } else if (!wasShown && isShown) { + // Every group was empty before, so the change only inserts children. + const group = delta.create(GROUP); + for (const op of shown.children) { + if (delta.$insertOp.check(op)) { + group.insert(op.insert, op.format, op.attribution); + } + } + out.retain(gap); + out.insert([group.done(false)]); + } + out.done(false); + return dt.createTransformResult(null, out); + } + + /** View-side change -> Y-side change. */ + override applyB(d: AnyDelta) { + const out: AnyDelta = delta.cloneShallow(d); + const groups = this.slots.filter((s): s is GroupSlot => s.group); + const plain = this.slots.length - groups.length; + const shown = isVisible(this.slots); + const newSlots: Slot[] = []; + let bi = 0; // view-side child index before the change + + for (const op of d.children) { + if (delta.$insertOp.check(op)) { + for (const el of op.insert) { + if (isGroup(el) && groups.length > 0) { + // The view shows a group again: refill the first hidden group + // rather than adding another one. + const refill = delta.create(); + insertChildren(refill, el, op); + out.modify(refill.done(false)); + groups[0] = { group: true, len: el.childCnt }; + if (groups.length > 1) { + out.retain(groups.length - 1); + } + newSlots.push(...groups); + groups.length = 0; + } else { + out.insert([el], op.format, op.attribution); + newSlots.push( + isGroup(el) + ? { group: true, len: el.childCnt } + : { group: false }, + ); + } + } + continue; + } + if (delta.$textOp.check(op)) { + copyOp(out, op); + continue; + } + for (let n = 0; n < op.length; n++, bi++) { + if (bi < plain) { + copyOp(out, op, 1); + if (!delta.$deleteOp.check(op)) { + newSlots.push(this.slots[bi]); + } + continue; + } + if (!shown || groups.length === 0) { + continue; + } + // the shown group stands for all of the Y side's groups + if (delta.$retainOp.check(op)) { + out.retain(groups.length, op.format, op.attribution); + newSlots.push(...groups); + } else if (delta.$deleteOp.check(op)) { + // Empty every group, but keep them: see the module doc. + for (const group of groups) { + if (group.len > 0) { + out.modify(delta.create().delete(group.len).done(false)); + } else { + out.retain(1); + } + newSlots.push({ group: true, len: 0 }); + } + } else { + this.routeIntoGroups(op.value, groups, out, newSlots); + } + groups.length = 0; + } + } + // untouched trailing slots + if (bi < plain) { + newSlots.push(...this.slots.slice(bi, plain)); + } + newSlots.push(...groups); + this.slots = newSlots; + out.done(false); + return dt.createTransformResult(out, null); + } + + /** Route a change to the shown group into changes to each Y-side group. */ + routeIntoGroups( + change: AnyDelta, + groups: GroupSlot[], + out: AnyDelta, + newSlots: Slot[], + ) { + const ends: number[] = []; + let acc = 0; + for (const group of groups) { + acc += group.len; + ends.push(acc); + } + const perGroup: AnyDelta[] = groups.map(() => delta.create()); + const lens = groups.map((group) => group.len); + let pos = 0; + for (const op of change.children) { + if (delta.$insertOp.check(op) || delta.$textOp.check(op)) { + // an insert at a boundary joins the earlier group + let k = ends.findIndex((end) => pos <= end); + k = k === -1 ? groups.length - 1 : k; + const start = k === 0 ? 0 : ends[k - 1]; + const target = perGroup[k]; + target.retain(Math.max(0, pos - start - written(target, "old"))); + copyOp(target, op); + lens[k] += op.length; + continue; + } + let remaining = op.length; + while (remaining > 0) { + const k = ends.findIndex((end) => pos < end); + if (k === -1) { + break; + } + const start = k === 0 ? 0 : ends[k - 1]; + const take = Math.min(remaining, ends[k] - pos); + const target = perGroup[k]; + target.retain(Math.max(0, pos - start - written(target, "old"))); + copyOp(target, op, take); + if (delta.$deleteOp.check(op)) { + lens[k] -= take; + } + pos += take; + remaining -= take; + } + } + groups.forEach((_, k) => { + if (isEmpty(perGroup[k])) { + out.retain(1); + } else { + out.modify(perGroup[k].done(false)); + } + newSlots.push({ group: true, len: lens[k] }); + }); + } +} + +class MergeBlockGroups extends dt.Template { + override get name() { + return "blocknote:mergeBlockGroups"; + } + override init() { + return new MergeBlockGroupsTransformer(this.$in, this.$out); + } +} + +const groupHolders = new Set(["blockGroup", "column", "columnList"]); + +/** Applies {@link MergeBlockGroupsTransformer} to every `blockContainer`. */ +export function mergeBlockGroups($d: any): dt.Template { + return dt.children($d, (child: AnyDelta, $c: any) => + child.name === "blockContainer" + ? dt.pipe( + $c, + ($1: any) => new MergeBlockGroups($1, delta.$deltaAny), + mergeBlockGroups, + ) + : groupHolders.has(child.name) + ? mergeBlockGroups($c) + : null, + ); +} diff --git a/packages/core/src/y/extensions/nestingChanges.test.ts b/packages/core/src/y/extensions/nestingChanges.test.ts index 50e57c2179..c5272e4a69 100644 --- a/packages/core/src/y/extensions/nestingChanges.test.ts +++ b/packages/core/src/y/extensions/nestingChanges.test.ts @@ -105,8 +105,7 @@ describe("concurrently created child groups", () => { return users; } - // To be fixed by #3168. - it.fails("shows both children under one parent", () => { + it("shows both children under one parent", () => { const { a, b, docA } = nestedTwice(); expect(outline(a.document)).toBe("P{X, Z}"); expect(outline(b.document)).toBe("P{X, Z}"); @@ -114,16 +113,14 @@ describe("concurrently created child groups", () => { expect(groupsOf(docA, "p")).toEqual([1, 1]); }); - // To be fixed by #3168. - it.fails("reads both children outside the editor", () => { + it("reads both children outside the editor", () => { const { docA } = nestedTwice(); expect(outline(yDocToBlocks(BlockNoteEditor.create(), docA, "doc"))).toBe( "P{X, Z}", ); }); - // To be fixed by #3168. - it.fails("routes edits back to each child's own group", () => { + it("routes edits back to each child's own group", () => { const users = nestedTwice(); users.a.setTextCursorPosition("z", "end"); users.a.insertInlineContent("!"); @@ -145,8 +142,7 @@ describe("concurrently created child groups", () => { }); describe("emptied child groups", () => { - // To be fixed by #3168. - it.fails("keeps a child added while another user removes the last one", () => { + it("keeps a child added while another user removes the last one", () => { const users = twoUsers([ { id: "p", @@ -166,8 +162,7 @@ describe("emptied child groups", () => { expect(outline(users.b.document)).toBe("P{D}"); }); - // To be fixed by #3168. - it.fails("reuses a hidden group when nesting again", () => { + it("reuses a hidden group when nesting again", () => { const users = twoUsers([ { id: "p", @@ -220,8 +215,7 @@ describe("version diff of a nesting change", () => { return changed; } - // To be fixed by #3168. - it.fails("shows an indent as a moved block, leaving the new parent unchanged", () => { + it("shows an indent as a moved block, leaving the new parent unchanged", () => { expect( diffOf( (editor) => { @@ -236,8 +230,7 @@ describe("version diff of a nesting change", () => { ).toEqual(["+X", "-X"]); }); - // To be fixed by #3168. - it.fails("shows an unindent as a moved block, leaving the old parent unchanged", () => { + it("shows an unindent as a moved block, leaving the old parent unchanged", () => { expect( diffOf( (editor) => { diff --git a/packages/core/src/y/extensions/snapshotCodec.ts b/packages/core/src/y/extensions/snapshotCodec.ts index 985c68502f..0e43537a88 100644 --- a/packages/core/src/y/extensions/snapshotCodec.ts +++ b/packages/core/src/y/extensions/snapshotCodec.ts @@ -19,7 +19,7 @@ export function decodeFragmentUpdate( opts?: { suggestionDoc?: boolean }, ): DecodedFragment { const doc = new Y.Doc( - opts?.suggestionDoc ? { isSuggestionDoc: true } : undefined, + opts?.suggestionDoc ? { gc: false, isSuggestionDoc: true } : { gc: false }, ); try { Y.applyUpdateV2(doc, content); diff --git a/packages/core/src/y/extensions/snapshotPreview.ts b/packages/core/src/y/extensions/snapshotPreview.ts index b619689062..5f461acbf0 100644 --- a/packages/core/src/y/extensions/snapshotPreview.ts +++ b/packages/core/src/y/extensions/snapshotPreview.ts @@ -2,11 +2,204 @@ import { configureYProsemirror } from "@y/prosemirror"; import * as Y from "@y/y"; import type { BlockNoteEditor } from "../../editor/BlockNoteEditor.js"; +import { findTypeInOtherYdoc } from "../utils.js"; +import { blockMatchNodes } from "./blockMatchNodes.js"; import { decodeFragmentUpdate, destroyDecodedFragment, } from "./snapshotCodec.js"; +/** + * Snapshots are decoded with `gc: false`, so content inside a deleted block keeps + * its own attribution: content swept away with a concurrently deleted block shows + * no author instead of inheriting the deleter. The stock renderer hides content + * that was inserted and deleted between the two versions only once it has been + * garbage collected; hide it here too. + */ +class SnapshotDiffRenderer extends Y.DiffRenderer { + override readContent( + ...[contents, client, clock, deleted, content, shouldRender]: Parameters< + Y.DiffRenderer["readContent"] + > + ) { + const start = contents.length; + super.readContent(contents, client, clock, deleted, content, shouldRender); + if (deleted) { + for (let i = contents.length - 1; i >= start; i--) { + if (this.inserts.has(client, contents[i].clock)) { + contents.splice(i, 1); + } + } + } + } +} + +/** Whether the item with this id was already in `baseline`. */ +function inBaseline(baseline: Y.Doc, id: Y.ID): boolean { + const last = baseline.store.clients.get(id.client)?.at(-1); + return last !== undefined && id.clock < last.id.clock + last.length; +} + +/** + * Moved blocks that are gone, with their content. + * + * A move (indenting, dragging, a type change, ...) deletes a block and inserts + * a copy with the same id. While a copy is shown, the diff shows the move as + * is. Once every copy is gone, only the original's deletion is shown, and + * that names the mover, who may not have deleted it: the copy can be lost + * with a concurrently deleted parent, or deleted later by someone else. Such + * deletions are shown without an author. + */ +function lostMoves(doc: Y.Doc, baseline: Y.Doc): Y.IdSet { + const originals = new Map(); + const copies = new Map(); + for (const structs of doc.store.clients.values()) { + for (const item of structs) { + if ( + !(item instanceof Y.Item) || + !(item.content instanceof Y.ContentType) || + item.content.type.name !== "blockContainer" + ) { + continue; + } + // Read the id directly: a deleted block's attributes read as unset. + const idItem = item.content.type._map.get("id"); + const id = idItem?.content.getContent().at(-1); + if (id == null) { + continue; + } + if (inBaseline(baseline, item.id)) { + originals.set(id, [...(originals.get(id) ?? []), item.content.type]); + } else { + copies.set(id, [...(copies.get(id) ?? []), item]); + } + } + } + const lost = Y.createIdSet(); + for (const [id, blocks] of originals) { + if (!copies.get(id)?.every((copy) => copy.deleted)) { + continue; + } + for (const structs of doc.store.clients.values()) { + for (const item of structs) { + if ( + item instanceof Y.Item && + blocks.some( + (block) => item === block._item || Y.isParentOf(block, item), + ) + ) { + lost.add(item.id.client, item.id.clock, item.length); + } + } + } + } + return lost; +} + +/** The distinct attributions recorded for `node` and its descendants. */ +function subtreeAttributions( + node: Y.Node, + map: Y.IdMap, +): Y.ContentAttribute[] { + const found = new Map>(); + for (const structs of node.doc!.store.clients.values()) { + for (const item of structs) { + if ( + item instanceof Y.Item && + (item === node._item || Y.isParentOf(node, item)) + ) { + for (const range of map.slice( + item.id.client, + item.id.clock, + item.length, + )) { + for (const attr of range.attrs ?? []) { + found.set(`${attr.name}:${String(attr.val)}`, attr); + } + } + } + } + } + return [...found.values()]; +} + +/** + * Replace each block that changed structurally since `baseline` with a fresh + * copy, where "structurally" is what `blockMatchNodes` treats as a different + * block (e.g. a type change). The old collaboration binding stored such changes + * inside the same container, which a diff renders as schema-invalid content + * that is then dropped. A fresh container diffs as a deleted block next to an + * inserted one, as the current binding stores it. The copy and the deletion + * take over the original's attributions, so the change keeps its author. + */ +function splitChangedBlocks( + node: Y.Node, + baseline: Y.Doc, + attributions: Y.ContentMap | undefined, + added: Y.ContentMap, +): void { + for (let index = 0; index < node.length; index++) { + const child = node.get(index); + if (!(child instanceof Y.Node)) { + continue; + } + if (child.name === "blockContainer" && child._item) { + // A block created after the baseline has no previous version. + const before = inBaseline(baseline, child._item.id) + ? findTypeInOtherYdoc(child, baseline) + : undefined; + if ( + before && + !blockMatchNodes(before.toDeltaDeep(), child.toDeltaDeep()) + ) { + const inserted = attributions + ? subtreeAttributions(child, attributions.inserts) + : []; + const deleted = attributions + ? subtreeAttributions(child, attributions.deletes) + : []; + const doc = child.doc!; + // A change that only inserted (or only deleted) still has one author + // for both sides of the split, under that side's attribution kind. + function as( + kind: "insert" | "delete", + attrs: Y.ContentAttribute[], + ) { + return attrs.map((attr) => Y.createContentAttribute(kind, attr.val)); + } + const authors = inserted.length ? inserted : deleted; + function record(tr: Y.Transaction) { + if (authors.length) { + Y.insertIntoIdMap( + added.inserts, + Y.createIdMapFromIdSet(tr.insertSet, as("insert", authors)), + ); + Y.insertIntoIdMap( + added.deletes, + Y.createIdMapFromIdSet( + tr.deleteSet, + as("delete", deleted.length ? deleted : authors), + ), + ); + } + } + doc.on("beforeObserverCalls", record); + try { + doc.transact(() => { + const copy = child.clone(); + node.delete(index); + node.insert(index, [copy]); + }); + } finally { + doc.off("beforeObserverCalls", record); + } + continue; + } + } + splitChangedBlocks(child, baseline, attributions, added); + } +} + /** * Decode a snapshot, diff it against a baseline if given, and render it. * @@ -33,14 +226,35 @@ export function showSnapshotPreview( try { const snapshot = decodeFragmentUpdate(fragment, snapshotContent); try { + let renderAttributions = attributions; + if (baseline) { + const added = Y.createContentMap(); + splitChangedBlocks( + snapshot.fragment, + baseline.doc, + attributions, + added, + ); + if (attributions) { + renderAttributions = Y.createContentMap( + Y.mergeIdMaps([attributions.inserts, added.inserts]), + Y.diffIdMap( + Y.mergeIdMaps([attributions.deletes, added.deletes]), + lostMoves(snapshot.doc, baseline.doc), + ), + ); + } + } editor.exec( configureYProsemirror({ ytype: snapshot.fragment, renderer: baseline - ? Y.createDiffRenderer( + ? new SnapshotDiffRenderer( baseline.doc, snapshot.doc, - attributions ? { attributions } : undefined, + renderAttributions + ? { attributions: renderAttributions } + : undefined, ) : undefined, }), diff --git a/packages/core/src/y/extensions/versionDiffAttribution.test.ts b/packages/core/src/y/extensions/versionDiffAttribution.test.ts index be62893e44..5d7467147b 100644 --- a/packages/core/src/y/extensions/versionDiffAttribution.test.ts +++ b/packages/core/src/y/extensions/versionDiffAttribution.test.ts @@ -149,8 +149,7 @@ function showDiff( } describe("version diff of a deleted block", () => { - // To be fixed by #3166. - it.fails("does not attribute content added concurrently inside it to the deleter", () => { + it("does not attribute content added concurrently inside it to the deleter", () => { const base = baseDocument([ { id: "parent", @@ -197,8 +196,7 @@ describe("version diff of a deleted block", () => { ]); }); - // To be fixed by #3166. - it.fails("names no author when hovering content removed with it", () => { + it("names no author when hovering content removed with it", () => { const base = baseDocument([ { id: "parent", type: "paragraph", content: "Parent" }, { id: "next", type: "paragraph", content: "Next" }, @@ -306,8 +304,7 @@ describe("version diff of a moved block", () => { .map((change) => `${change.text}: ${change.users.join(", ")}`); } - // To be fixed by #3166. - it.fails("attributes a move to the mover", () => { + it("attributes a move to the mover", () => { const base = blocks(); const server = history(base); const after = server.apply(editOf(base, 2, nest), "bob"); @@ -316,8 +313,7 @@ describe("version diff of a moved block", () => { ).toEqual(["[block moved]: bob", "Moved: bob"]); }); - // To be fixed by #3166. - it.fails("names no author for a block moved into a concurrently deleted one", () => { + it("names no author for a block moved into a concurrently deleted one", () => { const base = blocks(); const bob = editOf(base, 2, nest); const alice = editOf(base, 1, removeParent); @@ -330,8 +326,7 @@ describe("version diff of a moved block", () => { ]); }); - // To be fixed by #3166. - it.fails("keeps the parent's deleter when a child's concurrent type change is lost", () => { + it("keeps the parent's deleter when a child's concurrent type change is lost", () => { const base = blocks(); // Bob's type change replaces the child with a heading copy, which Alice's // deletion of the parent takes with it. @@ -352,8 +347,7 @@ describe("version diff of a moved block", () => { ]); }); - // To be fixed by #3166. - it.fails("names no author for a moved block that someone else deletes", () => { + it("names no author for a moved block that someone else deletes", () => { const base = blocks(); const server = history(base); server.apply(editOf(base, 1, nest), "alice"); @@ -370,8 +364,7 @@ describe("version diff of a moved block", () => { ]); }); - // To be fixed by #3168. - it.fails.each([ + it.each([ ["Bob", ["[block n1]: alice", "N1: alice", "[block n2]: ", "N2: "]], ["Alice", ["[block n2]: ", "N2: "]], ])( diff --git a/packages/core/src/y/utils.ts b/packages/core/src/y/utils.ts index c447c44678..2d97cb5dae 100644 --- a/packages/core/src/y/utils.ts +++ b/packages/core/src/y/utils.ts @@ -1,6 +1,5 @@ import { defaultTransformer, - deltaToPNode, deltaToPSteps, docToDelta, nodeToDelta, @@ -21,10 +20,17 @@ import { docToBlocks, } from "../index.js"; import { blockMatchNodes } from "./extensions/blockMatchNodes.js"; +import { mergeBlockGroups } from "./extensions/mergeBlockGroups.js"; import { mapAttributionToMark } from "./extensions/YSync.js"; import * as Y from "@y/y"; +// Renders Y content as the collaboration binding does (see YSync). +const bindingTransformer = defaultTransformer({ + mapAttributionToMark, + transformers: [mergeBlockGroups], +}); + /** * Find the equivalent of a Y.Node in another Y.Doc. * @@ -212,12 +218,15 @@ export function yfragmentToBlocks< // A fragment without block containers holds no blocks — e.g. one written // by an older `blocksToYDoc([])` (which used to write a childless block // group). Returning early avoids materializing the schema filler paragraph - // in `deltaToPNode` below, whose id would be freshly minted on every + // in `ynodeToPmnode` below, whose id would be freshly minted on every // read — making empty docs unstable. if (!deltaHasBlockContainer(delta)) { return []; } - const pmNode = deltaToPNode(delta, editor.pmSchema, null); + const pmNode = ynodeToPmnode(fragment, editor.pmSchema, { + renderer: null, + transformer: bindingTransformer, + }); return docToBlocks(pmNode); } @@ -333,7 +342,7 @@ export function yNodeToTransaction( options: NonNullable[2]> = {}, ): Transaction { const renderedDoc = ynodeToPmnode(node, tr.doc.type.schema, { - transformer: defaultTransformer({ mapAttributionToMark }), + transformer: bindingTransformer, ...options, }); const renderedDelta = docDiffToDelta(tr.doc, renderedDoc); diff --git a/tests/src/end-to-end/versioning/block-comparison.test.tsx b/tests/src/end-to-end/versioning/block-comparison.test.tsx index 65c26ab827..c719bad01b 100644 --- a/tests/src/end-to-end/versioning/block-comparison.test.tsx +++ b/tests/src/end-to-end/versioning/block-comparison.test.tsx @@ -18,7 +18,7 @@ import { testDocument } from "@shared/testDocument.js"; import * as Y from "@y/y"; import { expect, test } from "vite-plus/test"; import { render } from "vitest-browser-react"; -import { browserName, page } from "../../utils/context.js"; +import { browserName, page, userEvent } from "../../utils/context.js"; import { sleep } from "../../utils/editor.js"; import { screenshotFull } from "../../utils/screenshotFull.js"; @@ -164,6 +164,12 @@ for (const theme of ["light", "dark"] as const) { await expect .poll(() => root.querySelectorAll("del").length) .toBeGreaterThan(0); + // A preceding test can leave the pointer over the diff, whose changes + // show a tooltip on hover. Park it on the padding, outside the editor. + await userEvent.hover(root, { position: { x: 2, y: 2 } }); + await expect + .poll(() => document.querySelector(".bn-suggestion-tooltip")) + .toBeNull(); for (const toggle of root.querySelectorAll( '.bn-toggle-wrapper[data-show-children="false"]', )) { diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-chromium-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-chromium-linux.png index 8dc026817e..70f4b83e2b 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-chromium-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-chromium-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-firefox-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-firefox-linux.png index a6b54175a9..743acb6aca 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-firefox-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-firefox-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-webkit-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-webkit-linux.png index 348295e7f4..1d5a7d7db4 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-webkit-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-delete-nested-webkit-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-chromium-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-chromium-linux.png index c889839a09..c1b6acd200 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-chromium-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-chromium-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-firefox-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-firefox-linux.png index e3c0196a95..03610d80ba 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-firefox-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-firefox-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-webkit-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-webkit-linux.png index a4407868ae..2f8c424023 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-webkit-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/addRemoveBlocks.test.tsx/add-remove-nest-bullet-under-existing-webkit-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-chromium-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-chromium-linux.png index 055d7b3c3f..a7d12256b3 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-chromium-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-chromium-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-firefox-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-firefox-linux.png index 234d600fc3..f6dd02842a 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-firefox-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-firefox-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-webkit-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-webkit-linux.png index 2f569e9631..d545177305 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-webkit-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-indent-cascade-webkit-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-chromium-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-chromium-linux.png index bf9ef8a7d1..ac1c3b3de5 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-chromium-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-chromium-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-firefox-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-firefox-linux.png index 6e4fae9fd3..75a1f943e1 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-firefox-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-firefox-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-webkit-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-webkit-linux.png index 291c8633b7..f4bfbe4547 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-webkit-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.concurrent.test.tsx/concurrent-nest-both-under-n0-webkit-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-chromium-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-chromium-linux.png index 57d7ec24f4..416aae83ce 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-chromium-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-chromium-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-firefox-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-firefox-linux.png index 27ad94db9e..417c49d27f 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-firefox-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-firefox-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-webkit-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-webkit-linux.png index d77ed2020a..c7b22e98d3 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-webkit-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-indent-webkit-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-chromium-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-chromium-linux.png index 40b74367c1..8f8f16abab 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-chromium-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-chromium-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-firefox-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-firefox-linux.png index 65bb1c509e..48fc502157 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-firefox-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-firefox-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-webkit-linux.png b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-webkit-linux.png index a2b88c0b3b..4998c2e526 100644 Binary files a/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-webkit-linux.png and b/tests/src/end-to-end/y-prosemirror/__screenshots__/nesting.test.tsx/nesting-unindent-webkit-linux.png differ diff --git a/tests/src/end-to-end/y-prosemirror/__snapshots__/addRemoveBlocks.test.tsx.snap b/tests/src/end-to-end/y-prosemirror/__snapshots__/addRemoveBlocks.test.tsx.snap index 182ace0380..209683037e 100644 --- a/tests/src/end-to-end/y-prosemirror/__snapshots__/addRemoveBlocks.test.tsx.snap +++ b/tests/src/end-to-end/y-prosemirror/__snapshots__/addRemoveBlocks.test.tsx.snap @@ -121,27 +121,25 @@ exports[`suggestion mode: add colored block with child to empty doc 3`] = ` - - - - - - - - - Child block - - - - - - - - + + + + + + + + Child block + + + + + + + @@ -252,56 +250,52 @@ exports[`suggestion mode: add nested bullet list to empty doc 3`] = ` - - - - - + + + + + + + + Level 1 + + + + - - Level 1 - + + + + + Level 2 + + + + - - - - - - - - - Level 2 - - - - - - - - - - - - - + + + + + @@ -609,6 +603,7 @@ exports[`suggestion mode: delete nested block 2`] = ` " Parent + " `; @@ -616,61 +611,28 @@ exports[`suggestion mode: delete nested block 2`] = ` exports[`suggestion mode: delete nested block 3`] = ` " - - - - - - - Parent - - - - - + + Parent + + + + - - - - - Child - - - - + + Child + - - - - - - - - - - - - Parent - - - - - - + + + + + " `; @@ -736,6 +698,7 @@ exports[`suggestion mode: delete parent block (with children) 2`] = ` " + " `; @@ -743,59 +706,34 @@ exports[`suggestion mode: delete parent block (with children) 2`] = ` exports[`suggestion mode: delete parent block (with children) 3`] = ` " - - - + + + + Parent + + - - Parent - + + + + + Child + + + + - - - - - - - - - Child - - - - - - - - - - - - - - - - - - - - - - + + + " `; @@ -828,6 +766,7 @@ exports[`suggestion mode: delete parent with nested paragraph and image 2`] = ` " + " `; @@ -835,82 +774,57 @@ exports[`suggestion mode: delete parent with nested paragraph and image 2`] = ` exports[`suggestion mode: delete parent with nested paragraph and image 3`] = ` " - - - + + + + Parent + + - - Parent - + + + + + Nested paragraph + + + + - - - - - - - - Nested paragraph - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - + + + + " `; @@ -1057,27 +971,36 @@ exports[`suggestion mode: nest a bullet under an existing bullet 2`] = ` exports[`suggestion mode: nest a bullet under an existing bullet 3`] = ` " - - - - - - - Parent - - - - - - + + Parent + + + + + + + + Child + + + + + + + + - - - - - - - Parent - - - - - - - - - - - - Child - - - - - - - - - - - " `; diff --git a/tests/src/end-to-end/y-prosemirror/__snapshots__/moveBlocks.test.tsx.snap b/tests/src/end-to-end/y-prosemirror/__snapshots__/moveBlocks.test.tsx.snap index 70ce03a0f5..ac1f80dcfd 100644 --- a/tests/src/end-to-end/y-prosemirror/__snapshots__/moveBlocks.test.tsx.snap +++ b/tests/src/end-to-end/y-prosemirror/__snapshots__/moveBlocks.test.tsx.snap @@ -124,27 +124,25 @@ exports[`suggestion mode: move paragraph with children 3`] = ` - - - - - - - - - Child - - - - - - - - + + + + + + + + Child + + + + + + + @@ -165,27 +163,25 @@ exports[`suggestion mode: move paragraph with children 3`] = ` - - - - - - - - - Child - - - - - - - - + + + + + + + + Child + + + + + + + diff --git a/tests/src/end-to-end/y-prosemirror/__snapshots__/nesting.concurrent.test.tsx.snap b/tests/src/end-to-end/y-prosemirror/__snapshots__/nesting.concurrent.test.tsx.snap index 958d39c987..b0075bd1ef 100644 --- a/tests/src/end-to-end/y-prosemirror/__snapshots__/nesting.concurrent.test.tsx.snap +++ b/tests/src/end-to-end/y-prosemirror/__snapshots__/nesting.concurrent.test.tsx.snap @@ -56,37 +56,34 @@ exports[`concurrent: A indents N1, B indents N2 below N1 4`] = ` - - N1 - - - N2 - - - " `; exports[`concurrent: A indents N1, B indents N2 below N1 5`] = ` " - - - - - - - N0 - - - - - - + + N0 + + + + + + + + N1 + + + + + + + + - - - - - - - N0 - - - - - - - - - - - - N1 - - - - - - - - - - - - - - - - - - N1 - - - - - - - - - - - - N2 - - - - - - - - - - - " `; @@ -244,9 +165,6 @@ exports[`concurrent: A nests N1 under N0, B nests N2 under N0 4`] = ` N1 - - - N0 N2 @@ -259,99 +177,45 @@ exports[`concurrent: A nests N1 under N0, B nests N2 under N0 4`] = ` exports[`concurrent: A nests N1 under N0, B nests N2 under N0 5`] = ` " - - - - - - - N0 - - - - - - - - - - - - - N0 - - - - - + + N0 + + + + - - - - - N1 - - - - + + N1 + - - - - - - - - - - - - N0 - - - - - + + + + + + - - - - - N2 - - - - + + N2 + - - - - - + + + + + " `; diff --git a/tests/src/end-to-end/y-prosemirror/__snapshots__/nesting.test.tsx.snap b/tests/src/end-to-end/y-prosemirror/__snapshots__/nesting.test.tsx.snap index 6e4bd785a6..7fccedff22 100644 --- a/tests/src/end-to-end/y-prosemirror/__snapshots__/nesting.test.tsx.snap +++ b/tests/src/end-to-end/y-prosemirror/__snapshots__/nesting.test.tsx.snap @@ -49,27 +49,25 @@ exports[`suggestion mode: change block type of a block with children 3`] = ` - - - - - - - - - N1 - - - - - - - - + + + + + + + + N1 + + + + + + + @@ -93,27 +91,25 @@ exports[`suggestion mode: change block type of a block with children 3`] = ` - - - - - - - - - N1 - - - - - - - - + + + + + + + + N1 + + + + + + + @@ -148,23 +144,28 @@ exports[`suggestion mode: indent a block 2`] = ` exports[`suggestion mode: indent a block 3`] = ` " - - - - - - - N0 - - - - - - + + N0 + + + + + + + + N1 + + + + + + + + - - - - - - - N0 - - - - - - - - - - - - N1 - - - - - - - - - - - " `; @@ -241,6 +204,7 @@ exports[`suggestion mode: unindent a block 2`] = ` " N0 + N1 @@ -251,61 +215,28 @@ exports[`suggestion mode: unindent a block 2`] = ` exports[`suggestion mode: unindent a block 3`] = ` " - - - - - - - N0 - - - - - + + N0 + + + + - - - - - N1 - - - - + + N1 + - - - - - - - - - - - - N0 - - - - - - + + + + + A", - "insert A", - "insert "C1" A", - "insert B", - "insert B", - "insert "D1" B", - "insert A", - "insert A", - "insert "C2" A", - "insert B", - "insert B", - "insert "D2" B", - "insert A", - "insert A", - "insert A", - "insert "A3" A", - "insert A", - "insert A", - "insert "B3" A", - "insert A", - "insert A", - "insert "C3" A", + "delete block "A1B1A2B2" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A1" B,A", + "delete B,A", + "delete B,A", + "delete "B1" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A2" B,A", + "delete B,A", + "delete B,A", + "delete "B2" B,A", + "insert block "A1B1C1D1A2B2C2D2A3B3C3" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert "D1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert "D2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", + "insert B,A", + "insert B,A", + "insert "C3" B,A", "insert ", "insert ", ] @@ -31,29 +63,61 @@ exports[`versioning diff: A adds column then row, B adds column 1`] = ` exports[`versioning diff: A adds row then column, B adds row 1`] = ` [ - "insert A", - "insert A", - "insert "C1" A", - "insert A", - "insert A", - "insert "C2" A", - "insert A", - "insert A", - "insert A", - "insert "A3" A", - "insert A", - "insert A", - "insert "B3" A", - "insert A", - "insert A", - "insert "C3" A", - "insert B", - "insert B", - "insert B", - "insert "D1" B", - "insert B", - "insert B", - "insert "D2" B", + "delete block "A1B1A2B2" B,A", + "delete
B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A1" B,A", + "delete B,A", + "delete B,A", + "delete "B1" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A2" B,A", + "delete B,A", + "delete B,A", + "delete "B2" B,A", + "insert block "A1B1C1A2B2C2A3B3C3D1D2" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", + "insert B,A", + "insert B,A", + "insert "C3" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "D1" B,A", + "insert B,A", + "insert B,A", + "insert "D2" B,A", "insert ", "insert ", ] @@ -81,11 +145,9 @@ exports[`versioning diff: Add a whole document 1`] = ` "format "Welcome to this " A", "insert "demo 🙌!" A", "format "demo 🙌!" A", - "insert A", "insert block "Hello World nested" A", "insert A", "insert "Hello World nested" A", - "insert A", "insert block "Hello World double nested" A", "insert A", "insert "Hello World double nested" A", @@ -133,7 +195,6 @@ exports[`versioning diff: Add a whole document 1`] = ` "insert block "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", "insert A", "insert "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", - "insert A", "insert block "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", "insert A", "insert "Bullet List Item. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." A", @@ -146,7 +207,6 @@ exports[`versioning diff: Add a whole document 1`] = ` "insert block "Numbered List Item 2" A", "insert A", "insert "Numbered List Item 2" A", - "insert A", "insert block "Numbered List Item Nested 1" A", "insert A", "insert "Numbered List Item Nested 1" A", @@ -177,7 +237,6 @@ exports[`versioning diff: Add a whole document 1`] = ` "insert block "Toggle List Item" A", "insert A", "insert "Toggle List Item" A", - "insert A", "insert block "Content nested inside the toggle list item." A", "insert A", "insert "Content nested inside the toggle list item." A", @@ -187,7 +246,6 @@ exports[`versioning diff: Add a whole document 1`] = ` "insert block "Toggle Heading" A", "insert A", "insert "Toggle Heading" A", - "insert A", "insert block "Content nested inside the toggle heading." A", "insert A", "insert "Content nested inside the toggle heading." A", @@ -395,7 +453,6 @@ exports[`versioning diff: Add colored block with child 1`] = ` "insert block "Colored parent" A", "insert A", "insert "Colored parent" A", - "insert A", "insert block "Child block" A", "insert A", "insert "Child block" A", @@ -415,19 +472,51 @@ exports[`versioning diff: Add column 1`] = ` exports[`versioning diff: Add column vs add row 1`] = ` [ - "insert A", - "insert A", - "insert "C1" A", - "insert A", - "insert A", - "insert "C2" A", - "insert B", - "insert B", - "insert B", - "insert "A3" B", - "insert B", - "insert B", - "insert "B3" B", + "delete block "A1B1A2B2" B,A", + "delete
B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A1" B,A", + "delete B,A", + "delete B,A", + "delete "B1" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A2" B,A", + "delete B,A", + "delete B,A", + "delete "B2" B,A", + "insert block "A1B1C1A2B2C2A3B3" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", "insert ", "insert ", ] @@ -454,11 +543,9 @@ exports[`versioning diff: Add nested bullets 1`] = ` "insert block "Level 0" A", "insert A", "insert "Level 0" A", - "insert A", "insert block "Level 1" A", "insert A", "insert "Level 1" A", - "insert A", "insert block "Level 2" A", "insert A", "insert "Level 2" A", @@ -496,19 +583,51 @@ exports[`versioning diff: Add row 1`] = ` exports[`versioning diff: Add row vs add column 1`] = ` [ - "insert B", - "insert B", - "insert "C1" B", - "insert B", - "insert B", - "insert "C2" B", - "insert A", - "insert A", - "insert A", - "insert "A3" A", - "insert A", - "insert A", - "insert "B3" A", + "delete block "A1B1A2B2" B,A", + "delete
B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A1" B,A", + "delete B,A", + "delete B,A", + "delete "B1" B,A", + "delete B,A", + "delete B,A", + "delete B,A", + "delete "A2" B,A", + "delete B,A", + "delete B,A", + "delete "B2" B,A", + "insert block "A1B1C1A2B2C2A3B3" B,A", + "insert
B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A1" B,A", + "insert B,A", + "insert B,A", + "insert "B1" B,A", + "insert B,A", + "insert B,A", + "insert "C1" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A2" B,A", + "insert B,A", + "insert B,A", + "insert "B2" B,A", + "insert B,A", + "insert B,A", + "insert "C2" B,A", + "insert B,A", + "insert B,A", + "insert B,A", + "insert "A3" B,A", + "insert B,A", + "insert B,A", + "insert "B3" B,A", "insert ", "insert ", ] @@ -522,18 +641,9 @@ exports[`versioning diff: Bold vs italic 1`] = ` exports[`versioning diff: Both nest a new block under N0 1`] = ` [ - "delete block "N0" A,B", - "insert block "N0" A", - "insert A", - "insert "N0" A", - "insert A", "insert block "N1" A", "insert A", "insert "N1" A", - "insert block "N0" B", - "insert B", - "insert "N0" B", - "insert B", "insert block "N2" B", "insert B", "insert "N2" B", @@ -542,23 +652,15 @@ exports[`versioning diff: Both nest a new block under N0 1`] = ` exports[`versioning diff: Cascading indents 1`] = ` [ - "delete block "N0" A", - "delete block "N1" A,B", - "insert block "N0" A", - "insert A", - "insert "N0" A", - "insert A", "insert block "N1" A", "insert A", "insert "N1" A", - "delete block "N2" B", - "insert block "N1" B", - "insert B", - "insert "N1" B", - "insert B", - "insert block "N2" B", - "insert B", - "insert "N2" B", + "delete block "N1" A", + "delete A", + "delete "N1" A", + "delete block "N2"", + "delete ", + "delete "N2"", ] `; @@ -571,10 +673,14 @@ exports[`versioning diff: Center-align 1`] = ` exports[`versioning diff: Change a parent's type vs edit its child 1`] = ` [ "delete block "Parent" A", + "delete A", + "delete "Parent" A", + "delete block "Child" A", + "delete A", + "delete "Child" A", "insert block "Parent" A", "insert A", "insert "Parent" A", - "insert A", "insert block "Child" A", "insert A", "insert "Child" A", @@ -590,10 +696,14 @@ exports[`versioning diff: Change image source 1`] = ` exports[`versioning diff: Change type of a parent block 1`] = ` [ "delete block "N0" A", + "delete A", + "delete "N0" A", + "delete block "N1" A", + "delete A", + "delete "N1" A", "insert block "N0" A", "insert A", "insert "N0" A", - "insert A", "insert block "N1" A", "insert A", "insert "N1" A", @@ -617,6 +727,8 @@ exports[`versioning diff: Create two columns 1`] = ` exports[`versioning diff: Delete a code block 1`] = ` [ "delete block "const x = 1;" A", + "delete A", + "delete "const x = 1;" A", "insert block "" A", "insert A", ] @@ -625,6 +737,7 @@ exports[`versioning diff: Delete a code block 1`] = ` exports[`versioning diff: Delete a divider 1`] = ` [ "delete block "" A", + "delete A", "insert block "" A", "insert A", ] @@ -632,31 +745,41 @@ exports[`versioning diff: Delete a divider 1`] = ` exports[`versioning diff: Delete a nested block 1`] = ` [ - "delete block "Parent" A", - "insert block "Parent" A", - "insert A", - "insert "Parent" A", + "delete block "Child" A", + "delete A", + "delete "Child" A", ] `; exports[`versioning diff: Delete a parent block 1`] = ` [ - "delete block "Parent" A", - "insert block "" A", - "insert A", + "attrs block "Parent" id:A", + "delete "Parent" A", + "delete block "Child" A", + "delete A", + "delete "Child" A", ] `; exports[`versioning diff: Delete a parent vs change its child's type 1`] = ` [ "delete block "Parent" A", + "delete A", + "delete "Parent" A", + "delete block "Child"", + "delete ", + "delete "Child"", ] `; exports[`versioning diff: Delete column vs add row 1`] = ` [ "delete A", + "delete A", + "delete "B1" A", "delete A", + "delete A", + "delete "B2" A", "insert B", "insert B", "insert B", @@ -670,6 +793,7 @@ exports[`versioning diff: Delete column vs add row 1`] = ` exports[`versioning diff: Delete image 1`] = ` [ "delete block "" A", + "delete A", "insert block "" A", "insert A", ] @@ -678,23 +802,52 @@ exports[`versioning diff: Delete image 1`] = ` exports[`versioning diff: Delete one of two empty blocks 1`] = ` [ "delete block "" A", + "delete A", ] `; exports[`versioning diff: Delete parent with mixed children 1`] = ` [ - "delete block "Parent" A", - "insert block "" A", - "insert A", + "attrs block "Parent" id:A", + "delete "Parent" A", + "delete block "Nested paragraph" A", + "delete A", + "delete "Nested paragraph" A", + "delete block "" A", + "delete A", ] `; exports[`versioning diff: Delete row vs add column 1`] = ` [ + "delete block "A1B1A2B2" A", + "delete
A", + "delete A", + "delete A", + "delete A", + "delete "A1" A", + "delete A", + "delete A", + "delete "B1" A", + "delete A", + "delete A", + "delete A", + "delete "A2" A", + "delete A", + "delete A", + "delete "B2" A", + "insert block "A1B1C1" B", + "insert
B", + "insert B", + "insert B", + "insert B", + "insert "A1" B", + "insert B", + "insert B", + "insert "B1" B", "insert B", "insert B", "insert "C1" B", - "delete A", ] `; @@ -732,6 +885,11 @@ exports[`versioning diff: Edit a link 1`] = ` exports[`versioning diff: Edit text vs change to heading 1`] = ` [ "delete block "hello world" B", + "delete B", + "delete "hello " B", + "delete "wo" B,A", + "delete "r" B", + "delete "ld" B,A", "insert block "hello world" B", "insert B", "insert "hello world" B", @@ -760,6 +918,8 @@ exports[`versioning diff: Fix typo vs delete word 1`] = ` exports[`versioning diff: Heading vs list item 1`] = ` [ "delete block "hello world" A,B", + "delete A,B", + "delete "hello world" A,B", "insert block "hello world" A", "insert A", "insert "hello world" A", @@ -778,15 +938,12 @@ exports[`versioning diff: Highlight a column 1`] = ` exports[`versioning diff: Indent a block 1`] = ` [ - "delete block "N0" A", - "delete block "N1" A", - "insert block "N0" A", - "insert A", - "insert "N0" A", - "insert A", "insert block "N1" A", "insert A", "insert "N1" A", + "delete block "N1" A", + "delete A", + "delete "N1" A", ] `; @@ -808,6 +965,8 @@ exports[`versioning diff: Insert an image 1`] = ` exports[`versioning diff: List item → paragraph 1`] = ` [ "delete block "hello world" A", + "delete A", + "delete "hello world" A", "insert block "hello world" A", "insert A", "insert "hello world" A", @@ -818,6 +977,8 @@ exports[`versioning diff: Merge blocks vs edit block B 1`] = ` [ "insert "Second" A", "delete block "Second" A", + "delete A", + "delete "Second" A", ] `; @@ -826,6 +987,8 @@ exports[`versioning diff: Merge cells 1`] = ` "attrs colspan:A colwidth:A", "insert "+B1" A", "delete A", + "delete A", + "delete "B1" A", "insert ", "insert ", ] @@ -834,7 +997,14 @@ exports[`versioning diff: Merge cells 1`] = ` exports[`versioning diff: Move a block into a block that is deleted 1`] = ` [ "delete block "Parent" A", - "delete block "X" B", + "delete A", + "delete "Parent" A", + "delete block "Child" A", + "delete A", + "delete "Child" A", + "delete block "X"", + "delete ", + "delete "X"", ] `; @@ -844,6 +1014,8 @@ exports[`versioning diff: Move paragraph up 1`] = ` "insert A", "insert "Middle" A", "delete block "Middle" A", + "delete A", + "delete "Middle" A", ] `; @@ -852,61 +1024,54 @@ exports[`versioning diff: Move paragraph with children 1`] = ` "insert block "Parent" A", "insert A", "insert "Parent" A", - "insert A", "insert block "Child" A", "insert A", "insert "Child" A", "delete block "Parent" A", + "delete A", + "delete "Parent" A", + "delete block "Child" A", + "delete A", + "delete "Child" A", ] `; exports[`versioning diff: Nest a bullet under another 1`] = ` [ - "delete block "Parent" A", - "delete block "Child" A", - "insert block "Parent" A", - "insert A", - "insert "Parent" A", - "insert A", "insert block "Child" A", "insert A", "insert "Child" A", + "delete block "Child" A", + "delete A", + "delete "Child" A", ] `; exports[`versioning diff: Nest blocks into a block that is moved 1`] = ` [ - "delete block "R" B", - "delete block "Q" B,A", - "insert block "R" B", - "insert B", - "insert "R" B", - "insert B", "insert block "Q" B", "insert B", "insert "Q" B", - "delete block "B1" A", - "delete block "B2" A", - "delete block "B3" A", - "insert block "Q" A", - "insert A", - "insert "Q" A", - "insert A", - "insert block "B1" A", - "insert A", - "insert "B1" A", - "insert block "B2" A", - "insert A", - "insert "B2" A", - "insert block "B3" A", - "insert A", - "insert "B3" A", + "delete block "Q" B", + "delete B", + "delete "Q" B", + "delete block "B1"", + "delete ", + "delete "B1"", + "delete block "B2"", + "delete ", + "delete "B2"", + "delete block "B3"", + "delete ", + "delete "B3"", ] `; exports[`versioning diff: Paragraph → heading 1`] = ` [ "delete block "hello world" A", + "delete A", + "delete "hello world" A", "insert block "hello world" A", "insert A", "insert "hello world" A", @@ -916,6 +1081,14 @@ exports[`versioning diff: Paragraph → heading 1`] = ` exports[`versioning diff: Remove a column 1`] = ` [ "delete A", + "delete A", + "delete block "Left column" A", + "delete A", + "delete "Left column" A", + "delete A", + "delete block "Right column" A", + "delete A", + "delete "Right column" A", "insert block "Left column" A", "insert A", "insert "Left column" A", @@ -931,12 +1104,17 @@ exports[`versioning diff: Remove a link 1`] = ` exports[`versioning diff: Remove a middle column 1`] = ` [ "delete A", + "delete block "Right column" A", + "delete A", + "delete "Right column" A", ] `; exports[`versioning diff: Remove a paragraph 1`] = ` [ "delete block "Body text" A", + "delete A", + "delete "Body text" A", ] `; @@ -949,13 +1127,23 @@ exports[`versioning diff: Remove bold 1`] = ` exports[`versioning diff: Remove column 1`] = ` [ "delete A", + "delete A", + "delete "B1" A", "delete A", + "delete A", + "delete "B2" A", ] `; exports[`versioning diff: Remove row 1`] = ` [ "delete A", + "delete A", + "delete A", + "delete "A2" A", + "delete A", + "delete A", + "delete "B2" A", ] `; @@ -1009,10 +1197,9 @@ exports[`versioning diff: Text color vs background color 1`] = ` exports[`versioning diff: Unindent a block 1`] = ` [ - "delete block "N0" A", - "insert block "N0" A", - "insert A", - "insert "N0" A", + "delete block "N1" A", + "delete A", + "delete "N1" A", "insert block "N1" A", "insert A", "insert "N1" A",