221 lines
5.9 KiB
TypeScript
221 lines
5.9 KiB
TypeScript
export type MobileDiffLineKind = 'context' | 'add' | 'delete'
|
|
|
|
export type MobileDiffLine = {
|
|
kind: MobileDiffLineKind
|
|
text: string
|
|
oldLineNumber?: number
|
|
newLineNumber?: number
|
|
}
|
|
|
|
const MAX_DIFF_CELLS = 200_000
|
|
const MAX_MOBILE_DIFF_LINES = 2_500
|
|
const MAX_BUILT_DIFF_LINES = MAX_MOBILE_DIFF_LINES + 1
|
|
const TRUNCATED_LINE: MobileDiffLine = {
|
|
kind: 'context',
|
|
text: '... diff truncated for mobile preview ...'
|
|
}
|
|
|
|
export function buildMobileDiffLines(
|
|
originalContent: string,
|
|
modifiedContent: string
|
|
): { lines: MobileDiffLine[]; truncated: boolean } {
|
|
const originalLines = splitContentLines(originalContent)
|
|
const modifiedLines = splitContentLines(modifiedContent)
|
|
// Why: the LCS table is quadratic. Large generated files still need a
|
|
// responsive mobile preview, so fall back to prefix/suffix diffing.
|
|
const lines =
|
|
originalLines.length * modifiedLines.length <= MAX_DIFF_CELLS
|
|
? buildLcsDiffLines(originalLines, modifiedLines)
|
|
: buildPrefixSuffixDiffLines(originalLines, modifiedLines)
|
|
|
|
return finalizeMobileDiffLines(lines)
|
|
}
|
|
|
|
function splitContentLines(content: string): string[] {
|
|
if (content.length === 0) {
|
|
return []
|
|
}
|
|
const lines = content.split(/\r?\n/)
|
|
if (content.endsWith('\n')) {
|
|
lines.pop()
|
|
}
|
|
return lines
|
|
}
|
|
|
|
function buildLcsDiffLines(originalLines: string[], modifiedLines: string[]): MobileDiffLine[] {
|
|
const rowWidth = modifiedLines.length + 1
|
|
const dp = new Uint32Array((originalLines.length + 1) * rowWidth)
|
|
for (let i = originalLines.length - 1; i >= 0; i -= 1) {
|
|
for (let j = modifiedLines.length - 1; j >= 0; j -= 1) {
|
|
dp[i * rowWidth + j] =
|
|
originalLines[i] === modifiedLines[j]
|
|
? dp[(i + 1) * rowWidth + j + 1] + 1
|
|
: Math.max(dp[(i + 1) * rowWidth + j], dp[i * rowWidth + j + 1])
|
|
}
|
|
}
|
|
|
|
const lines: MobileDiffLine[] = []
|
|
let originalIndex = 0
|
|
let modifiedIndex = 0
|
|
while (originalIndex < originalLines.length && modifiedIndex < modifiedLines.length) {
|
|
if (originalLines[originalIndex] === modifiedLines[modifiedIndex]) {
|
|
if (
|
|
!appendDiffLine(lines, {
|
|
kind: 'context',
|
|
text: originalLines[originalIndex] ?? '',
|
|
oldLineNumber: originalIndex + 1,
|
|
newLineNumber: modifiedIndex + 1
|
|
})
|
|
) {
|
|
return lines
|
|
}
|
|
originalIndex += 1
|
|
modifiedIndex += 1
|
|
} else if (
|
|
dp[(originalIndex + 1) * rowWidth + modifiedIndex] >=
|
|
dp[originalIndex * rowWidth + modifiedIndex + 1]
|
|
) {
|
|
if (
|
|
!appendDiffLine(lines, {
|
|
kind: 'delete',
|
|
text: originalLines[originalIndex] ?? '',
|
|
oldLineNumber: originalIndex + 1
|
|
})
|
|
) {
|
|
return lines
|
|
}
|
|
originalIndex += 1
|
|
} else {
|
|
if (
|
|
!appendDiffLine(lines, {
|
|
kind: 'add',
|
|
text: modifiedLines[modifiedIndex] ?? '',
|
|
newLineNumber: modifiedIndex + 1
|
|
})
|
|
) {
|
|
return lines
|
|
}
|
|
modifiedIndex += 1
|
|
}
|
|
}
|
|
|
|
while (originalIndex < originalLines.length) {
|
|
if (
|
|
!appendDiffLine(lines, {
|
|
kind: 'delete',
|
|
text: originalLines[originalIndex] ?? '',
|
|
oldLineNumber: originalIndex + 1
|
|
})
|
|
) {
|
|
return lines
|
|
}
|
|
originalIndex += 1
|
|
}
|
|
while (modifiedIndex < modifiedLines.length) {
|
|
if (
|
|
!appendDiffLine(lines, {
|
|
kind: 'add',
|
|
text: modifiedLines[modifiedIndex] ?? '',
|
|
newLineNumber: modifiedIndex + 1
|
|
})
|
|
) {
|
|
return lines
|
|
}
|
|
modifiedIndex += 1
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
function buildPrefixSuffixDiffLines(
|
|
originalLines: string[],
|
|
modifiedLines: string[]
|
|
): MobileDiffLine[] {
|
|
let prefixLength = 0
|
|
while (
|
|
prefixLength < originalLines.length &&
|
|
prefixLength < modifiedLines.length &&
|
|
originalLines[prefixLength] === modifiedLines[prefixLength]
|
|
) {
|
|
prefixLength += 1
|
|
}
|
|
|
|
let suffixLength = 0
|
|
while (
|
|
suffixLength + prefixLength < originalLines.length &&
|
|
suffixLength + prefixLength < modifiedLines.length &&
|
|
originalLines[originalLines.length - suffixLength - 1] ===
|
|
modifiedLines[modifiedLines.length - suffixLength - 1]
|
|
) {
|
|
suffixLength += 1
|
|
}
|
|
|
|
const lines: MobileDiffLine[] = []
|
|
for (let i = 0; i < prefixLength; i += 1) {
|
|
if (
|
|
!appendDiffLine(lines, {
|
|
kind: 'context',
|
|
text: originalLines[i] ?? '',
|
|
oldLineNumber: i + 1,
|
|
newLineNumber: i + 1
|
|
})
|
|
) {
|
|
return lines
|
|
}
|
|
}
|
|
for (let i = prefixLength; i < originalLines.length - suffixLength; i += 1) {
|
|
if (
|
|
!appendDiffLine(lines, {
|
|
kind: 'delete',
|
|
text: originalLines[i] ?? '',
|
|
oldLineNumber: i + 1
|
|
})
|
|
) {
|
|
return lines
|
|
}
|
|
}
|
|
for (let i = prefixLength; i < modifiedLines.length - suffixLength; i += 1) {
|
|
if (
|
|
!appendDiffLine(lines, {
|
|
kind: 'add',
|
|
text: modifiedLines[i] ?? '',
|
|
newLineNumber: i + 1
|
|
})
|
|
) {
|
|
return lines
|
|
}
|
|
}
|
|
for (let i = originalLines.length - suffixLength; i < originalLines.length; i += 1) {
|
|
const modifiedIndex =
|
|
modifiedLines.length - suffixLength + (i - (originalLines.length - suffixLength))
|
|
if (
|
|
!appendDiffLine(lines, {
|
|
kind: 'context',
|
|
text: originalLines[i] ?? '',
|
|
oldLineNumber: i + 1,
|
|
newLineNumber: modifiedIndex + 1
|
|
})
|
|
) {
|
|
return lines
|
|
}
|
|
}
|
|
return lines
|
|
}
|
|
|
|
function appendDiffLine(lines: MobileDiffLine[], line: MobileDiffLine): boolean {
|
|
// Why: the mobile preview only renders the first capped rows; keep one extra
|
|
// row solely to preserve the existing "truncated" marker decision.
|
|
lines.push(line)
|
|
return lines.length < MAX_BUILT_DIFF_LINES
|
|
}
|
|
|
|
function finalizeMobileDiffLines(lines: MobileDiffLine[]): {
|
|
lines: MobileDiffLine[]
|
|
truncated: boolean
|
|
} {
|
|
if (lines.length <= MAX_MOBILE_DIFF_LINES) {
|
|
return { lines, truncated: false }
|
|
}
|
|
return { lines: [...lines.slice(0, MAX_MOBILE_DIFF_LINES), TRUNCATED_LINE], truncated: true }
|
|
}
|