Files
orca/mobile/src/session/mobile-diff-lines.ts
T
Kelvin Amoaba 9d2e441947 fix(mobile): compare branches and show diffs with desktop's base-ref and whitespace settings (#34038)
* fix(mobile): compare branches and show diffs with desktop's base-ref and whitespace settings

Base-ref resolution moves to src/shared and the host publishes the compare-against-upstream
and show-whitespace settings, so the phone's branch compare and diffs match desktop.

* fix(mobile): use the upstream compare policy on the review screen and rebuild diffs when whitespace changes

* fix(mobile): reread Source Control settings on Refresh and restart diffs loading under the old whitespace setting

* test(mobile): cover diffs dropped when desktop's whitespace setting arrives

* refactor(mobile): share one Source Control settings read per host across screens
2026-10-27 05:29:46 +00:00

237 lines
6.5 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,
options?: { ignoreTrimWhitespace?: boolean }
): { lines: MobileDiffLine[]; truncated: boolean } {
const originalLines = splitContentLines(originalContent)
const modifiedLines = splitContentLines(modifiedContent)
// Desktop's diff hides leading/trailing-whitespace-only edits unless whitespace is shown.
const originalKeys = options?.ignoreTrimWhitespace
? originalLines.map((line) => line.trim())
: originalLines
const modifiedKeys = options?.ignoreTrimWhitespace
? modifiedLines.map((line) => line.trim())
: modifiedLines
// 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, originalKeys, modifiedKeys)
: buildPrefixSuffixDiffLines(originalLines, modifiedLines, originalKeys, modifiedKeys)
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
}
// Keys are the lines compared for equality; the lines themselves are what the diff shows.
function buildLcsDiffLines(
originalLines: string[],
modifiedLines: string[],
originalKeys: string[],
modifiedKeys: 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] =
originalKeys[i] === modifiedKeys[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 (originalKeys[originalIndex] === modifiedKeys[modifiedIndex]) {
if (
!appendDiffLine(lines, {
kind: 'context',
text: modifiedLines[modifiedIndex] ?? '',
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[],
originalKeys: string[],
modifiedKeys: string[]
): MobileDiffLine[] {
let prefixLength = 0
while (
prefixLength < originalLines.length &&
prefixLength < modifiedLines.length &&
originalKeys[prefixLength] === modifiedKeys[prefixLength]
) {
prefixLength += 1
}
let suffixLength = 0
while (
suffixLength + prefixLength < originalLines.length &&
suffixLength + prefixLength < modifiedLines.length &&
originalKeys[originalLines.length - suffixLength - 1] ===
modifiedKeys[modifiedLines.length - suffixLength - 1]
) {
suffixLength += 1
}
const lines: MobileDiffLine[] = []
for (let i = 0; i < prefixLength; i += 1) {
if (
!appendDiffLine(lines, {
kind: 'context',
text: modifiedLines[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: modifiedLines[modifiedIndex] ?? '',
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 }
}