一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
216 行
5.8 KiB
TypeScript
216 行
5.8 KiB
TypeScript
// A local copy of node-diff3's diff3MergeRegions (MIT licensed). The region combining is
|
|
// unchanged; only the hunks come from a bounded Myers diff rather than its LCS, which
|
|
// blocks the UI for seconds on a long note.
|
|
const { diffArrays } = require('diff');
|
|
|
|
interface StableRegion {
|
|
stable: true;
|
|
buffer: 'a' | 'o' | 'b';
|
|
bufferStart: number;
|
|
bufferLength: number;
|
|
bufferContent: string[];
|
|
}
|
|
|
|
interface UnstableRegion {
|
|
stable: false;
|
|
aStart: number;
|
|
aLength: number;
|
|
aContent: string[];
|
|
oStart: number;
|
|
oLength: number;
|
|
oContent: string[];
|
|
bStart: number;
|
|
bLength: number;
|
|
bContent: string[];
|
|
}
|
|
|
|
export type Region = StableRegion | UnstableRegion;
|
|
|
|
export interface ArrayChange {
|
|
added?: boolean;
|
|
removed?: boolean;
|
|
count: number;
|
|
value: string[];
|
|
}
|
|
|
|
interface Hunk {
|
|
ab: 'a' | 'b';
|
|
oStart: number;
|
|
oLength: number;
|
|
abStart: number;
|
|
abLength: number;
|
|
}
|
|
|
|
// The merge runs during sync, so it times out sooner than the conflict viewer
|
|
export interface DiffOptions {
|
|
maxEditLength: number;
|
|
timeout: number;
|
|
}
|
|
|
|
export const diffOptions: DiffOptions = { maxEditLength: 5000, timeout: 1000 };
|
|
export const viewerDiffOptions: DiffOptions = { maxEditLength: 10000, timeout: 3000 };
|
|
|
|
// The duplicate line check and the merge diff the same two pairs, so whichever runs
|
|
// second reuses the result
|
|
export type DiffLines = (base: string[], side: string[])=> ArrayChange[]|undefined;
|
|
|
|
// The cache belongs to one merge, so it cannot be shared with another
|
|
// and is released along with it.
|
|
export const createDiffLines = (options: DiffOptions = diffOptions): DiffLines => {
|
|
const cache: { base: string[]; side: string[]; changes: ArrayChange[]|undefined }[] = [];
|
|
|
|
return (base, side) => {
|
|
const cached = cache.find(entry => entry.base === base && entry.side === side);
|
|
if (cached) return cached.changes;
|
|
|
|
const changes: ArrayChange[]|undefined = diffArrays(base, side, options);
|
|
cache.push({ base, side, changes });
|
|
return changes;
|
|
};
|
|
};
|
|
|
|
// Where the two buffers differ, in the same shape as node-diff3's diffIndices
|
|
const boundedDiffIndices = (o: string[], side: string[], diffLines: DiffLines) => {
|
|
const changes = diffLines(o, side);
|
|
if (!changes) return null;
|
|
|
|
const result: { oStart: number; oLength: number; sideStart: number; sideLength: number }[] = [];
|
|
let oIndex = 0;
|
|
let sideIndex = 0;
|
|
let start: { o: number; side: number }|null = null;
|
|
let oLength = 0;
|
|
let sideLength = 0;
|
|
|
|
const flush = () => {
|
|
if (start) result.push({ oStart: start.o, oLength, sideStart: start.side, sideLength });
|
|
start = null;
|
|
oLength = 0;
|
|
sideLength = 0;
|
|
};
|
|
|
|
for (const change of changes) {
|
|
if (!change.added && !change.removed) {
|
|
flush();
|
|
oIndex += change.count;
|
|
sideIndex += change.count;
|
|
continue;
|
|
}
|
|
|
|
if (!start) start = { o: oIndex, side: sideIndex };
|
|
|
|
if (change.removed) {
|
|
oLength += change.count;
|
|
oIndex += change.count;
|
|
} else {
|
|
sideLength += change.count;
|
|
sideIndex += change.count;
|
|
}
|
|
}
|
|
|
|
flush();
|
|
return result;
|
|
};
|
|
|
|
// Null when the diff cannot be merged, so the caller can fall back to a conflict
|
|
export default (a: string[], o: string[], b: string[], diffLines: DiffLines): Region[]|null => {
|
|
const hunks: Hunk[] = [];
|
|
|
|
for (const ab of ['a', 'b'] as const) {
|
|
const changes = boundedDiffIndices(o, ab === 'a' ? a : b, diffLines);
|
|
if (!changes) return null;
|
|
for (const change of changes) {
|
|
hunks.push({ ab, oStart: change.oStart, oLength: change.oLength, abStart: change.sideStart, abLength: change.sideLength });
|
|
}
|
|
}
|
|
|
|
hunks.sort((x, y) => x.oStart - y.oStart);
|
|
|
|
const results: Region[] = [];
|
|
let currOffset = 0;
|
|
|
|
const advanceTo = (endOffset: number) => {
|
|
if (endOffset > currOffset) {
|
|
results.push({
|
|
stable: true,
|
|
buffer: 'o',
|
|
bufferStart: currOffset,
|
|
bufferLength: endOffset - currOffset,
|
|
bufferContent: o.slice(currOffset, endOffset),
|
|
});
|
|
currOffset = endOffset;
|
|
}
|
|
};
|
|
|
|
while (hunks.length) {
|
|
let hunk = hunks.shift();
|
|
const regionStart = hunk.oStart;
|
|
let regionEnd = hunk.oStart + hunk.oLength;
|
|
const regionHunks = [hunk];
|
|
advanceTo(regionStart);
|
|
|
|
while (hunks.length) {
|
|
const nextHunk = hunks[0];
|
|
if (nextHunk.oStart > regionEnd) break;
|
|
|
|
regionEnd = Math.max(regionEnd, nextHunk.oStart + nextHunk.oLength);
|
|
regionHunks.push(hunks.shift());
|
|
}
|
|
|
|
if (regionHunks.length === 1) {
|
|
// Only one side changed here, so it can be taken as is
|
|
if (hunk.abLength > 0) {
|
|
const buffer = hunk.ab === 'a' ? a : b;
|
|
results.push({
|
|
stable: true,
|
|
buffer: hunk.ab,
|
|
bufferStart: hunk.abStart,
|
|
bufferLength: hunk.abLength,
|
|
bufferContent: buffer.slice(hunk.abStart, hunk.abStart + hunk.abLength),
|
|
});
|
|
}
|
|
} else {
|
|
// Both sides changed here. Each side's hunks become one span, corrected for the
|
|
// differing amounts of `o` they covered
|
|
const bounds: Record<'a'|'b', [number, number, number, number]> = {
|
|
a: [a.length, -1, o.length, -1],
|
|
b: [b.length, -1, o.length, -1],
|
|
};
|
|
|
|
while (regionHunks.length) {
|
|
hunk = regionHunks.shift();
|
|
const oEnd = hunk.oStart + hunk.oLength;
|
|
const abEnd = hunk.abStart + hunk.abLength;
|
|
const bound = bounds[hunk.ab];
|
|
bound[0] = Math.min(hunk.abStart, bound[0]);
|
|
bound[1] = Math.max(abEnd, bound[1]);
|
|
bound[2] = Math.min(hunk.oStart, bound[2]);
|
|
bound[3] = Math.max(oEnd, bound[3]);
|
|
}
|
|
|
|
const aStart = bounds.a[0] + (regionStart - bounds.a[2]);
|
|
const aEnd = bounds.a[1] + (regionEnd - bounds.a[3]);
|
|
const bStart = bounds.b[0] + (regionStart - bounds.b[2]);
|
|
const bEnd = bounds.b[1] + (regionEnd - bounds.b[3]);
|
|
|
|
results.push({
|
|
stable: false,
|
|
aStart,
|
|
aLength: aEnd - aStart,
|
|
aContent: a.slice(aStart, aEnd),
|
|
oStart: regionStart,
|
|
oLength: regionEnd - regionStart,
|
|
oContent: o.slice(regionStart, regionEnd),
|
|
bStart,
|
|
bLength: bEnd - bStart,
|
|
bContent: b.slice(bStart, bEnd),
|
|
});
|
|
}
|
|
|
|
currOffset = regionEnd;
|
|
}
|
|
|
|
advanceTo(o.length);
|
|
|
|
return results;
|
|
};
|