Keep large diffs responsive across web and mobile (#4174)

* perf(app): keep large diffs responsive

Render file headers on the canvas and bound expensive text shaping to the active viewport while preserving sticky, hover, and horizontal-scroll behavior. Let native scroll cancellation arbitrate header presses so finger scrolling cannot expand files.

* test(app): assert canvas diff header accessibility

* test(app): assert canvas diff header semantics
This commit is contained in:
Mohamed Boudra
2026-09-01 23:52:11 +02:00
committed by GitHub
parent ba54575ab7
commit 3998ea11d9
33 changed files with 2250 additions and 530 deletions
+351 -92
View File
@@ -37,9 +37,33 @@ interface CellSource {
sourceIdentity: DiffSourceIdentity;
}
interface GeometryLine {
cells: DiffLineRow["cells"];
reviewHeight: number;
height: number;
}
interface ReusableModelIndex {
model: DiffDocumentModel;
filesBySource: Map<BuildDiffDocumentModelInput["files"][number], DiffFileSection>;
measuredFiles: Set<DiffFileSection>;
}
interface ReusableFileRows {
model: DiffDocumentModel;
file: DiffFileSection;
}
interface FileRowsResult {
bottom: number;
maximumHorizontalOverflow: number;
}
export function buildDiffDocumentModel(input: BuildDiffDocumentModelInput): DiffDocumentModel {
const rows: DiffRow[] = [];
const files: DiffFileSection[] = [];
const reusableModels = indexReusableModels(input.reuseFrom);
const currentReviewGeometryKey = reviewGeometryKey(input.reviewActions);
let documentTop = 0;
for (const [fileIndex, file] of input.files.entries()) {
@@ -51,96 +75,40 @@ export function buildDiffDocumentModel(input: BuildDiffDocumentModelInput): Diff
const gutterWidth = lineNumberGutterWidth(maximumLineNumber(file), input.typography.size);
let maximumHorizontalOverflow = 0;
const reusableModel = input.reuseFrom?.find((candidate) =>
candidate.files.some((entry) => entry.file === file && !entry.isCollapsed),
);
const reusableFile = reusableModel?.files.find(
(candidate) => candidate.file === file && !candidate.isCollapsed,
);
const reusable = findReusableFileRows({
indexes: reusableModels,
buildInput: input,
sourceFile: file,
fileTop,
bodyTop,
});
const reusableModel = reusable?.model;
const reusableFile = reusable?.file;
if (!isCollapsed && reusableFile) {
const reusedRows = reusableModel!.rows.slice(reusableFile.rowStart, reusableFile.rowEnd);
for (const reusedRow of reusedRows) {
if (reusedRow.kind === "line") {
const reviewHeight = reviewHeightForCells(reusedRow.cells, input);
const textHeight = reusedRow.height - reusedRow.reviewHeight;
const height = textHeight + reviewHeight;
rows.push({
...reusedRow,
index: rows.length,
fileIndex,
top: documentTop,
height,
reviewHeight,
});
documentTop += height;
continue;
}
rows.push({
...reusedRow,
index: rows.length,
fileIndex,
top: documentTop,
});
documentTop += reusedRow.height;
}
maximumHorizontalOverflow = Math.max(0, reusableFile.contentWidth - input.viewportWidth);
documentTop += DIFF_BODY_BORDER_HEIGHT;
} else if (!isCollapsed) {
if (file.status === "binary" || file.status === "too_large") {
const height = input.typography.lineHeight + 24;
rows.push({
kind: "status",
index: rows.length,
fileIndex,
path: file.path,
top: documentTop,
height,
label: file.status === "binary" ? input.labels.binary : input.labels.tooLarge,
});
documentTop += height;
} else {
const sources = lineSources(file, input.layout);
for (const sourceCells of sources) {
const columnCount = sourceCells.length;
const columnWidth = input.viewportWidth / columnCount;
const availableWidth = Math.max(
input.measureText.measure("M"),
columnWidth - gutterWidth - CODE_HORIZONTAL_PADDING,
);
const cells = sourceCells.map((source) => {
if (!source) return null;
const cell = measureCell({
source,
availableWidth,
if (!isCollapsed) {
const result =
reusableModel && reusableFile
? appendReusableFileRows({
rows,
input,
model: reusableModel,
file: reusableFile,
fileIndex,
fileTop,
bodyTop,
reviewGeometryKey: currentReviewGeometryKey,
})
: appendNewFileRows({
rows,
input,
file,
fileIndex,
fileTop,
bodyTop,
gutterWidth,
});
maximumHorizontalOverflow = Math.max(
maximumHorizontalOverflow,
gutterWidth + CODE_HORIZONTAL_PADDING + measureCellWidth(cell) - columnWidth,
);
return cell;
}) as DiffLineRow["cells"];
const textHeight = Math.max(
input.typography.lineHeight,
...cells.map((cell) => (cell?.fragments.length ?? 1) * input.typography.lineHeight),
);
const reviewHeight = reviewHeightForCells(cells, input);
const height = textHeight + reviewHeight;
rows.push({
kind: "line",
index: rows.length,
fileIndex,
path: file.path,
top: documentTop,
height,
cells,
reviewHeight,
});
documentTop += height;
}
}
documentTop += DIFF_BODY_BORDER_HEIGHT;
documentTop = result.bottom;
maximumHorizontalOverflow = result.maximumHorizontalOverflow;
}
files.push({
@@ -170,9 +138,193 @@ export function buildDiffDocumentModel(input: BuildDiffDocumentModelInput): Diff
layout: input.layout,
wrapLines: input.wrapLines,
viewportWidth: input.viewportWidth,
reviewGeometryKey: currentReviewGeometryKey,
};
}
function appendReusableFileRows(candidate: {
rows: DiffRow[];
input: BuildDiffDocumentModelInput;
model: DiffDocumentModel;
file: DiffFileSection;
fileIndex: number;
fileTop: number;
bodyTop: number;
reviewGeometryKey: string;
}): FileRowsResult {
const reusedRows = candidate.model.rows.slice(candidate.file.rowStart, candidate.file.rowEnd);
const retainsRowGeometry =
!candidate.input.wrapLines &&
candidate.model.reviewGeometryKey === candidate.reviewGeometryKey &&
candidate.file.top === candidate.fileTop &&
candidate.file.rowStart === candidate.rows.length;
if (retainsRowGeometry) {
candidate.rows.push(...reusedRows);
return {
bottom: candidate.bodyTop + candidate.file.bodyHeight,
maximumHorizontalOverflow: Math.max(
0,
candidate.file.contentWidth - candidate.input.viewportWidth,
),
};
}
let top = candidate.bodyTop;
for (const reusedRow of reusedRows) {
const nextRow = repositionReusedRow(reusedRow, candidate, top);
candidate.rows.push(nextRow);
top += nextRow.height;
}
return {
bottom: top + DIFF_BODY_BORDER_HEIGHT,
maximumHorizontalOverflow: Math.max(
0,
candidate.file.contentWidth - candidate.input.viewportWidth,
),
};
}
function repositionReusedRow(
row: DiffRow,
candidate: Pick<Parameters<typeof appendReusableFileRows>[0], "rows" | "input" | "fileIndex">,
top: number,
): DiffRow {
if (row.kind !== "line") {
return { ...row, index: candidate.rows.length, fileIndex: candidate.fileIndex, top };
}
const reviewHeight = reviewHeightForCells(row.cells, candidate.input);
return {
...row,
index: candidate.rows.length,
fileIndex: candidate.fileIndex,
top,
height: row.height - row.reviewHeight + reviewHeight,
reviewHeight,
};
}
function appendNewFileRows(candidate: {
rows: DiffRow[];
input: BuildDiffDocumentModelInput;
file: BuildDiffDocumentModelInput["files"][number];
fileIndex: number;
fileTop: number;
bodyTop: number;
gutterWidth: number;
}): FileRowsResult {
if (candidate.file.status === "binary" || candidate.file.status === "too_large") {
const height = candidate.input.typography.lineHeight + 24;
candidate.rows.push({
kind: "status",
index: candidate.rows.length,
fileIndex: candidate.fileIndex,
path: candidate.file.path,
top: candidate.bodyTop,
height,
label:
candidate.file.status === "binary"
? candidate.input.labels.binary
: candidate.input.labels.tooLarge,
});
return {
bottom: candidate.bodyTop + height + DIFF_BODY_BORDER_HEIGHT,
maximumHorizontalOverflow: 0,
};
}
const deferredLines =
candidate.input.materializationWindow && !candidate.input.wrapLines
? geometryLines(lineSources(candidate.file, candidate.input.layout, false), candidate.input)
: null;
const deferredBodyHeight = deferredLines?.reduce((height, line) => height + line.height, 0) ?? 0;
const shouldMaterialize =
deferredLines === null ||
intersectsMaterializationWindow(
candidate.input,
candidate.fileTop,
candidate.bodyTop + deferredBodyHeight + DIFF_BODY_BORDER_HEIGHT,
);
return shouldMaterialize
? appendMeasuredFileRows(candidate)
: appendGeometryFileRows(candidate, deferredLines);
}
function appendGeometryFileRows(
candidate: Parameters<typeof appendNewFileRows>[0],
lines: readonly GeometryLine[],
): FileRowsResult {
let top = candidate.bodyTop;
for (const line of lines) {
candidate.rows.push({
kind: "line",
index: candidate.rows.length,
fileIndex: candidate.fileIndex,
path: candidate.file.path,
top,
height: line.height,
cells: line.cells,
reviewHeight: line.reviewHeight,
});
top += line.height;
}
return { bottom: top + DIFF_BODY_BORDER_HEIGHT, maximumHorizontalOverflow: 0 };
}
function appendMeasuredFileRows(
candidate: Parameters<typeof appendNewFileRows>[0],
): FileRowsResult {
let top = candidate.bodyTop;
let maximumHorizontalOverflow = 0;
for (const sourceCells of lineSources(candidate.file, candidate.input.layout, true)) {
const columnCount = sourceCells.length;
const columnWidth = candidate.input.viewportWidth / columnCount;
const availableWidth = Math.max(
candidate.input.measureText.measure("M"),
columnWidth - candidate.gutterWidth - CODE_HORIZONTAL_PADDING,
);
const cells = sourceCells.map((source) => {
if (!source) return null;
const cell = measureCell({ source, availableWidth, input: candidate.input });
maximumHorizontalOverflow = Math.max(
maximumHorizontalOverflow,
candidate.gutterWidth + CODE_HORIZONTAL_PADDING + measureCellWidth(cell) - columnWidth,
);
return cell;
}) as DiffLineRow["cells"];
const textHeight = Math.max(
candidate.input.typography.lineHeight,
...cells.map((cell) => (cell?.fragments.length ?? 1) * candidate.input.typography.lineHeight),
);
const reviewHeight = reviewHeightForCells(cells, candidate.input);
const height = textHeight + reviewHeight;
candidate.rows.push({
kind: "line",
index: candidate.rows.length,
fileIndex: candidate.fileIndex,
path: candidate.file.path,
top,
height,
cells,
reviewHeight,
});
top += height;
}
return { bottom: top + DIFF_BODY_BORDER_HEIGHT, maximumHorizontalOverflow };
}
export function reviewGeometryKey(
reviewActions: BuildDiffDocumentModelInput["reviewActions"],
): string {
if (!reviewActions) return "";
const comments = [...reviewActions.commentsByTarget.entries()]
.map(([target, targetComments]) => [target, targetComments.map((comment) => comment.id).sort()])
.sort(([left], [right]) => String(left).localeCompare(String(right)));
const editor = reviewActions.editor
? [reviewActions.editor.target.key, reviewActions.editor.commentId]
: null;
return JSON.stringify([comments, editor]);
}
export function expandedBodyBorderTop(file: DiffFileSection): number | null {
"worklet";
return file.isCollapsed ? null : file.bottom - DIFF_BODY_BORDER_HEIGHT;
@@ -183,7 +335,7 @@ export function retainReusableModels(
next: DiffDocumentModel,
): DiffDocumentModel[] {
const fullest = [...(previous ?? []), next].reduce((best, candidate) =>
candidate.rows.length > best.rows.length ? candidate : best,
measuredFileCount(candidate) > measuredFileCount(best) ? candidate : best,
);
return fullest === next ? [next] : [fullest, next];
}
@@ -191,13 +343,14 @@ export function retainReusableModels(
function lineSources(
file: BuildDiffDocumentModelInput["files"][number],
layout: "unified" | "split",
includeTokens: boolean,
): Array<[CellSource] | [CellSource | null, CellSource | null]> {
if (layout === "split") {
return buildSplitDiffRows(file).map((row) => {
if (row.kind === "header") {
return [headerSource(row.content, row.hunkIndex, row.lineIndex)];
}
return [cellSource(row.left), cellSource(row.right)];
return [cellSource(row.left, includeTokens), cellSource(row.right, includeTokens)];
});
}
return buildUnifiedDiffLines(file).map((entry) => [
@@ -206,7 +359,7 @@ function lineSources(
content: entry.line.content,
lineNumber: entry.lineNumber,
reviewTarget: entry.reviewTarget,
tokenText: compactHighlightTokens(entry.line.tokens ?? []),
tokenText: includeTokens ? compactHighlightTokens(entry.line.tokens ?? []) : [],
sourceIdentity: {
hunkIndex: entry.hunkIndex,
lineIndex: entry.lineIndex,
@@ -227,14 +380,14 @@ function headerSource(content: string, hunkIndex: number, lineIndex: number): Ce
};
}
function cellSource(line: SplitDiffDisplayLine | null): CellSource | null {
function cellSource(line: SplitDiffDisplayLine | null, includeTokens: boolean): CellSource | null {
if (!line) return null;
return {
type: line.type,
content: line.content,
lineNumber: line.lineNumber,
reviewTarget: line.reviewTarget,
tokenText: compactHighlightTokens(line.tokens ?? []),
tokenText: includeTokens ? compactHighlightTokens(line.tokens ?? []) : [],
sourceIdentity: {
hunkIndex: line.hunkIndex,
lineIndex: line.lineIndex,
@@ -243,6 +396,112 @@ function cellSource(line: SplitDiffDisplayLine | null): CellSource | null {
};
}
function geometryCell(source: CellSource): DiffCell {
return {
type: source.type,
content: source.content,
lineNumber: source.lineNumber,
tokens: [],
fragments: [],
reviewTarget: source.reviewTarget,
sourceIdentity: source.sourceIdentity,
};
}
function geometryLines(
sources: Array<[CellSource] | [CellSource | null, CellSource | null]>,
input: BuildDiffDocumentModelInput,
): GeometryLine[] {
return sources.map((sourceCells) => {
const cells = sourceCells.map((source) =>
source ? geometryCell(source) : null,
) as DiffLineRow["cells"];
const reviewHeight = reviewHeightForCells(cells, input);
return {
cells,
reviewHeight,
height: input.typography.lineHeight + reviewHeight,
};
});
}
function intersectsMaterializationWindow(
input: BuildDiffDocumentModelInput,
fileTop: number,
fileBottom: number,
): boolean {
const window = input.materializationWindow;
if (!window) return true;
return fileTop < window.top + window.height && fileBottom > window.top;
}
function hasMeasuredFileRows(model: DiffDocumentModel, file: DiffFileSection): boolean {
if (file.isCollapsed) return false;
return model.rows.slice(file.rowStart, file.rowEnd).every((row) => {
if (row.kind !== "line") return true;
return row.cells.every((cell) => !cell || cell.fragments.length > 0);
});
}
function indexReusableModels(
models: readonly DiffDocumentModel[] | undefined,
): ReusableModelIndex[] {
return (models ?? []).map((model) => {
const filesBySource = new Map<BuildDiffDocumentModelInput["files"][number], DiffFileSection>();
const measuredFiles = new Set<DiffFileSection>();
for (const file of model.files) {
filesBySource.set(file.file, file);
if (hasMeasuredFileRows(model, file)) measuredFiles.add(file);
}
return { model, filesBySource, measuredFiles };
});
}
function findReusableFileRows(candidate: {
indexes: readonly ReusableModelIndex[];
buildInput: BuildDiffDocumentModelInput;
sourceFile: BuildDiffDocumentModelInput["files"][number];
fileTop: number;
bodyTop: number;
}): ReusableFileRows | null {
for (const index of candidate.indexes) {
const file = index.filesBySource.get(candidate.sourceFile);
if (
file &&
canReuseFileRows({
buildInput: candidate.buildInput,
file,
isMeasured: index.measuredFiles.has(file),
fileTop: candidate.fileTop,
bodyTop: candidate.bodyTop,
})
) {
return { model: index.model, file };
}
}
return null;
}
function canReuseFileRows(candidate: {
buildInput: BuildDiffDocumentModelInput;
file: DiffFileSection;
isMeasured: boolean;
fileTop: number;
bodyTop: number;
}): boolean {
if (candidate.file.isCollapsed) return false;
if (candidate.isMeasured) return true;
return !intersectsMaterializationWindow(
candidate.buildInput,
candidate.fileTop,
candidate.bodyTop + candidate.file.bodyHeight,
);
}
function measuredFileCount(model: DiffDocumentModel): number {
return model.files.filter((file) => hasMeasuredFileRows(model, file)).length;
}
function measureCell(input: {
source: CellSource;
availableWidth: number;