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',
index: 2,
after: 'paragraph-1',
block: {
id: 'paragraph-3',
type: 'affine:paragraph',

View File

@@ -334,4 +334,58 @@ Inserted at tail.
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',
index: 1,
after: 'block-001',
block: {
id: 'block-002',
type: 'paragraph',
@@ -104,6 +105,7 @@ New paragraph.
{
op: 'insert',
index: 2,
after: 'block-002',
block: {
id: 'block-004',
type: 'paragraph',
@@ -138,6 +140,7 @@ Second inserted paragraph.
{
op: 'insert',
index: 1,
after: 'block-001',
block: {
id: 'block-002',
type: 'paragraph',
@@ -147,6 +150,7 @@ Second inserted paragraph.
{
op: 'insert',
index: 2,
after: 'block-002',
block: {
id: 'block-003',
type: 'paragraph',
@@ -213,6 +217,7 @@ This is a new paragraph inserted after deletion.
{
op: 'insert',
index: 2,
after: 'block-003',
block: {
id: 'block-004',
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()) {
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;
}
await applyPatchToDoc(doc, [
{ op: 'insert', index: baseIndex + payload.offset, block },
{
op: 'insert',
index: baseIndex + payload.offset,
after: payload.from,
block,
},
]);
break;
}

View File

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

View File

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