Keep large diff reviews responsive (#3422)
* perf(app): bound large diff row mounting Keep the existing vertical and horizontal scroll owners so virtualization does not change gesture or scrolling behavior. Reserve exact offscreen row heights while mounting only the visible window, and retain the full renderer when wrapping or editing reviews requires dynamic geometry. * perf(app): keep large diff reviews responsive Opening an inline editor changed the whole review action object, reconciled large syntax-token trees, and disabled row virtualization. Keep the expensive token subtree stable and preserve deterministic row geometry while the editor is open.\n\nBound server-side highlighting for pathological single lines and remove the unused client highlighter so the actual diff pipeline owns the limit. * test(app): check mounted diff typography Row virtualization can unmount a fixed diff row after scrolling. Assert the configured font size against a currently mounted row so the test continues to verify rendered typography without depending on window position. * test(app): stabilize Mermaid streaming coverage
This commit is contained in:
@@ -331,6 +331,28 @@ describe("reconstructOldFile", () => {
|
||||
});
|
||||
|
||||
describe("highlightDiffFromHunks", () => {
|
||||
it("continues highlighting large diffs whose lines stay bounded", () => {
|
||||
const sourceLines = Array.from(
|
||||
{ length: 101 },
|
||||
(_, index) => `const value${index} = "${"x".repeat(990)}";`,
|
||||
);
|
||||
const [file] = parseDiff(
|
||||
[
|
||||
"diff --git a/example.ts b/example.ts",
|
||||
"--- /dev/null",
|
||||
"+++ b/example.ts",
|
||||
`@@ -0,0 +1,${sourceLines.length} @@`,
|
||||
...sourceLines.map((line) => `+${line}`),
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const highlighted = highlightDiffFromHunks(file);
|
||||
const contentLines = highlighted.hunks[0].lines.slice(1);
|
||||
|
||||
expect(contentLines[0].tokens).toContainEqual({ text: "const", style: "keyword" });
|
||||
expect(contentLines[100].tokens).toContainEqual({ text: "const", style: "keyword" });
|
||||
});
|
||||
|
||||
it("adds syntax highlighting tokens to TypeScript code", () => {
|
||||
const files = parseDiff(SIMPLE_DIFF);
|
||||
const highlighted = highlightDiffFromHunks(files[0]);
|
||||
@@ -507,6 +529,40 @@ describe("highlightDiffFromHunks", () => {
|
||||
});
|
||||
|
||||
describe("highlightDiffWithFileContent", () => {
|
||||
it("leaves files with an oversized line unhighlighted", async () => {
|
||||
const oversizedLine = `const payload = "${"x".repeat(10_001)}";`;
|
||||
const [file] = parseDiff(
|
||||
[
|
||||
"diff --git a/example.ts b/example.ts",
|
||||
"--- /dev/null",
|
||||
"+++ b/example.ts",
|
||||
"@@ -0,0 +1 @@",
|
||||
`+${oversizedLine}`,
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const highlighted = await highlightDiffWithFileContent(file, ".", {
|
||||
newFileContent: oversizedLine,
|
||||
});
|
||||
|
||||
expect(highlighted.hunks[0].lines[1]).toEqual({
|
||||
type: "add",
|
||||
content: oversizedLine,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses hunk context when the full file has an oversized line elsewhere", async () => {
|
||||
const file = parseDiff(SIMPLE_DIFF)[0];
|
||||
const oversizedUnchangedLine = `const payload = "${"x".repeat(10_001)}";`;
|
||||
|
||||
const highlighted = await highlightDiffWithFileContent(file, ".", {
|
||||
newFileContent: `${oversizedUnchangedLine}\nconst bar = 3;`,
|
||||
});
|
||||
const addedLine = highlighted.hunks[0].lines.find((line) => line.type === "add");
|
||||
|
||||
expect(addedLine?.tokens).toContainEqual({ text: "3", style: "number" });
|
||||
});
|
||||
|
||||
it("uses the old file content to preserve syntax context for removed lines", async () => {
|
||||
const cwd = mkdtempSync(join(tmpdir(), "diff-highlight-old-file-"));
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { highlightCode, isLanguageSupported, type HighlightToken } from "@getpaseo/highlight";
|
||||
|
||||
const MAX_DIFF_HIGHLIGHT_LINE_CHARS = 10_000;
|
||||
|
||||
export interface DiffLine {
|
||||
type: "add" | "remove" | "context" | "header";
|
||||
content: string;
|
||||
@@ -227,6 +229,25 @@ function buildFileContent(lineMap: Map<number, string>): string {
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function hasOversizedLine(content: string): boolean {
|
||||
let lineStart = 0;
|
||||
for (let index = 0; index <= content.length; index++) {
|
||||
if (index !== content.length && content.charCodeAt(index) !== 10) continue;
|
||||
if (index - lineStart > MAX_DIFF_HIGHLIGHT_LINE_CHARS) return true;
|
||||
lineStart = index + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasOversizedDiffLine(file: ParsedDiffFile): boolean {
|
||||
for (const hunk of file.hunks) {
|
||||
for (const line of hunk.lines) {
|
||||
if (line.content.length > MAX_DIFF_HIGHLIGHT_LINE_CHARS) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function buildTokenLookup(
|
||||
lineMap: Map<number, string>,
|
||||
highlighted: HighlightToken[][],
|
||||
@@ -251,7 +272,9 @@ function buildTokenLookup(
|
||||
function buildFullFileTokenLookup(
|
||||
fileContent: string,
|
||||
path: string,
|
||||
): Map<number, HighlightToken[]> {
|
||||
): Map<number, HighlightToken[]> | null {
|
||||
if (hasOversizedLine(fileContent)) return null;
|
||||
|
||||
const lookup = new Map<number, HighlightToken[]>();
|
||||
const highlighted = highlightCode(fileContent, path);
|
||||
|
||||
@@ -284,13 +307,16 @@ function buildReconstructedTokenLookups(file: ParsedDiffFile): {
|
||||
* This is the fallback when actual file content is not available.
|
||||
*/
|
||||
export function highlightDiffFromHunks(file: ParsedDiffFile): ParsedDiffFile {
|
||||
if (!isLanguageSupported(file.path)) {
|
||||
if (!isLanguageSupported(file.path) || hasOversizedDiffLine(file)) {
|
||||
return file;
|
||||
}
|
||||
|
||||
const { newTokensByLine, oldTokensByLine } = buildReconstructedTokenLookups(file);
|
||||
|
||||
return applyTokensToHunks(file, newTokensByLine, oldTokensByLine);
|
||||
const reconstructedTokens = buildReconstructedTokenLookups(file);
|
||||
return applyTokensToHunks(
|
||||
file,
|
||||
reconstructedTokens.newTokensByLine,
|
||||
reconstructedTokens.oldTokensByLine,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,7 +328,7 @@ export async function highlightDiffWithFileContent(
|
||||
cwd: string,
|
||||
options: HighlightDiffWithFileContentOptions = {},
|
||||
): Promise<ParsedDiffFile> {
|
||||
if (!isLanguageSupported(file.path)) {
|
||||
if (!isLanguageSupported(file.path) || hasOversizedDiffLine(file)) {
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -311,18 +337,20 @@ export async function highlightDiffWithFileContent(
|
||||
let oldTokensByLine = reconstructedTokens.oldTokensByLine;
|
||||
|
||||
if (typeof options.oldFileContent === "string") {
|
||||
oldTokensByLine = buildFullFileTokenLookup(options.oldFileContent, file.path);
|
||||
oldTokensByLine =
|
||||
buildFullFileTokenLookup(options.oldFileContent, file.path) ?? oldTokensByLine;
|
||||
}
|
||||
|
||||
if (typeof options.newFileContent === "string") {
|
||||
newTokensByLine = buildFullFileTokenLookup(options.newFileContent, file.path);
|
||||
newTokensByLine =
|
||||
buildFullFileTokenLookup(options.newFileContent, file.path) ?? newTokensByLine;
|
||||
return applyTokensToHunks(file, newTokensByLine, oldTokensByLine);
|
||||
}
|
||||
|
||||
const filePath = resolve(cwd, file.path);
|
||||
try {
|
||||
const fileContent = await readFile(filePath, "utf-8");
|
||||
newTokensByLine = buildFullFileTokenLookup(fileContent, file.path);
|
||||
newTokensByLine = buildFullFileTokenLookup(fileContent, file.path) ?? newTokensByLine;
|
||||
} catch {
|
||||
// If file read fails (deleted file, etc.), fall back to reconstructed new-side tokens.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user