Files
paseo/packages/app/src/git/diff-document/model.test.ts
T
Mohamed Boudra d311797653 Reduce stalls when opening large diffs (#4574)
* perf(app): reduce avoidable work when opening large diffs

Large snapshots were recursively reconstructed for reference stability, while native scroll worklets captured complete file models. Preserve identity at the data producers and prepare text for the visible window without rebuilding unchanged files.

* test(app): await commit diff query completion without polling

* chore(app): remove profiling artifacts from the source tree
2026-09-09 16:05:44 +02:00

727 lines
25 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { ParsedDiffFile } from "@getpaseo/protocol/messages";
import { describe, expect, it } from "vitest";
import {
buildDiffDocumentModel,
captureScrollAnchor,
FILE_HEADER_HEIGHT,
graphemeBoundaries,
measureFragments,
resolveScrollAnchor,
resolveRelayoutScrollTop,
shouldApplyRelayoutScroll,
} from "./model";
import type { BuildDiffDocumentModelInput, TextMeasurer } from "./types";
const measurer: TextMeasurer = { measure: (text) => Array.from(text).length * 10 };
function file(path = "src/a.ts"): ParsedDiffFile {
return {
path,
isNew: false,
isDeleted: false,
additions: 1,
deletions: 1,
hunks: [
{
oldStart: 1,
oldCount: 1,
newStart: 1,
newCount: 1,
lines: [
{ type: "header", content: "@@ -1 +1 @@" },
{ type: "remove", content: "const oldValue = '👨‍👩‍👧‍👦';" },
{ type: "add", content: "const newValue = 'é';" },
],
},
],
};
}
function input(overrides: Partial<BuildDiffDocumentModelInput> = {}): BuildDiffDocumentModelInput {
return {
files: [file()],
collapsedFilePaths: new Set(),
layout: "unified",
wrapLines: true,
viewportWidth: 180,
typography: { family: "monospace", size: 12, lineHeight: 18 },
measureText: measurer,
palette: {
surface: "#000",
headerSurface: "#111",
border: "#222",
foreground: "#fff",
foregroundMuted: "#aaa",
addition: "green",
deletion: "red",
additionBackground: "#010",
deletionBackground: "#100",
emptyBackground: "#111",
selection: "blue",
headerActiveSurface: "#222",
headerBorder: "#333",
statusSuccess: "green",
statusDanger: "red",
statusWarning: "orange",
syntax: {},
},
labels: { binary: "Binary", tooLarge: "Too large" },
...overrides,
};
}
describe("diff document model", () => {
it("materializes only visible rows of a large file while preserving full horizontal extent", () => {
const source = file("large.ts");
source.hunks[0]!.lines = Array.from({ length: 1000 }, (_, index) => ({
type: "add",
content: index === 900 ? "a very wide offscreen line ".repeat(20) : `line ${index}`,
tokens: [{ text: `line ${index}`, style: null }],
}));
let advancedCharacters = 0;
const measureText: TextMeasurer = {
measure: (text) => text.length * 10,
measureAdvances(graphemes) {
advancedCharacters += graphemes.join("").length;
return graphemes.map((_, index) => (index + 1) * 10);
},
};
const base = input({ files: [source], wrapLines: false, viewportWidth: 300, measureText });
const eager = buildDiffDocumentModel(base);
advancedCharacters = 0;
const first = buildDiffDocumentModel({
...base,
materializationWindow: { top: FILE_HEADER_HEIGHT, height: 54 },
});
expect(measuredFragments(first, source.path)).toBe(3);
expect(advancedCharacters).toBeLessThan(100);
expect(first.height).toBe(eager.height);
expect(first.files[0]!.contentWidth).toBe(eager.files[0]!.contentWidth);
const priorCells = firstLineCells(first, source.path);
advancedCharacters = 0;
const next = buildDiffDocumentModel({
...base,
reuseFrom: [first],
materializationWindow: { top: FILE_HEADER_HEIGHT + 900 * 18, height: 54 },
});
expect(firstLineCells(next, source.path)).toBe(priorCells);
expect(next.rows[900]).toMatchObject({
kind: "line",
cells: [{ fragments: [{ text: source.hunks[0]!.lines[900]!.content }] }],
});
expect(advancedCharacters).toBeLessThan(600);
expect(next.height).toBe(eager.height);
advancedCharacters = 0;
buildDiffDocumentModel({
...base,
reuseFrom: [next],
materializationWindow: { top: FILE_HEADER_HEIGHT, height: 54 },
});
expect(advancedCharacters).toBe(0);
});
it("does not imperatively scroll for a no-op relayout", () => {
expect(shouldApplyRelayoutScroll(320, 320)).toBe(false);
expect(shouldApplyRelayoutScroll(320, 320.4)).toBe(false);
expect(shouldApplyRelayoutScroll(320, 321)).toBe(true);
});
it("segments graphemes when Intl.Segmenter is unavailable", () => {
const originalSegmenter = Intl.Segmenter;
Object.defineProperty(Intl, "Segmenter", { configurable: true, value: undefined });
try {
const source = "中é👨‍👩‍👧‍👦";
const fragments = measureFragments({
text: source,
availableWidth: 100,
wrapLines: true,
lineHeight: 18,
measureText: measurer,
});
expect(fragments[0]?.graphemes.map((grapheme) => grapheme.text)).toEqual(["中", "é", "👨‍👩‍👧‍👦"]);
} finally {
Object.defineProperty(Intl, "Segmenter", { configurable: true, value: originalSegmenter });
}
});
it("measures grapheme-safe wrapped fragments", () => {
const text = "ab👨‍👩‍👧‍👦cd";
const fragments = measureFragments({
text,
availableWidth: 30,
wrapLines: true,
lineHeight: 18,
measureText: measurer,
});
const boundaries = new Set(graphemeBoundaries(text));
expect(fragments.length).toBeGreaterThan(1);
for (const fragment of fragments) {
expect(boundaries.has(fragment.start)).toBe(true);
expect(boundaries.has(fragment.end)).toBe(true);
}
});
it("measures wrapped ligatures in the exact fragment shaping context", () => {
const ligatureMeasurer = shapingMeasurer(ligatureWidth);
const fragments = measureFragments({
text: "xfi",
availableWidth: 12,
wrapLines: true,
lineHeight: 18,
measureText: ligatureMeasurer,
});
expect(fragments.map((fragment) => fragment.text)).toEqual(["x", "fi"]);
expect(fragments.map((fragment) => fragment.width)).toEqual([10, 12]);
expect(fragments[1]?.graphemes.map((grapheme) => grapheme.width)).toEqual([8, 4]);
});
it("remeasures contextual-script glyphs after wrapping", () => {
const contextualMeasurer = shapingMeasurer((text) => {
if (text === "اب") return 15;
if (text === "ا") return 10;
if (text === "ب") return 9;
return 0;
});
const fragments = measureFragments({
text: "اب",
availableWidth: 10,
wrapLines: true,
lineHeight: 18,
measureText: contextualMeasurer,
});
expect(fragments.map((fragment) => fragment.text)).toEqual(["ا", "ب"]);
expect(fragments.map((fragment) => fragment.width)).toEqual([10, 9]);
expect(fragments[1]?.graphemes[0]?.width).toBe(9);
});
it("expands tabs to stable stops while retaining source offsets", () => {
const source = "a\tb\t中👨‍👩‍👧‍👦é";
const model = buildDiffDocumentModel(
input({ files: [fileWithChangedLine(source)], wrapLines: false }),
);
const cell = addedCell(model);
expect(cell.content).toBe(source);
expect(cell.fragments[0]?.text).toBe("a b 中👨‍👩‍👧‍👦é");
expect(cell.fragments[0]?.graphemes.map((grapheme) => grapheme.text)).toEqual([
"a",
" ",
"b",
" ",
"中",
"👨‍👩‍👧‍👦",
"é",
]);
expect(cell.content).toBe(source);
expect(cell.fragments[0]?.width).toBe(180);
});
it("measures CJK, combining marks, and ZWJ emoji as fallback-capable grapheme runs", () => {
const model = buildDiffDocumentModel(
input({
files: [fileWithChangedLine("中é👨‍👩‍👧‍👦")],
}),
);
expect(addedCell(model).fragments[0]?.graphemes.map((grapheme) => grapheme.text)).toEqual([
"中",
"é",
"👨‍👩‍👧‍👦",
]);
});
it("removes collapsed bodies and leaves future files expanded by default", () => {
const expanded = buildDiffDocumentModel(input({ files: [file("a.ts"), file("b.ts")] }));
const collapsed = buildDiffDocumentModel(
input({ files: [file("a.ts"), file("b.ts")], collapsedFilePaths: new Set(["a.ts"]) }),
);
expect(collapsed.files[0]?.bodyHeight).toBe(0);
expect(collapsed.files[1]?.bodyHeight).toBeGreaterThan(0);
expect(collapsed.height).toBeLessThan(expanded.height);
});
it("reuses unchanged file measurements when collapse state changes", () => {
const files = [file("a.ts"), file("b.ts")];
let measurementCount = 0;
const countingMeasurer: TextMeasurer = {
measure(text) {
measurementCount += 1;
return Array.from(text).length * 10;
},
};
const expanded = buildDiffDocumentModel(input({ files, measureText: countingMeasurer }));
const initialMeasurementCount = measurementCount;
const collapsed = buildDiffDocumentModel(
input({
files,
collapsedFilePaths: new Set(["a.ts"]),
measureText: countingMeasurer,
reuseFrom: [expanded],
}),
);
expect(measurementCount).toBe(initialMeasurementCount);
expect(collapsed.files[1]?.bodyHeight).toBe(expanded.files[1]?.bodyHeight);
expect(collapsed.rows.every((row, index) => row.index === index)).toBe(true);
expect(collapsed.rows[0]?.top).toBe(FILE_HEADER_HEIGHT * 2);
const expandedAgain = buildDiffDocumentModel(
input({
files,
measureText: countingMeasurer,
reuseFrom: [expanded, collapsed],
}),
);
expect(measurementCount).toBe(initialMeasurementCount);
expect(expandedAgain.height).toBe(expanded.height);
});
it("keeps the toggled sticky header anchored through collapse and expansion", () => {
const files = [file("a.ts"), file("b.ts")];
const expanded = buildDiffDocumentModel(input({ files }));
const firstFile = expanded.files[0]!;
const scrollTop = firstFile.top + 20;
const collapsed = buildDiffDocumentModel(
input({ files, collapsedFilePaths: new Set(["a.ts"]), reuseFrom: [expanded] }),
);
expect(resolveRelayoutScrollTop(expanded, collapsed, scrollTop)).toBe(collapsed.files[0]!.top);
expect(resolveRelayoutScrollTop(collapsed, expanded, collapsed.files[0]!.top)).toBe(
expanded.files[0]!.top,
);
});
it("reserves review geometry in the measured row", () => {
const model = buildDiffDocumentModel(
input({
reviewActions: {
commentsByTarget: new Map(),
editor: {
target: {
key: "src/a.ts:old:1",
filePath: "src/a.ts",
hunkHeader: "@@ -1 +1 @@",
hunkIndex: 0,
lineIndex: 1,
oldLineNumber: 1,
newLineNumber: null,
side: "old",
lineNumber: 1,
lineType: "remove",
content: "const oldValue = '👨‍👩‍👧‍👦';",
},
body: "",
commentId: null,
},
onStartComment() {},
onCancelEditor() {},
onSaveEditor() {},
onEditComment() {},
onDeleteComment() {},
},
}),
);
const reviewRow = model.rows.find((row) => {
if (row.kind !== "line") return false;
return (
row.cells[0]?.reviewTarget?.side === "old" || row.cells[1]?.reviewTarget?.side === "old"
);
});
expect(reviewRow?.kind === "line" ? reviewRow.reviewHeight : 0).toBe(148);
});
it("reflows review geometry without remeasuring unchanged text", () => {
const files = [file("a.ts"), file("b.ts")];
let measurementCount = 0;
const countingMeasurer: TextMeasurer = {
measure(text) {
measurementCount += 1;
return Array.from(text).length * 10;
},
};
const base = buildDiffDocumentModel(input({ files, measureText: countingMeasurer }));
const initialMeasurementCount = measurementCount;
const reviewCell = base.rows
.filter((row) => row.kind === "line")
.flatMap((row) => row.cells)
.find((cell) => cell?.reviewTarget);
if (!reviewCell?.reviewTarget) throw new Error("Expected a reviewable cell");
const baseReviewRow = rowForCell(base, reviewCell);
const baseSecondFileTop = base.files[1]!.top;
const withEditor = buildDiffDocumentModel(
input({
files,
measureText: countingMeasurer,
reviewActions: reviewActionsWithEditor(reviewCell.reviewTarget),
reuseFrom: [base],
}),
);
const editorRow = rowForReviewTarget(withEditor, reviewCell.reviewTarget.key);
expect(measurementCount).toBe(initialMeasurementCount);
expect(editorRow.reviewHeight).toBe(148);
expect(editorRow.cells).toBe(baseReviewRow.cells);
expect(withEditor.files[1]!.top).toBe(baseSecondFileTop + 148);
const withoutEditor = buildDiffDocumentModel(
input({
files,
measureText: countingMeasurer,
reviewActions: reviewActionsWithEditor(null),
reuseFrom: [base, withEditor],
}),
);
expect(measurementCount).toBe(initialMeasurementCount);
expect(withoutEditor.height).toBe(base.height);
expect(withoutEditor.files[1]!.top).toBe(baseSecondFileTop);
});
it("preserves a logical source anchor through width and font metric relayout", () => {
const narrow = buildDiffDocumentModel(
input({ files: [fileWithChangedLine("abcdefghij")], viewportWidth: 90 }),
);
const narrowCell = addedCell(narrow);
const narrowRow = narrow.rows.find(
(row) => row.kind === "line" && row.cells.includes(narrowCell),
);
expect(narrowRow?.kind).toBe("line");
expect(narrowCell.fragments.length).toBeGreaterThan(1);
const scrollTop = narrowRow!.top + narrowCell.fragments[1]!.top + 3;
const anchor = captureScrollAnchor(narrow, scrollTop);
expect(anchor?.kind).toBe("text");
const wide = buildDiffDocumentModel(
input({
files: [fileWithChangedLine("abcdefghij")],
viewportWidth: 300,
typography: { family: "wide-font", size: 18, lineHeight: 27 },
measureText: { measure: (text) => Array.from(text).length * 14 },
}),
);
const resolved = resolveScrollAnchor(wide, anchor!);
const wideCell = addedCell(wide);
const wideRow = wide.rows.find((row) => row.kind === "line" && row.cells.includes(wideCell));
const anchoredFragment = wideCell.fragments.find(
(fragment) =>
anchor?.kind === "text" &&
fragment.start <= anchor.cellOffset &&
anchor.cellOffset < fragment.end,
);
expect(resolved).toBe(wideRow!.top + anchoredFragment!.top - anchor!.viewportDelta);
const wideAnchor = captureScrollAnchor(wide, wideRow!.top + 2);
const backToNarrow = resolveScrollAnchor(narrow, wideAnchor!);
const firstNarrowFragment = narrowCell.fragments[0];
expect(backToNarrow).toBe(
narrowRow!.top + firstNarrowFragment!.top - wideAnchor!.viewportDelta,
);
});
it("preserves a source-line anchor from unified to split and back", () => {
const changedFile = fileWithUnevenChanges();
const unified = buildDiffDocumentModel(
input({ files: [changedFile], layout: "unified", viewportWidth: 100 }),
);
const identity = { hunkIndex: 0, lineIndex: 4, side: "new" } as const;
const unifiedCell = cellByIdentity(unified, identity);
const unifiedRow = rowForCell(unified, unifiedCell);
expect(unifiedCell.fragments.length).toBeGreaterThan(1);
const anchoredFragment = unifiedCell.fragments[1]!;
const anchor = captureScrollAnchor(unified, unifiedRow.top + anchoredFragment.top + 2);
expect(anchor).toMatchObject({
kind: "text",
sourceIdentity: identity,
cellOffset: anchoredFragment.start,
viewportDelta: -2,
});
const split = buildDiffDocumentModel(
input({ files: [changedFile], layout: "split", viewportWidth: 100 }),
);
const splitCell = cellByIdentity(split, identity);
const splitRow = rowForCell(split, splitCell);
const splitFragment = splitCell.fragments.find(
(fragment) =>
fragment.start <= anchoredFragment.start && anchoredFragment.start < fragment.end,
);
expect(resolveScrollAnchor(split, anchor!)).toBe(
splitRow.top + splitFragment!.top - anchor!.viewportDelta,
);
const splitAnchor = captureScrollAnchor(
split,
splitRow.top + splitFragment!.top - anchor!.viewportDelta,
);
expect(splitAnchor).toMatchObject({
kind: "text",
sourceIdentity: identity,
cellOffset: splitFragment!.start,
});
const unifiedAgain = buildDiffDocumentModel(
input({ files: [changedFile], layout: "unified", viewportWidth: 100 }),
);
const finalCell = cellByIdentity(unifiedAgain, identity);
const finalRow = rowForCell(unifiedAgain, finalCell);
const finalFragment = finalCell.fragments.find(
(fragment) =>
splitAnchor?.kind === "text" &&
fragment.start <= splitAnchor.cellOffset &&
splitAnchor.cellOffset < fragment.end,
);
expect(resolveScrollAnchor(unifiedAgain, splitAnchor!)).toBe(
finalRow.top + finalFragment!.top - splitAnchor!.viewportDelta,
);
});
it("uses the exact unified maximum horizontal scroll range", () => {
const model = buildDiffDocumentModel(
input({ files: [fileWithChangedLine("x".repeat(30))], wrapLines: false, viewportWidth: 240 }),
);
expect(model.files[0]?.contentWidth).toBe(344);
expect(model.files[0]!.contentWidth - model.viewportWidth).toBe(104);
});
it("uses the widest split-column overflow once for the shared file offset", () => {
const model = buildDiffDocumentModel(
input({
files: [fileWithChangedLine("x".repeat(30))],
layout: "split",
wrapLines: false,
viewportWidth: 240,
}),
);
expect(model.files[0]?.contentWidth).toBe(464);
expect(model.files[0]!.contentWidth - model.viewportWidth).toBe(224);
});
it("builds exact unwrapped scroll geometry without measuring offscreen files", () => {
const files = [file("a.ts"), file("b.ts"), file("c.ts")];
let measurementCount = 0;
const countingMeasurer: TextMeasurer = {
measure(text) {
measurementCount += 1;
return Array.from(text).length * 10;
},
};
const eager = buildDiffDocumentModel(input({ files, wrapLines: false }));
const firstFileWindow = buildDiffDocumentModel(
input({
files,
wrapLines: false,
measureText: countingMeasurer,
materializationWindow: { top: 0, height: eager.files[0]!.bottom },
}),
);
expect(firstFileWindow.height).toBe(eager.height);
expect(firstFileWindow.files.map((entry) => [entry.top, entry.bottom])).toEqual(
eager.files.map((entry) => [entry.top, entry.bottom]),
);
expect(firstFileWindow.rows[firstFileWindow.files[0]!.rowStart]).toMatchObject({
kind: "line",
});
expect(firstFileWindow.rows[firstFileWindow.files[1]!.rowStart]).toMatchObject({
kind: "line",
});
expect(measuredFragments(firstFileWindow, "a.ts")).toBeGreaterThan(0);
expect(measuredFragments(firstFileWindow, "b.ts")).toBe(0);
expect(measuredFragments(firstFileWindow, "c.ts")).toBe(0);
expect(measurementCount).toBeGreaterThan(0);
});
it("retains measured files while materializing a later unwrapped window", () => {
const files = [file("a.ts"), file("b.ts"), file("c.ts")];
const eager = buildDiffDocumentModel(input({ files, wrapLines: false }));
const first = buildDiffDocumentModel(
input({
files,
wrapLines: false,
materializationWindow: { top: 0, height: eager.files[0]!.bottom },
}),
);
const lastFile = eager.files[2]!;
const last = buildDiffDocumentModel(
input({
files,
wrapLines: false,
materializationWindow: { top: lastFile.top, height: lastFile.bottom - lastFile.top },
reuseFrom: [first],
}),
);
expect(measuredFragments(last, "a.ts")).toBeGreaterThan(0);
expect(measuredFragments(last, "b.ts")).toBe(0);
expect(measuredFragments(last, "c.ts")).toBeGreaterThan(0);
expect(last.height).toBe(eager.height);
expect(firstLine(last, "a.ts")).toBe(firstLine(first, "a.ts"));
expect(firstLine(last, "b.ts")).toBe(firstLine(first, "b.ts"));
expect(firstLineCells(last, "a.ts")).toBe(firstLineCells(first, "a.ts"));
expect(firstLineCells(last, "b.ts")).toBe(firstLineCells(first, "b.ts"));
});
it("keeps wrapped geometry fully measured", () => {
const model = buildDiffDocumentModel(
input({
files: [file("a.ts"), file("b.ts")],
wrapLines: true,
materializationWindow: { top: 0, height: FILE_HEADER_HEIGHT },
}),
);
expect(measuredFragments(model, "a.ts")).toBeGreaterThan(0);
expect(measuredFragments(model, "b.ts")).toBeGreaterThan(0);
});
});
function measuredFragments(model: ReturnType<typeof buildDiffDocumentModel>, path: string): number {
const fileSection = model.files.find((entry) => entry.path === path);
if (!fileSection) throw new Error(`Expected ${path}`);
return model.rows
.slice(fileSection.rowStart, fileSection.rowEnd)
.filter((row) => row.kind === "line")
.flatMap((row) => row.cells)
.reduce((count, cell) => count + (cell?.fragments.length ?? 0), 0);
}
function firstLineCells(model: ReturnType<typeof buildDiffDocumentModel>, path: string) {
return firstLine(model, path).cells;
}
function firstLine(model: ReturnType<typeof buildDiffDocumentModel>, path: string) {
const fileSection = model.files.find((entry) => entry.path === path);
if (!fileSection) throw new Error(`Expected ${path}`);
const row = model.rows[fileSection.rowStart];
if (!row || row.kind !== "line") throw new Error(`Expected a line in ${path}`);
return row;
}
function shapingMeasurer(measure: (text: string) => number): TextMeasurer {
return {
measure,
measureAdvances(graphemes) {
let prefix = "";
return graphemes.map((grapheme) => measure((prefix += grapheme)));
},
};
}
function ligatureWidth(text: string): number {
if (text === "fi") return 12;
if (text === "xfi") return 25;
let width = 0;
for (const character of text) width += { x: 10, f: 8, i: 7 }[character] ?? 0;
return width;
}
function fileWithChangedLine(content: string): ParsedDiffFile {
const changed = file();
const hunk = changed.hunks[0]!;
return {
...changed,
hunks: [
{
oldStart: hunk.oldStart,
oldCount: hunk.oldCount,
newStart: hunk.newStart,
newCount: hunk.newCount,
lines: hunk.lines.map((line) =>
line.type === "add" ? Object.assign({}, line, { content }) : line,
),
},
],
};
}
function fileWithUnevenChanges(): ParsedDiffFile {
return {
path: "src/anchor.ts",
isNew: false,
isDeleted: false,
additions: 3,
deletions: 2,
hunks: [
{
oldStart: 10,
oldCount: 2,
newStart: 20,
newCount: 3,
lines: [
{ type: "header", content: "@@ -10,2 +20,3 @@" },
{ type: "remove", content: "removed first" },
{ type: "remove", content: "x" },
{ type: "add", content: "added first" },
{ type: "add", content: "anchored addition has several wrapped fragments" },
{ type: "add", content: "added third" },
],
},
],
};
}
function addedCell(model: ReturnType<typeof buildDiffDocumentModel>) {
const cell = model.rows
.filter((row) => row.kind === "line")
.flatMap((row) => row.cells)
.find((candidate) => candidate?.type === "add");
if (!cell) throw new Error("Expected an added cell");
return cell;
}
function cellByIdentity(
model: ReturnType<typeof buildDiffDocumentModel>,
identity: { hunkIndex: number; lineIndex: number; side: "old" | "new" },
) {
const cell = model.rows
.filter((row) => row.kind === "line")
.flatMap((row) => row.cells)
.find(
(candidate) =>
candidate?.sourceIdentity.hunkIndex === identity.hunkIndex &&
candidate.sourceIdentity.lineIndex === identity.lineIndex &&
candidate.sourceIdentity.side === identity.side,
);
if (!cell) throw new Error("Expected a cell with the source identity");
return cell;
}
function rowForCell(
model: ReturnType<typeof buildDiffDocumentModel>,
cell: ReturnType<typeof cellByIdentity>,
) {
const row = model.rows.find(
(candidate) => candidate.kind === "line" && candidate.cells.includes(cell),
);
if (!row || row.kind !== "line") throw new Error("Expected the cell row");
return row;
}
function rowForReviewTarget(model: ReturnType<typeof buildDiffDocumentModel>, key: string) {
const row = model.rows.find(
(candidate) =>
candidate.kind === "line" && candidate.cells.some((cell) => cell?.reviewTarget?.key === key),
);
if (!row || row.kind !== "line") throw new Error("Expected the review row");
return row;
}
function reviewActionsWithEditor(
target: NonNullable<ReturnType<typeof addedCell>["reviewTarget"]> | null,
): NonNullable<BuildDiffDocumentModelInput["reviewActions"]> {
return {
commentsByTarget: new Map(),
editor: target ? { target, body: "", commentId: null } : null,
onStartComment() {},
onCancelEditor() {},
onSaveEditor() {},
onEditComment() {},
onDeleteComment() {},
};
}