fix(core): insert diff not displayed after the expected block (#13086)

> CLOSE AI-319

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Improved block insertion behavior by specifying the reference block
after which new blocks are inserted.
  
* **Bug Fixes**
* Enhanced accuracy and clarity of block diffing and patch application,
ensuring correct handling of insertions and deletions.

* **Tests**
* Added and updated test cases to verify correct handling of interval
insertions, deletions, and complete block replacements.
* Updated test expectations to include explicit insertion context for
greater consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
德布劳外 · 贾贵
2025-07-08 16:46:17 +08:00
committed by GitHub
parent f6a45ae20b
commit 8c49a45162
6 changed files with 225 additions and 39 deletions

View File

@@ -87,6 +87,7 @@ describe('applyPatchToDoc', () => {
{ {
op: 'insert', op: 'insert',
index: 2, index: 2,
after: 'paragraph-1',
block: { block: {
id: 'paragraph-3', id: 'paragraph-3',
type: 'affine:paragraph', type: 'affine:paragraph',

View File

@@ -334,4 +334,58 @@ Inserted at tail.
updates: {}, updates: {},
}); });
}); });
test('should handle interval insertions & deletions', () => {
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# 1
<!-- block_id=block-002 flavour=paragraph -->
2
<!-- block_id=block-003 flavour=paragraph -->
3
<!-- block_id=block-004 flavour=paragraph -->
4
<!-- block_id=block-005 flavour=paragraph -->
5
`;
const newMd = `
<!-- block_id=block-001 flavour=title -->
# 1
<!-- block_id=block-002 flavour=paragraph -->
2
<!-- block_id=block-004 flavour=paragraph -->
4
<!-- block_id=block-006 flavour=paragraph -->
6
<!-- block_id=block-007 flavour=paragraph -->
7
`;
const diff = generateRenderDiff(oldMd, newMd);
expect(diff).toEqual({
deletes: ['block-003', 'block-005'],
inserts: {
'block-004': [
{
id: 'block-006',
type: 'paragraph',
content: '6',
},
{
id: 'block-007',
type: 'paragraph',
content: '7',
},
],
},
updates: {},
});
});
}); });

View File

@@ -21,6 +21,7 @@ This is a new paragraph.
{ {
op: 'insert', op: 'insert',
index: 1, index: 1,
after: 'block-001',
block: { block: {
id: 'block-002', id: 'block-002',
type: 'paragraph', type: 'paragraph',
@@ -104,6 +105,7 @@ New paragraph.
{ {
op: 'insert', op: 'insert',
index: 2, index: 2,
after: 'block-002',
block: { block: {
id: 'block-004', id: 'block-004',
type: 'paragraph', type: 'paragraph',
@@ -138,6 +140,7 @@ Second inserted paragraph.
{ {
op: 'insert', op: 'insert',
index: 1, index: 1,
after: 'block-001',
block: { block: {
id: 'block-002', id: 'block-002',
type: 'paragraph', type: 'paragraph',
@@ -147,6 +150,7 @@ Second inserted paragraph.
{ {
op: 'insert', op: 'insert',
index: 2, index: 2,
after: 'block-002',
block: { block: {
id: 'block-003', id: 'block-003',
type: 'paragraph', type: 'paragraph',
@@ -213,6 +217,7 @@ This is a new paragraph inserted after deletion.
{ {
op: 'insert', op: 'insert',
index: 2, index: 2,
after: 'block-003',
block: { block: {
id: 'block-004', id: 'block-004',
type: 'paragraph', type: 'paragraph',
@@ -225,4 +230,133 @@ This is a new paragraph inserted after deletion.
}, },
]); ]);
}); });
test('should diff interval insertions & deletions', () => {
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# 1
<!-- block_id=block-002 flavour=paragraph -->
2
<!-- block_id=block-003 flavour=paragraph -->
3
<!-- block_id=block-004 flavour=paragraph -->
4
<!-- block_id=block-005 flavour=paragraph -->
5
`;
const newMd = `
<!-- block_id=block-001 flavour=title -->
# 1
<!-- block_id=block-002 flavour=paragraph -->
2
<!-- block_id=block-004 flavour=paragraph -->
4
<!-- block_id=block-006 flavour=paragraph -->
6
<!-- block_id=block-007 flavour=paragraph -->
7
`;
const { patches } = diffMarkdown(oldMd, newMd);
expect(patches).toEqual([
{
op: 'insert',
index: 3,
after: 'block-004',
block: {
id: 'block-006',
type: 'paragraph',
content: '6',
},
},
{
op: 'insert',
index: 4,
after: 'block-006',
block: {
id: 'block-007',
type: 'paragraph',
content: '7',
},
},
{
op: 'delete',
id: 'block-003',
},
{
op: 'delete',
id: 'block-005',
},
]);
});
test('should diff insertions after remove all', () => {
const oldMd = `
<!-- block_id=block-001 flavour=title -->
# 1
<!-- block_id=block-002 flavour=paragraph -->
2
`;
const newMd = `
<!-- block_id=block-003 flavour=paragraph -->
3
<!-- block_id=block-004 flavour=paragraph -->
4
<!-- block_id=block-005 flavour=paragraph -->
5
`;
const { patches } = diffMarkdown(oldMd, newMd);
expect(patches).toEqual([
{
op: 'insert',
index: 0,
after: 'HEAD',
block: {
id: 'block-003',
type: 'paragraph',
content: '3',
},
},
{
op: 'insert',
index: 1,
after: 'block-003',
block: {
id: 'block-004',
type: 'paragraph',
content: '4',
},
},
{
op: 'insert',
index: 2,
after: 'block-004',
block: {
id: 'block-005',
type: 'paragraph',
content: '5',
},
},
{
op: 'delete',
id: 'block-001',
},
{
op: 'delete',
id: 'block-002',
},
]);
});
}); });

View File

@@ -264,7 +264,7 @@ export class BlockDiffService extends Extension implements BlockDiffProvider {
} }
for (const [offset, block] of blocks.entries()) { for (const [offset, block] of blocks.entries()) {
await applyPatchToDoc(doc, [ await applyPatchToDoc(doc, [
{ op: 'insert', index: baseIndex + offset, block }, { op: 'insert', index: baseIndex + offset, after: from, block },
]); ]);
} }
} }
@@ -301,7 +301,12 @@ export class BlockDiffService extends Extension implements BlockDiffProvider {
baseIndex = this.getBlockIndexById(doc, payload.from) + 1; baseIndex = this.getBlockIndexById(doc, payload.from) + 1;
} }
await applyPatchToDoc(doc, [ await applyPatchToDoc(doc, [
{ op: 'insert', index: baseIndex + payload.offset, block }, {
op: 'insert',
index: baseIndex + payload.offset,
after: payload.from,
block,
},
]); ]);
break; break;
} }

View File

@@ -22,50 +22,57 @@ export interface RenderDiffs {
* *
* <!-- block_id=004 flavour=paragraph --> * <!-- block_id=004 flavour=paragraph -->
* This is the fourth paragraph * This is the fourth paragraph
*
* <!-- block_id=005 flavour=paragraph -->
* This is the fifth paragraph
* ``` * ```
* *
* New markdown: * New markdown:
* ```md * ```md
* <!-- block_id=001 flavour=paragraph --> * <!-- block_id=001 flavour=paragraph -->
* This is the first paragraph * This is the 1st paragraph
* *
* <!-- block_id=003 flavour=paragraph --> * <!-- block_id=002 flavour=paragraph -->
* This is the 3rd paragraph * This is the second paragraph
* *
* <!-- block_id=005 flavour=paragraph --> * <!-- block_id=004 flavour=paragraph -->
* New inserted paragraph 1 * This is the fourth paragraph
* *
* <!-- block_id=006 flavour=paragraph --> * <!-- block_id=006 flavour=paragraph -->
* New inserted paragraph 1
*
* <!-- block_id=007 flavour=paragraph -->
* New inserted paragraph 2 * New inserted paragraph 2
* ``` * ```
* *
* The generated patches: * The generated patches:
* ```js * ```js
* [ * [
* { op: 'insert', index: 2, block: { id: '005', ... } }, * { op: 'insert', index: 3, after: '004', block: { id: '006', ... } },
* { op: 'insert', index: 3, bthirdlock: { id: '006', ... } }, * { op: 'insert', index: 4, after: '006', block: { id: '007', ... } },
* { op: 'update', id: '003', content: 'This is the 3rd paragraph' }, * { op: 'update', id: '001', content: 'This is the 1st paragraph' },
* { op: 'delete', id: '002' }, * { op: 'delete', id: '003' },
* { op: 'delete', id: '004' } * { op: 'delete', id: '005' }
* ] * ]
* ``` * ```
* *
* UI expected: * UI expected:
* ``` * ```
* This is the first paragraph * [UPDATE DIFF]This is the first paragraph
* [DELETE DIFF] This is the second paragraph * This is the second paragraph
* This is the third paragraph * [DELETE DIFF]This is the third paragraph
* [DELETE DIFF] This is the fourth paragraph * This is the fourth paragraph
* [INSERT DIFF] New inserted paragraph 1 * [INSERT DIFF] New inserted paragraph 1
* [INSERT DIFF] New inserted paragraph 2 * [INSERT DIFF] New inserted paragraph 2
* [DELETE DIFF] This is the fifth paragraph
* ``` * ```
* *
* The resulting diffMap: * The resulting diffMap:
* ```js * ```js
* { * {
* deletes: ['002', '004'], * deletes: ['003', '005'],
* inserts: { 3: [block_005, block_006] }, * inserts: { '004': [block_006, block_007] },
* updates: {} * updates: { '001': 'This is the 1st paragraph' }
* } * }
* ``` * ```
*/ */
@@ -73,10 +80,7 @@ export function generateRenderDiff(
originalMarkdown: string, originalMarkdown: string,
changedMarkdown: string changedMarkdown: string
) { ) {
const { patches, oldBlocks } = diffMarkdown( const { patches } = diffMarkdown(originalMarkdown, changedMarkdown);
originalMarkdown,
changedMarkdown
);
const diffMap: RenderDiffs = { const diffMap: RenderDiffs = {
deletes: [], deletes: [],
@@ -84,19 +88,6 @@ export function generateRenderDiff(
updates: {}, updates: {},
}; };
const indexToBlockId: Record<number, string> = {};
oldBlocks.forEach((block, idx) => {
indexToBlockId[idx] = block.id;
});
function getPrevBlock(index: number) {
let start = index - 1;
while (!indexToBlockId[start] && start >= 0) {
start--;
}
return indexToBlockId[start] || 'HEAD';
}
const insertGroups: Record<string, Block[]> = {}; const insertGroups: Record<string, Block[]> = {};
let lastInsertKey: string | null = null; let lastInsertKey: string | null = null;
let lastInsertIndex: number | null = null; let lastInsertIndex: number | null = null;
@@ -107,7 +98,7 @@ export function generateRenderDiff(
diffMap.deletes.push(patch.id); diffMap.deletes.push(patch.id);
break; break;
case 'insert': { case 'insert': {
const prevBlockId = getPrevBlock(patch.index); const prevBlockId = patch.after;
if ( if (
lastInsertKey !== null && lastInsertKey !== null &&
lastInsertIndex !== null && lastInsertIndex !== null &&

View File

@@ -7,7 +7,7 @@ export type Block = {
export type PatchOp = export type PatchOp =
| { op: 'replace'; id: string; content: string } | { op: 'replace'; id: string; content: string }
| { op: 'delete'; id: string } | { op: 'delete'; id: string }
| { op: 'insert'; index: number; block: Block }; | { op: 'insert'; index: number; after: string; block: Block };
const BLOCK_MATCH_REGEXP = /^\s*<!--\s*block_id=(.*?)\s+flavour=(.*?)\s*-->/; const BLOCK_MATCH_REGEXP = /^\s*<!--\s*block_id=(.*?)\s+flavour=(.*?)\s*-->/;
@@ -61,7 +61,6 @@ function diffBlockLists(oldBlocks: Block[], newBlocks: Block[]): PatchOp[] {
// Mark old blocks that have been handled // Mark old blocks that have been handled
const handledOld = new Set<string>(); const handledOld = new Set<string>();
// First process newBlocks in order
newBlocks.forEach((newBlock, newIdx) => { newBlocks.forEach((newBlock, newIdx) => {
const old = oldMap.get(newBlock.id); const old = oldMap.get(newBlock.id);
if (old) { if (old) {
@@ -74,9 +73,11 @@ function diffBlockLists(oldBlocks: Block[], newBlocks: Block[]): PatchOp[] {
}); });
} }
} else { } else {
const after = newIdx > 0 ? newBlocks[newIdx - 1].id : 'HEAD';
patch.push({ patch.push({
op: 'insert', op: 'insert',
index: newIdx, index: newIdx,
after,
block: { block: {
id: newBlock.id, id: newBlock.id,
type: newBlock.type, type: newBlock.type,