milkdown-inline-diff 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +207 -0
- package/dist/diff-config.d.ts +11 -0
- package/dist/diff-config.d.ts.map +1 -0
- package/dist/diff-tooltip.d.ts +37 -0
- package/dist/diff-tooltip.d.ts.map +1 -0
- package/dist/diffDecorationState.d.ts +7 -0
- package/dist/diffDecorationState.d.ts.map +1 -0
- package/dist/extended-table-schema.d.ts +2 -0
- package/dist/extended-table-schema.d.ts.map +1 -0
- package/dist/index.cjs +75 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26590 -0
- package/dist/markdown-diff.d.ts +52 -0
- package/dist/markdown-diff.d.ts.map +1 -0
- package/dist/myers-diff.d.ts +7 -0
- package/dist/myers-diff.d.ts.map +1 -0
- package/package.json +57 -0
- package/src/diff-config.ts +28 -0
- package/src/diff-tooltip.ts +302 -0
- package/src/diffDecorationState.ts +48 -0
- package/src/extended-table-schema.ts +11 -0
- package/src/index.ts +43 -0
- package/src/markdown-diff.ts +971 -0
- package/src/myers-diff.ts +134 -0
- package/src/style.css +152 -0
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
import { Node, Fragment, Schema } from "@milkdown/prose/model";
|
|
2
|
+
import { Decoration, DecorationSet } from "@milkdown/prose/view";
|
|
3
|
+
import { myersDiff } from "./myers-diff";
|
|
4
|
+
import { Ctx } from "@milkdown/ctx";
|
|
5
|
+
import { diffDecorationState } from "./diffDecorationState";
|
|
6
|
+
import { editorViewCtx, parserCtx } from "@milkdown/core";
|
|
7
|
+
import { EditorState, TextSelection } from "@milkdown/prose/state";
|
|
8
|
+
export type ChangeType = "insert" | "delete" | "unchanged";
|
|
9
|
+
|
|
10
|
+
export interface DiffState {
|
|
11
|
+
currentIndex: number;
|
|
12
|
+
count: number;
|
|
13
|
+
}
|
|
14
|
+
interface ExtractedBlock {
|
|
15
|
+
nodes: Node[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const LEAF_BLOCK_TYPES: readonly string[] = [
|
|
19
|
+
"paragraph",
|
|
20
|
+
"heading",
|
|
21
|
+
"code_block",
|
|
22
|
+
"hr",
|
|
23
|
+
"table_header_row",
|
|
24
|
+
"table_row",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function extractBlocks(doc: Node): ExtractedBlock[] {
|
|
28
|
+
const blocks: ExtractedBlock[] = [];
|
|
29
|
+
|
|
30
|
+
const extractFromNode = (node: Node, path: Node[]) => {
|
|
31
|
+
const typeName = node.type.name;
|
|
32
|
+
|
|
33
|
+
if (LEAF_BLOCK_TYPES.indexOf(typeName as any) >= 0) {
|
|
34
|
+
blocks.push({
|
|
35
|
+
nodes: [...path, node],
|
|
36
|
+
});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (node.childCount > 0) {
|
|
41
|
+
node.forEach((child: Node) => {
|
|
42
|
+
extractFromNode(child, [...path, node]);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
doc.forEach((child: Node) => {
|
|
48
|
+
extractFromNode(child, [doc]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return blocks;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function nodesEqual(a: Node, b: Node): boolean {
|
|
55
|
+
if (a.type.name !== b.type.name) return false;
|
|
56
|
+
|
|
57
|
+
const aAttrs = a.attrs || {};
|
|
58
|
+
const bAttrs = b.attrs || {};
|
|
59
|
+
const aKeys = Object.keys(aAttrs).sort();
|
|
60
|
+
const bKeys = Object.keys(bAttrs).sort();
|
|
61
|
+
|
|
62
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
65
|
+
const key = aKeys[i];
|
|
66
|
+
if (key !== bKeys[i]) return false;
|
|
67
|
+
if (aAttrs[key] !== bAttrs[key]) return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const aMarks = a.marks || [];
|
|
71
|
+
const bMarks = b.marks || [];
|
|
72
|
+
|
|
73
|
+
if (aMarks.length !== bMarks.length) return false;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < aMarks.length; i++) {
|
|
76
|
+
const aMark = aMarks[i];
|
|
77
|
+
const bMark = bMarks[i];
|
|
78
|
+
|
|
79
|
+
if (aMark.type.name !== bMark.type.name) return false;
|
|
80
|
+
|
|
81
|
+
const aMarkAttrs = aMark.attrs || {};
|
|
82
|
+
const bMarkAttrs = bMark.attrs || {};
|
|
83
|
+
const aMarkKeys = Object.keys(aMarkAttrs).sort();
|
|
84
|
+
const bMarkKeys = Object.keys(bMarkAttrs).sort();
|
|
85
|
+
|
|
86
|
+
if (aMarkKeys.length !== bMarkKeys.length) return false;
|
|
87
|
+
|
|
88
|
+
for (let j = 0; j < aMarkKeys.length; j++) {
|
|
89
|
+
const key = aMarkKeys[j];
|
|
90
|
+
if (key !== bMarkKeys[j]) return false;
|
|
91
|
+
if (aMarkAttrs[key] !== bMarkAttrs[key]) return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function childrenEqual(a: Node, b: Node): boolean {
|
|
99
|
+
if (a.childCount !== b.childCount) return false;
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < a.childCount; i++) {
|
|
102
|
+
const aChild = a.child(i);
|
|
103
|
+
const bChild = b.child(i);
|
|
104
|
+
|
|
105
|
+
if (!nodesEqual(aChild, bChild)) return false;
|
|
106
|
+
|
|
107
|
+
if (aChild.isText && bChild.isText) {
|
|
108
|
+
if (aChild.text !== bChild.text) return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (aChild.childCount > 0 || bChild.childCount > 0) {
|
|
112
|
+
if (!childrenEqual(aChild, bChild)) return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function blockEqual(a: ExtractedBlock, b: ExtractedBlock): boolean {
|
|
120
|
+
if (a.nodes.length !== b.nodes.length) return false;
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < a.nodes.length; i++) {
|
|
123
|
+
if (!nodesEqual(a.nodes[i], b.nodes[i])) return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const aLeaf = a.nodes[a.nodes.length - 1];
|
|
127
|
+
const bLeaf = b.nodes[b.nodes.length - 1];
|
|
128
|
+
|
|
129
|
+
return childrenEqual(aLeaf, bLeaf);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function deepEquals(node: Node, other: Node): boolean {
|
|
133
|
+
return nodesEqual(node, other) && childrenEqual(node, other);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface BlockChange {
|
|
137
|
+
type: ChangeType;
|
|
138
|
+
A: ExtractedBlock | null;
|
|
139
|
+
B: ExtractedBlock | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function blockDiff(docA: Node, docB: Node): BlockChange[] {
|
|
143
|
+
const blocksA = extractBlocks(docA);
|
|
144
|
+
const blocksB = extractBlocks(docB);
|
|
145
|
+
|
|
146
|
+
let changes: BlockChange[] = [];
|
|
147
|
+
|
|
148
|
+
const diffResult = myersDiff(blocksA, blocksB, blockEqual);
|
|
149
|
+
|
|
150
|
+
for (const op of diffResult) {
|
|
151
|
+
switch (op.type) {
|
|
152
|
+
case "equal":
|
|
153
|
+
for (let i = 0; i < op.items.length; i++) {
|
|
154
|
+
const change: BlockChange = {
|
|
155
|
+
type: "unchanged",
|
|
156
|
+
A: op.items[i],
|
|
157
|
+
B: op.items[i],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
changes.push(change);
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
case "delete":
|
|
164
|
+
for (const block of op.items) {
|
|
165
|
+
const change: BlockChange = {
|
|
166
|
+
type: "delete",
|
|
167
|
+
A: block,
|
|
168
|
+
B: null,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
changes.push(change);
|
|
172
|
+
}
|
|
173
|
+
break;
|
|
174
|
+
case "insert":
|
|
175
|
+
for (const block of op.items) {
|
|
176
|
+
const change: BlockChange = {
|
|
177
|
+
type: "insert",
|
|
178
|
+
A: null,
|
|
179
|
+
B: block,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
changes.push(change);
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
changes = sortTableRows(changes);
|
|
189
|
+
return changes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isTableRowChange(change: BlockChange): boolean {
|
|
193
|
+
const nodeA = change.A?.nodes[change.A.nodes.length - 1];
|
|
194
|
+
const nodeB = change.B?.nodes[change.B.nodes.length - 1];
|
|
195
|
+
const node = nodeA ?? nodeB;
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
node?.type.name === "table_row" || node?.type.name === "table_header_row"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isTableHeaderRow(change: BlockChange): boolean {
|
|
203
|
+
const node =
|
|
204
|
+
change.A?.nodes[change.A.nodes.length - 1] ??
|
|
205
|
+
change.B?.nodes[change.B.nodes.length - 1];
|
|
206
|
+
return node?.type.name === "table_header_row";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getTableRowColumnCount(change: BlockChange): number {
|
|
210
|
+
const node =
|
|
211
|
+
change.A?.nodes[change.A.nodes.length - 1] ??
|
|
212
|
+
change.B?.nodes[change.B.nodes.length - 1];
|
|
213
|
+
return node?.childCount ?? 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getParentPath(change: BlockChange): Node[] | null {
|
|
217
|
+
const nodes = change.A?.nodes ?? change.B?.nodes;
|
|
218
|
+
if (!nodes || nodes.length < 2) return null;
|
|
219
|
+
return nodes.slice(0, -1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sameParent(a: BlockChange, b: BlockChange): boolean {
|
|
223
|
+
const pathA = getParentPath(a);
|
|
224
|
+
const pathB = getParentPath(b);
|
|
225
|
+
if (!pathA || !pathB) return false;
|
|
226
|
+
if (pathA.length !== pathB.length) return false;
|
|
227
|
+
return pathA.every((node, i) => nodesEqual(node, pathB[i]));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function sortByColumnCount(group: BlockChange[]): BlockChange[] {
|
|
231
|
+
const withIndex = group.map((change, index) => ({
|
|
232
|
+
change,
|
|
233
|
+
columnCount: getTableRowColumnCount(change),
|
|
234
|
+
originalIndex: index,
|
|
235
|
+
isHeader: isTableHeaderRow(change),
|
|
236
|
+
}));
|
|
237
|
+
|
|
238
|
+
const firstAppearanceOrder = new Map<number, number>();
|
|
239
|
+
let order = 0;
|
|
240
|
+
for (const item of withIndex) {
|
|
241
|
+
if (!firstAppearanceOrder.has(item.columnCount)) {
|
|
242
|
+
firstAppearanceOrder.set(item.columnCount, order++);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
withIndex.sort((a, b) => {
|
|
247
|
+
const orderA = firstAppearanceOrder.get(a.columnCount)!;
|
|
248
|
+
const orderB = firstAppearanceOrder.get(b.columnCount)!;
|
|
249
|
+
if (orderA !== orderB) {
|
|
250
|
+
return orderA - orderB;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (a.isHeader !== b.isHeader) {
|
|
254
|
+
return a.isHeader ? -1 : 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return a.originalIndex - b.originalIndex;
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return withIndex.map((item) => item.change);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sortTableRows(changes: BlockChange[]): BlockChange[] {
|
|
264
|
+
const result: BlockChange[] = [];
|
|
265
|
+
let i = 0;
|
|
266
|
+
|
|
267
|
+
while (i < changes.length) {
|
|
268
|
+
const current = changes[i];
|
|
269
|
+
|
|
270
|
+
if (!isTableRowChange(current)) {
|
|
271
|
+
result.push(current);
|
|
272
|
+
i++;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const group: BlockChange[] = [current];
|
|
277
|
+
let j = i + 1;
|
|
278
|
+
|
|
279
|
+
while (j < changes.length) {
|
|
280
|
+
const next = changes[j];
|
|
281
|
+
if (!isTableRowChange(next) || !sameParent(current, next)) {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
group.push(next);
|
|
285
|
+
j++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const sortedGroup = sortByColumnCount(group);
|
|
289
|
+
|
|
290
|
+
result.push(...sortedGroup);
|
|
291
|
+
|
|
292
|
+
i = j;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface DiffEditState {
|
|
299
|
+
mergedDoc: Node;
|
|
300
|
+
decorations: DecorationSet;
|
|
301
|
+
mergeGroups: VNode[][];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export interface DiffDecoration {
|
|
305
|
+
groupIndex: string;
|
|
306
|
+
delete: Decoration[];
|
|
307
|
+
insert: Decoration[];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export interface VNode {
|
|
311
|
+
node: Node;
|
|
312
|
+
children: VNode[];
|
|
313
|
+
changeType: ChangeType;
|
|
314
|
+
newNode?: Node;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function getDiffDecorations(ctx: Ctx): DiffDecoration[] {
|
|
318
|
+
const decorations = ctx.get(diffDecorationState.key).decorations;
|
|
319
|
+
if (!decorations) {
|
|
320
|
+
return [];
|
|
321
|
+
}
|
|
322
|
+
const result: DiffDecoration[] = [];
|
|
323
|
+
const found = decorations.find(
|
|
324
|
+
0,
|
|
325
|
+
undefined,
|
|
326
|
+
(spec) => (spec as any).decorationType === "diff",
|
|
327
|
+
);
|
|
328
|
+
const filtered = found.filter((deco): deco is Decoration => deco !== null);
|
|
329
|
+
|
|
330
|
+
for (const deco of filtered) {
|
|
331
|
+
const spec = deco.spec as any;
|
|
332
|
+
const groupIndex = spec.groupIndex;
|
|
333
|
+
const changeType = spec.changeType;
|
|
334
|
+
|
|
335
|
+
let group = result.find((g) => g.groupIndex === groupIndex);
|
|
336
|
+
if (!group) {
|
|
337
|
+
group = { groupIndex, delete: [], insert: [] };
|
|
338
|
+
result.push(group);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (changeType === "delete") {
|
|
342
|
+
group.delete.push(deco);
|
|
343
|
+
} else if (changeType === "insert") {
|
|
344
|
+
group.insert.push(deco);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return result;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function findNodePosition(doc: Node, targetNode: Node): number {
|
|
352
|
+
let foundPos = -1;
|
|
353
|
+
doc.descendants((node, pos) => {
|
|
354
|
+
if (node === targetNode) {
|
|
355
|
+
foundPos = pos;
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
return true;
|
|
359
|
+
});
|
|
360
|
+
return foundPos;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function commonNodes(vnodes: VNode[], nodes: Node[]): VNode[] {
|
|
364
|
+
let rs: VNode[] = [];
|
|
365
|
+
const maxLen = Math.min(vnodes.length, nodes.length);
|
|
366
|
+
for (let i = 0; i < maxLen; i++) {
|
|
367
|
+
if (nodesEqual(vnodes[i].node, nodes[i])) {
|
|
368
|
+
rs.push(vnodes[i]);
|
|
369
|
+
} else {
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return rs;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function trySwitchTable(
|
|
377
|
+
current: Node,
|
|
378
|
+
ancestorStack: VNode[],
|
|
379
|
+
ancestors: Node[],
|
|
380
|
+
): VNode {
|
|
381
|
+
let parentVNode = ancestorStack[ancestorStack.length - 1];
|
|
382
|
+
|
|
383
|
+
if (parentVNode && parentVNode.children.length > 0) {
|
|
384
|
+
let sibling = parentVNode.children[0];
|
|
385
|
+
if (sibling.node.childCount != current.childCount) {
|
|
386
|
+
ancestorStack.pop();
|
|
387
|
+
let newParent = {
|
|
388
|
+
node: ancestors[ancestors.length - 1],
|
|
389
|
+
children: [],
|
|
390
|
+
changeType: "unchanged" as ChangeType,
|
|
391
|
+
};
|
|
392
|
+
if (ancestorStack.length > 0) {
|
|
393
|
+
ancestorStack[ancestorStack.length - 1].children.push(newParent);
|
|
394
|
+
}
|
|
395
|
+
ancestorStack.push(newParent);
|
|
396
|
+
parentVNode = newParent;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return parentVNode;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function trySwitchListItem(
|
|
404
|
+
current: Node,
|
|
405
|
+
ancestorStack: VNode[],
|
|
406
|
+
ancestors: Node[],
|
|
407
|
+
): VNode {
|
|
408
|
+
let parentVNode = ancestorStack[ancestorStack.length - 1];
|
|
409
|
+
let parent = ancestors[ancestors.length - 1];
|
|
410
|
+
|
|
411
|
+
let newItem =
|
|
412
|
+
parentVNode.children.length > 0 && current.type.name == "paragraph";
|
|
413
|
+
|
|
414
|
+
if (newItem) {
|
|
415
|
+
ancestorStack.pop();
|
|
416
|
+
let newParent = {
|
|
417
|
+
node: parent,
|
|
418
|
+
children: [],
|
|
419
|
+
changeType: "unchanged" as ChangeType,
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
if (ancestorStack.length > 0) {
|
|
423
|
+
ancestorStack[ancestorStack.length - 1].children.push(newParent);
|
|
424
|
+
}
|
|
425
|
+
ancestorStack.push(newParent);
|
|
426
|
+
|
|
427
|
+
parentVNode = newParent;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return parentVNode;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export function buildVNode(blockChanges: BlockChange[]): {
|
|
434
|
+
root: VNode;
|
|
435
|
+
mergeGroups: VNode[][];
|
|
436
|
+
} {
|
|
437
|
+
let mergeGroups: VNode[][] = [];
|
|
438
|
+
let currentMergeGroup: VNode[] = [];
|
|
439
|
+
let previousChangeType: ChangeType = "unchanged";
|
|
440
|
+
let ancestorStack: VNode[] = [];
|
|
441
|
+
|
|
442
|
+
for (const change of blockChanges) {
|
|
443
|
+
let nodes = change.A?.nodes || change.B?.nodes || [];
|
|
444
|
+
if (nodes.length === 0) continue;
|
|
445
|
+
|
|
446
|
+
let ancestors = nodes.slice(0, -1);
|
|
447
|
+
let current = nodes[nodes.length - 1];
|
|
448
|
+
const common = commonNodes(ancestorStack, ancestors);
|
|
449
|
+
|
|
450
|
+
if (common.length >= ancestors.length) {
|
|
451
|
+
ancestorStack = ancestorStack.slice(0, ancestors.length);
|
|
452
|
+
} else if (common.length < ancestors.length) {
|
|
453
|
+
ancestorStack = ancestorStack.slice(0, common.length);
|
|
454
|
+
|
|
455
|
+
for (let i = common.length; i < ancestors.length; i++) {
|
|
456
|
+
const ancestor = ancestors[i];
|
|
457
|
+
let vnode = {
|
|
458
|
+
node: ancestor,
|
|
459
|
+
children: [],
|
|
460
|
+
changeType: "unchanged" as ChangeType,
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
if (ancestorStack.length > 0) {
|
|
464
|
+
ancestorStack[ancestorStack.length - 1].children.push(vnode);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
ancestorStack.push(vnode);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
let parent =
|
|
472
|
+
ancestorStack.length > 0 ? ancestorStack[ancestorStack.length - 1] : null;
|
|
473
|
+
|
|
474
|
+
if (
|
|
475
|
+
current.type.name == "table_row" ||
|
|
476
|
+
current.type.name == "table_header_row"
|
|
477
|
+
) {
|
|
478
|
+
parent = trySwitchTable(current, ancestorStack, ancestors);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (parent?.node.type.name == "list_item") {
|
|
482
|
+
parent = trySwitchListItem(current, ancestorStack, ancestors);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
let vnode = {
|
|
486
|
+
node: current,
|
|
487
|
+
children: [],
|
|
488
|
+
changeType: change.type,
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
if (parent) {
|
|
492
|
+
parent.children.push(vnode);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
ancestorStack.push(vnode);
|
|
496
|
+
|
|
497
|
+
if (change.type === "unchanged") {
|
|
498
|
+
if (currentMergeGroup.length) {
|
|
499
|
+
mergeGroups.push(currentMergeGroup);
|
|
500
|
+
currentMergeGroup = [];
|
|
501
|
+
}
|
|
502
|
+
} else if (change.type === "delete") {
|
|
503
|
+
if (previousChangeType === "insert") {
|
|
504
|
+
mergeGroups.push(currentMergeGroup);
|
|
505
|
+
currentMergeGroup = [];
|
|
506
|
+
}
|
|
507
|
+
currentMergeGroup.push(vnode);
|
|
508
|
+
} else if (change.type === "insert") {
|
|
509
|
+
currentMergeGroup.push(vnode);
|
|
510
|
+
}
|
|
511
|
+
previousChangeType = change.type;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (currentMergeGroup.length > 0) {
|
|
515
|
+
mergeGroups.push(currentMergeGroup);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return { root: ancestorStack[0], mergeGroups };
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function createDecorationForNode(
|
|
522
|
+
pos: number,
|
|
523
|
+
node: Node,
|
|
524
|
+
changeType: ChangeType,
|
|
525
|
+
groupIndex: number,
|
|
526
|
+
): Decoration {
|
|
527
|
+
const className = getDecorationClass(
|
|
528
|
+
changeType as "delete" | "insert" | "modify",
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
if (node.type.name === "heading") {
|
|
532
|
+
const decoration = Decoration.inline(
|
|
533
|
+
pos + 1,
|
|
534
|
+
pos + node.nodeSize - 1,
|
|
535
|
+
{
|
|
536
|
+
class: className,
|
|
537
|
+
},
|
|
538
|
+
{ changeType, groupIndex, decorationType: "diff", offset: true },
|
|
539
|
+
);
|
|
540
|
+
return decoration;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const decoration = Decoration.node(
|
|
544
|
+
pos,
|
|
545
|
+
pos + node.nodeSize,
|
|
546
|
+
{
|
|
547
|
+
class: className,
|
|
548
|
+
},
|
|
549
|
+
{ changeType, groupIndex, decorationType: "diff" },
|
|
550
|
+
);
|
|
551
|
+
return decoration;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function cloneNodeWithSchema(node: Node, schema: Schema): Node {
|
|
555
|
+
if (node.isText) {
|
|
556
|
+
return schema.text(node.text!, node.marks);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const nodeType = schema.nodes[node.type.name];
|
|
560
|
+
if (!nodeType) {
|
|
561
|
+
throw new Error(`Node type ${node.type.name} not found`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const children: Node[] = [];
|
|
565
|
+
node.content.forEach((child) => {
|
|
566
|
+
children.push(cloneNodeWithSchema(child, schema));
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
return nodeType.createChecked(node.attrs, Fragment.from(children));
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function buildNodeFromVNode(vnode: VNode, schema: Schema): Node {
|
|
573
|
+
if (vnode.children.length > 0) {
|
|
574
|
+
const children: Node[] = [];
|
|
575
|
+
for (const child of vnode.children) {
|
|
576
|
+
children.push(buildNodeFromVNode(child, schema));
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const nodeType = schema.nodes[vnode.node.type.name];
|
|
580
|
+
if (!nodeType) {
|
|
581
|
+
throw new Error(`Node type ${vnode.node.type.name} not found`);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const node = nodeType.createChecked(
|
|
585
|
+
vnode.node.attrs,
|
|
586
|
+
Fragment.from(children),
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
vnode.newNode = node;
|
|
590
|
+
return node;
|
|
591
|
+
} else {
|
|
592
|
+
const node = cloneNodeWithSchema(vnode.node, schema);
|
|
593
|
+
vnode.newNode = node;
|
|
594
|
+
return node;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function createDiffEditState(
|
|
599
|
+
originalDoc: Node,
|
|
600
|
+
modifiedDoc: Node,
|
|
601
|
+
schema: Schema,
|
|
602
|
+
): DiffEditState {
|
|
603
|
+
const changes = blockDiff(originalDoc, modifiedDoc);
|
|
604
|
+
const { root, mergeGroups } = buildVNode(changes);
|
|
605
|
+
const mergedDoc = buildNodeFromVNode(root, schema);
|
|
606
|
+
|
|
607
|
+
const decorations: Decoration[] = [];
|
|
608
|
+
|
|
609
|
+
for (let groupIndex = 0; groupIndex < mergeGroups.length; groupIndex++) {
|
|
610
|
+
const group = mergeGroups[groupIndex];
|
|
611
|
+
for (const vnode of group) {
|
|
612
|
+
if (vnode.newNode) {
|
|
613
|
+
const pos = findNodePosition(mergedDoc, vnode.newNode);
|
|
614
|
+
if (pos !== -1) {
|
|
615
|
+
const decoration = createDecorationForNode(
|
|
616
|
+
pos,
|
|
617
|
+
vnode.newNode,
|
|
618
|
+
vnode.changeType,
|
|
619
|
+
groupIndex,
|
|
620
|
+
);
|
|
621
|
+
decorations.push(decoration);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const decorationSet = DecorationSet.create(mergedDoc, decorations);
|
|
628
|
+
|
|
629
|
+
return { mergedDoc, decorations: decorationSet, mergeGroups };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export function getDecorationClass(
|
|
633
|
+
type: "delete" | "insert" | "modify",
|
|
634
|
+
): string {
|
|
635
|
+
switch (type) {
|
|
636
|
+
case "delete":
|
|
637
|
+
return "diff-decoration-delete";
|
|
638
|
+
case "insert":
|
|
639
|
+
return "diff-decoration-insert";
|
|
640
|
+
case "modify":
|
|
641
|
+
return "diff-decoration-modify";
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function filterDeleteNodeDecoration(
|
|
646
|
+
diffDecorations: DiffDecoration[],
|
|
647
|
+
action: "accept" | "reject",
|
|
648
|
+
): Decoration[] {
|
|
649
|
+
const result: Decoration[] = [];
|
|
650
|
+
|
|
651
|
+
for (const group of diffDecorations) {
|
|
652
|
+
if (action === "accept") {
|
|
653
|
+
result.push(...group.delete);
|
|
654
|
+
} else {
|
|
655
|
+
result.push(...group.insert);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return result;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
interface TreeNode {
|
|
663
|
+
node: Node;
|
|
664
|
+
pos: number;
|
|
665
|
+
children: TreeNode[];
|
|
666
|
+
parent: TreeNode | null;
|
|
667
|
+
toDelete: boolean;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function buildDeleteTree(doc: Node): TreeNode {
|
|
671
|
+
const root: TreeNode = {
|
|
672
|
+
node: doc,
|
|
673
|
+
pos: 0,
|
|
674
|
+
children: [],
|
|
675
|
+
parent: null,
|
|
676
|
+
toDelete: false,
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
const nodeMap = new Map<Node, TreeNode>();
|
|
680
|
+
nodeMap.set(doc, root);
|
|
681
|
+
|
|
682
|
+
doc.descendants((node: Node, pos: number, parent: Node | null) => {
|
|
683
|
+
if (parent === null) return;
|
|
684
|
+
const parentNode = nodeMap.get(parent);
|
|
685
|
+
if (!parentNode) return;
|
|
686
|
+
|
|
687
|
+
const childNode: TreeNode = {
|
|
688
|
+
node: node,
|
|
689
|
+
pos: pos,
|
|
690
|
+
children: [],
|
|
691
|
+
parent: parentNode,
|
|
692
|
+
toDelete: false,
|
|
693
|
+
};
|
|
694
|
+
parentNode.children.push(childNode);
|
|
695
|
+
nodeMap.set(node, childNode);
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
return root;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function markNodesToDelete(
|
|
702
|
+
treeRoot: TreeNode,
|
|
703
|
+
decorations: Decoration[],
|
|
704
|
+
): void {
|
|
705
|
+
for (const decoration of decorations) {
|
|
706
|
+
const from = decoration.from;
|
|
707
|
+
const to = decoration.to;
|
|
708
|
+
|
|
709
|
+
const processedNodes = new Set<any>();
|
|
710
|
+
|
|
711
|
+
treeRoot.node.descendants((node: any, pos: number, _parent: any) => {
|
|
712
|
+
const nodeEnd = pos + node.nodeSize;
|
|
713
|
+
|
|
714
|
+
if (!processedNodes.has(node)) {
|
|
715
|
+
processedNodes.add(node);
|
|
716
|
+
|
|
717
|
+
const treeNode = findTreeNodeByNodeRef(treeRoot, node);
|
|
718
|
+
if (treeNode) {
|
|
719
|
+
if (pos >= from && nodeEnd <= to) {
|
|
720
|
+
treeNode.toDelete = true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function findTreeNodeByNodeRef(
|
|
729
|
+
treeRoot: TreeNode,
|
|
730
|
+
targetNode: Node,
|
|
731
|
+
): TreeNode | null {
|
|
732
|
+
function traverse(node: TreeNode): TreeNode | null {
|
|
733
|
+
if (node.node === targetNode) {
|
|
734
|
+
return node;
|
|
735
|
+
}
|
|
736
|
+
for (const child of node.children) {
|
|
737
|
+
const found = traverse(child);
|
|
738
|
+
if (found) return found;
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
return traverse(treeRoot);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function markEmptyParentsToDelete(treeRoot: TreeNode): void {
|
|
746
|
+
function traverse(node: TreeNode, depth: number = 0): void {
|
|
747
|
+
if (node.node.type.name === "doc") {
|
|
748
|
+
for (const child of node.children) {
|
|
749
|
+
traverse(child, depth + 1);
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
let allChildrenDeleted = true;
|
|
755
|
+
for (const child of node.children) {
|
|
756
|
+
traverse(child, depth + 1);
|
|
757
|
+
if (!child.toDelete) {
|
|
758
|
+
allChildrenDeleted = false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (allChildrenDeleted && node.children.length > 0) {
|
|
763
|
+
node.toDelete = true;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
traverse(treeRoot);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function collectNodesToDelete(treeRoot: TreeNode): TreeNode[] {
|
|
771
|
+
const nodes: TreeNode[] = [];
|
|
772
|
+
|
|
773
|
+
function traverse(node: TreeNode): void {
|
|
774
|
+
if (node.toDelete) {
|
|
775
|
+
nodes.push(node);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
for (const child of node.children) {
|
|
780
|
+
traverse(child);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
traverse(treeRoot);
|
|
785
|
+
return nodes;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function shouldReplaceWholeDoc(treeRoot: TreeNode): boolean {
|
|
789
|
+
if (treeRoot.node.type.name !== "doc") return false;
|
|
790
|
+
|
|
791
|
+
for (const child of treeRoot.children) {
|
|
792
|
+
if (!child.toDelete) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return treeRoot.children.length > 0;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function merge(
|
|
801
|
+
ctx: Ctx,
|
|
802
|
+
action: "accept" | "reject",
|
|
803
|
+
index: number,
|
|
804
|
+
mergeAll: boolean,
|
|
805
|
+
): boolean {
|
|
806
|
+
let rs = true;
|
|
807
|
+
|
|
808
|
+
const view = ctx.get(editorViewCtx);
|
|
809
|
+
const state = view.state;
|
|
810
|
+
|
|
811
|
+
const diffDecorations = getDiffDecorations(ctx);
|
|
812
|
+
if (diffDecorations.length == 0) {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const decorations = ctx.get(diffDecorationState.key).decorations!;
|
|
817
|
+
|
|
818
|
+
let processDiffDecorations: DiffDecoration[];
|
|
819
|
+
|
|
820
|
+
if (mergeAll) {
|
|
821
|
+
processDiffDecorations = diffDecorations;
|
|
822
|
+
} else {
|
|
823
|
+
processDiffDecorations = [diffDecorations[index]];
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
let deleteNodeDecorations = filterDeleteNodeDecoration(
|
|
827
|
+
processDiffDecorations,
|
|
828
|
+
action,
|
|
829
|
+
);
|
|
830
|
+
let processDecorations = processDiffDecorations
|
|
831
|
+
.map((d) => [...d.delete, ...d.insert])
|
|
832
|
+
.flat();
|
|
833
|
+
|
|
834
|
+
const remainingDecorations = decorations.remove(processDecorations);
|
|
835
|
+
|
|
836
|
+
ctx.set(diffDecorationState.key, {
|
|
837
|
+
decorations: remainingDecorations,
|
|
838
|
+
});
|
|
839
|
+
rs = remainingDecorations.find().length > 0;
|
|
840
|
+
|
|
841
|
+
const emptyTr = state.tr;
|
|
842
|
+
view.dispatch(emptyTr);
|
|
843
|
+
|
|
844
|
+
const treeRoot = buildDeleteTree(state.doc);
|
|
845
|
+
|
|
846
|
+
markNodesToDelete(treeRoot, deleteNodeDecorations);
|
|
847
|
+
|
|
848
|
+
markEmptyParentsToDelete(treeRoot);
|
|
849
|
+
|
|
850
|
+
const nodesToDelete = collectNodesToDelete(treeRoot);
|
|
851
|
+
|
|
852
|
+
const shouldReplaceDoc = shouldReplaceWholeDoc(treeRoot);
|
|
853
|
+
|
|
854
|
+
if (nodesToDelete.length === 0) {
|
|
855
|
+
return rs;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
if (shouldReplaceDoc) {
|
|
859
|
+
const schema = state.doc.type.schema;
|
|
860
|
+
const emptyText = schema.text("");
|
|
861
|
+
const emptyParagraph = schema.nodes.paragraph.create(null, [emptyText]);
|
|
862
|
+
const newDoc = schema.nodes.doc.create(null, [emptyParagraph]);
|
|
863
|
+
|
|
864
|
+
let tr = state.tr;
|
|
865
|
+
tr.replaceWith(0, state.doc.nodeSize, newDoc);
|
|
866
|
+
view.dispatch(tr);
|
|
867
|
+
return rs;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const sortedNodes = nodesToDelete.sort(
|
|
871
|
+
(a: TreeNode, b: TreeNode) => b.pos - a.pos,
|
|
872
|
+
);
|
|
873
|
+
|
|
874
|
+
let tr = state.tr;
|
|
875
|
+
for (const node of sortedNodes) {
|
|
876
|
+
const from = node.pos;
|
|
877
|
+
const to = node.pos + node.node.nodeSize;
|
|
878
|
+
tr.delete(from, to);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
view.dispatch(tr);
|
|
882
|
+
|
|
883
|
+
return rs;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export function getDiffState(ctx: Ctx): {
|
|
887
|
+
currentIndex: number;
|
|
888
|
+
count: number;
|
|
889
|
+
} {
|
|
890
|
+
const groups = getDiffDecorations(ctx);
|
|
891
|
+
if (groups.length == 0) {
|
|
892
|
+
return { currentIndex: -1, count: 0 };
|
|
893
|
+
}
|
|
894
|
+
const view = ctx.get(editorViewCtx);
|
|
895
|
+
const selectionFrom = view.state.selection.from;
|
|
896
|
+
let currentIndex = -1;
|
|
897
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
898
|
+
const group = groups[i];
|
|
899
|
+
const firstDecoration = [...group.delete, ...group.insert][0];
|
|
900
|
+
if (firstDecoration) {
|
|
901
|
+
if (firstDecoration.from <= selectionFrom) {
|
|
902
|
+
currentIndex = i;
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return { currentIndex, count: groups.length };
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
export function jumpTo(ctx: Ctx, index: number) {
|
|
912
|
+
const view = ctx.get(editorViewCtx);
|
|
913
|
+
|
|
914
|
+
const groups = getDiffDecorations(ctx);
|
|
915
|
+
|
|
916
|
+
if (index < 0 || index >= groups.length) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const conflict = groups[index];
|
|
921
|
+
const doc = view.state.doc;
|
|
922
|
+
|
|
923
|
+
let firstDecoration = [...conflict.delete, ...conflict.insert][0];
|
|
924
|
+
|
|
925
|
+
const resolvedPos = doc.resolve(firstDecoration.from);
|
|
926
|
+
const selection = TextSelection.near(resolvedPos, 1);
|
|
927
|
+
view.focus();
|
|
928
|
+
const tr = view.state.tr.setSelection(selection);
|
|
929
|
+
view.dispatch(tr);
|
|
930
|
+
const dom = view.domAtPos(resolvedPos.pos).node as HTMLElement;
|
|
931
|
+
if (!dom) return;
|
|
932
|
+
dom.scrollIntoView({
|
|
933
|
+
behavior: "smooth",
|
|
934
|
+
block: "center",
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export function diff(
|
|
939
|
+
ctx: Ctx,
|
|
940
|
+
newContent: string,
|
|
941
|
+
originContent?: string,
|
|
942
|
+
): void {
|
|
943
|
+
const view = ctx.get(editorViewCtx);
|
|
944
|
+
if (!view) {
|
|
945
|
+
console.warn("Editor view not found.");
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const editorState = view.state;
|
|
950
|
+
const currentDoc =
|
|
951
|
+
originContent === undefined
|
|
952
|
+
? editorState.doc
|
|
953
|
+
: ctx.get(parserCtx)(originContent);
|
|
954
|
+
const schema = editorState.schema;
|
|
955
|
+
|
|
956
|
+
const modifiedDoc = ctx.get(parserCtx)(newContent);
|
|
957
|
+
const diffState = createDiffEditState(currentDoc, modifiedDoc, schema);
|
|
958
|
+
|
|
959
|
+
ctx.set(diffDecorationState.key, {
|
|
960
|
+
decorations: diffState.decorations,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// let oldState = view.state
|
|
964
|
+
let newState = EditorState.create({
|
|
965
|
+
doc: diffState.mergedDoc,
|
|
966
|
+
schema: schema,
|
|
967
|
+
plugins: editorState.plugins,
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
view.updateState(newState);
|
|
971
|
+
}
|