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:
Mohamed Boudra
2026-08-16 15:38:12 +08:00
committed by GitHub
parent 4748aad103
commit 7f8a485a77
15 changed files with 755 additions and 930 deletions
@@ -1,520 +0,0 @@
import { describe, expect, it } from "vitest";
import {
parseDiff,
reconstructNewFile,
reconstructOldFile,
highlightDiffFile,
parseAndHighlightDiff,
type ParsedDiffFile,
} from "./diff-highlighter";
const SIMPLE_DIFF = `diff --git a/example.ts b/example.ts
index 1234567..abcdefg 100644
--- a/example.ts
+++ b/example.ts
@@ -1,5 +1,5 @@
const foo = 1;
-const bar = 2;
+const bar = 3;
const baz = foo + bar;
export { foo, bar, baz };
`;
const MULTI_HUNK_DIFF = `diff --git a/example.ts b/example.ts
index 1234567..abcdefg 100644
--- a/example.ts
+++ b/example.ts
@@ -1,3 +1,3 @@
const foo = 1;
-const bar = 2;
+const bar = 3;
const baz = foo + bar;
@@ -10,3 +10,4 @@
function greet(name: string) {
return "Hello, " + name;
}
+export { greet };
`;
const NEW_FILE_DIFF = `diff --git a/newfile.ts b/newfile.ts
new file mode 100644
index 0000000..1234567
--- /dev/null
+++ b/newfile.ts
@@ -0,0 +1,3 @@
+const x = 1;
+const y = 2;
+export { x, y };
`;
const DELETED_FILE_DIFF = `diff --git a/oldfile.ts b/oldfile.ts
deleted file mode 100644
index 1234567..0000000
--- a/oldfile.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-const legacy = true;
-export { legacy };
`;
const RUST_DIFF = `diff --git a/lib.rs b/lib.rs
index 1234567..abcdefg 100644
--- a/lib.rs
+++ b/lib.rs
@@ -1,4 +1,4 @@
fn main() {
- let version = 1;
+ let version = 2;
println!("Hello, world!");
}
`;
const C_DIFF = `diff --git a/example.c b/example.c
index 1234567..abcdefg 100644
--- a/example.c
+++ b/example.c
@@ -1,4 +1,4 @@
int main(void) {
- int version = 1;
+ int version = 2;
return version;
}
`;
const JAVA_DIFF = `diff --git a/Main.java b/Main.java
index 1234567..abcdefg 100644
--- a/Main.java
+++ b/Main.java
@@ -1,5 +1,5 @@
public class Main {
public static void main(String[] args) {
- int version = 1;
+ int version = 2;
}
}
`;
const OBJECTIVE_C_DIFF = `diff --git a/AppDelegate.m b/AppDelegate.m
index 1234567..abcdefg 100644
--- a/AppDelegate.m
+++ b/AppDelegate.m
@@ -1,4 +1,4 @@
int main(void) {
- int version = 1;
+ int version = 2;
return version;
}
`;
const GO_DIFF = `diff --git a/main.go b/main.go
index 1234567..abcdefg 100644
--- a/main.go
+++ b/main.go
@@ -1,7 +1,7 @@
package main
import "fmt"
func main() {
- version := 1
+ version := 2
fmt.Println(version)
}
`;
const PHP_DIFF = `diff --git a/index.php b/index.php
index 1234567..abcdefg 100644
--- a/index.php
+++ b/index.php
@@ -1,4 +1,4 @@
<?php
-$version = 1;
+$version = 2;
echo $version;
?>
`;
const YAML_DIFF = `diff --git a/config.yaml b/config.yaml
index 1234567..abcdefg 100644
--- a/config.yaml
+++ b/config.yaml
@@ -1,3 +1,3 @@
app: paseo
-count: 1
+count: 2
enabled: true
`;
const XML_DIFF = `diff --git a/config.xml b/config.xml
index 1234567..abcdefg 100644
--- a/config.xml
+++ b/config.xml
@@ -1,3 +1,3 @@
<config>
- <count>1</count>
+ <count>2</count>
</config>
`;
describe("parseDiff", () => {
it("parses a simple diff with one hunk", () => {
const files = parseDiff(SIMPLE_DIFF);
expect(files).toHaveLength(1);
expect(files[0].path).toBe("example.ts");
expect(files[0].isNew).toBe(false);
expect(files[0].isDeleted).toBe(false);
expect(files[0].additions).toBe(1);
expect(files[0].deletions).toBe(1);
expect(files[0].hunks).toHaveLength(1);
const hunk = files[0].hunks[0];
expect(hunk.oldStart).toBe(1);
expect(hunk.oldCount).toBe(5);
expect(hunk.newStart).toBe(1);
expect(hunk.newCount).toBe(5);
// Header + 5 content lines (1 context, 1 remove, 1 add, 1 context, 1 blank context, 1 context)
expect(hunk.lines[0].type).toBe("header");
expect(hunk.lines[1].type).toBe("context");
expect(hunk.lines[1].content).toBe("const foo = 1;");
expect(hunk.lines[2].type).toBe("remove");
expect(hunk.lines[2].content).toBe("const bar = 2;");
expect(hunk.lines[3].type).toBe("add");
expect(hunk.lines[3].content).toBe("const bar = 3;");
});
it("parses no-prefix git diff headers", () => {
const files = parseDiff(
SIMPLE_DIFF.replace("a/example.ts b/example.ts", "example.ts example.ts")
.replace("--- a/example.ts", "--- example.ts")
.replace("+++ b/example.ts", "+++ example.ts"),
);
expect(files).toHaveLength(1);
expect(files[0].path).toBe("example.ts");
expect(files[0].additions).toBe(1);
expect(files[0].deletions).toBe(1);
expect(files[0].hunks).toHaveLength(1);
});
it("parses paths with spaces", () => {
const files = parseDiff(SIMPLE_DIFF.replaceAll("example.ts", "file with space.ts"));
expect(files).toHaveLength(1);
expect(files[0].path).toBe("file with space.ts");
expect(files[0].hunks).toHaveLength(1);
});
it("parses no-prefix paths with spaces", () => {
const files = parseDiff(
SIMPLE_DIFF.replaceAll("example.ts", "file with space.ts")
.replace(
"a/file with space.ts b/file with space.ts",
"file with space.ts file with space.ts",
)
.replace("--- a/file with space.ts", "--- file with space.ts")
.replace("+++ b/file with space.ts", "+++ file with space.ts"),
);
expect(files).toHaveLength(1);
expect(files[0].path).toBe("file with space.ts");
expect(files[0].hunks).toHaveLength(1);
});
it("preserves no-prefix paths that start with a or b", () => {
const files = parseDiff(
`${SIMPLE_DIFF.replace(
"diff --git a/example.ts b/example.ts",
"diff --git a/example.ts a/example.ts",
).replace("--- a/example.ts\n+++ b/example.ts", "--- a/example.ts\n+++ a/example.ts")}
${SIMPLE_DIFF.replaceAll("example.ts", "other.ts")
.replace("diff --git a/other.ts b/other.ts", "diff --git b/other.ts b/other.ts")
.replace("--- a/other.ts\n+++ b/other.ts", "--- b/other.ts\n+++ b/other.ts")}`,
);
expect(files.map((file) => file.path)).toEqual(["a/example.ts", "b/other.ts"]);
});
it("parses a diff with multiple hunks", () => {
const files = parseDiff(MULTI_HUNK_DIFF);
expect(files).toHaveLength(1);
expect(files[0].hunks).toHaveLength(2);
expect(files[0].hunks[0].oldStart).toBe(1);
expect(files[0].hunks[0].newStart).toBe(1);
expect(files[0].hunks[1].oldStart).toBe(10);
expect(files[0].hunks[1].newStart).toBe(10);
});
it("parses a new file diff", () => {
const files = parseDiff(NEW_FILE_DIFF);
expect(files).toHaveLength(1);
expect(files[0].path).toBe("newfile.ts");
expect(files[0].isNew).toBe(true);
expect(files[0].isDeleted).toBe(false);
expect(files[0].additions).toBe(3);
expect(files[0].deletions).toBe(0);
});
it("parses a deleted file diff", () => {
const files = parseDiff(DELETED_FILE_DIFF);
expect(files).toHaveLength(1);
expect(files[0].path).toBe("oldfile.ts");
expect(files[0].isNew).toBe(false);
expect(files[0].isDeleted).toBe(true);
expect(files[0].additions).toBe(0);
expect(files[0].deletions).toBe(2);
});
it("returns empty array for empty input", () => {
expect(parseDiff("")).toEqual([]);
expect(parseDiff(" ")).toEqual([]);
});
});
describe("reconstructNewFile", () => {
it("reconstructs the new file version from hunks", () => {
const files = parseDiff(SIMPLE_DIFF);
const newFile = reconstructNewFile(files[0].hunks);
expect(newFile.get(1)).toBe("const foo = 1;");
expect(newFile.get(2)).toBe("const bar = 3;"); // Changed line
expect(newFile.get(3)).toBe("const baz = foo + bar;");
// Note: blank lines in diffs should have a space prefix to be parsed
// as context lines. In this test the blank line has no prefix so it's
// not included. That's OK - real git diffs have the space.
expect(newFile.get(4)).toBe("export { foo, bar, baz };");
// Old value should not be present
expect(Array.from(newFile.values())).not.toContain("const bar = 2;");
});
it("handles new file (all additions)", () => {
const files = parseDiff(NEW_FILE_DIFF);
const newFile = reconstructNewFile(files[0].hunks);
expect(newFile.get(1)).toBe("const x = 1;");
expect(newFile.get(2)).toBe("const y = 2;");
expect(newFile.get(3)).toBe("export { x, y };");
});
});
describe("reconstructOldFile", () => {
it("reconstructs the old file version from hunks", () => {
const files = parseDiff(SIMPLE_DIFF);
const oldFile = reconstructOldFile(files[0].hunks);
expect(oldFile.get(1)).toBe("const foo = 1;");
expect(oldFile.get(2)).toBe("const bar = 2;"); // Original line
expect(oldFile.get(3)).toBe("const baz = foo + bar;");
// New value should not be present
expect(Array.from(oldFile.values())).not.toContain("const bar = 3;");
});
it("handles deleted file (all removals)", () => {
const files = parseDiff(DELETED_FILE_DIFF);
const oldFile = reconstructOldFile(files[0].hunks);
expect(oldFile.get(1)).toBe("const legacy = true;");
expect(oldFile.get(2)).toBe("export { legacy };");
});
});
describe("highlightDiffFile", () => {
it("adds syntax highlighting tokens to TypeScript code", () => {
const files = parseDiff(SIMPLE_DIFF);
const highlighted = highlightDiffFile(files[0]);
// Check that tokens are added
const hunk = highlighted.hunks[0];
// First content line: "const foo = 1;"
const constLine = hunk.lines[1];
expect(constLine.tokens).toContainEqual(
expect.objectContaining({ text: "const", style: "keyword" }),
);
expect(constLine.tokens).toContainEqual(
expect.objectContaining({ text: "1", style: "number" }),
);
});
it("highlights both added and removed lines correctly", () => {
const files = parseDiff(SIMPLE_DIFF);
const highlighted = highlightDiffFile(files[0]);
const hunk = highlighted.hunks[0];
// Removed line: "const bar = 2;"
const removedLine = hunk.lines.find((l) => l.type === "remove" && l.content.includes("bar"));
expect(removedLine?.tokens).toContainEqual(
expect.objectContaining({ text: "2", style: "number" }),
);
// Added line: "const bar = 3;"
const addedLine = hunk.lines.find((l) => l.type === "add" && l.content.includes("bar"));
expect(addedLine?.tokens).toContainEqual(
expect.objectContaining({ text: "3", style: "number" }),
);
});
it("does not modify unsupported file types", () => {
const file: ParsedDiffFile = {
path: "README.txt",
isNew: false,
isDeleted: false,
additions: 1,
deletions: 0,
hunks: [
{
oldStart: 1,
oldCount: 1,
newStart: 1,
newCount: 2,
lines: [
{ type: "header", content: "@@ -1,1 +1,2 @@" },
{ type: "context", content: "Hello" },
{ type: "add", content: "World" },
],
},
],
};
const highlighted = highlightDiffFile(file);
// Lines should not have tokens
expect(highlighted.hunks[0].lines[1].tokens).toBeUndefined();
expect(highlighted.hunks[0].lines[2].tokens).toBeUndefined();
});
it("adds syntax highlighting tokens to Rust code", () => {
const files = parseDiff(RUST_DIFF);
const highlighted = highlightDiffFile(files[0]);
const hunk = highlighted.hunks[0];
expect(hunk.lines[1].tokens).toContainEqual(
expect.objectContaining({ text: "fn", style: "keyword" }),
);
const addedLine = hunk.lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toContainEqual(
expect.objectContaining({ text: "2", style: "number" }),
);
});
it("adds syntax highlighting tokens to C code", () => {
const files = parseDiff(C_DIFF);
const highlighted = highlightDiffFile(files[0]);
const hunk = highlighted.hunks[0];
expect(hunk.lines[1].tokens?.length).toBeGreaterThan(0);
const addedLine = hunk.lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toContainEqual(
expect.objectContaining({ text: "2", style: "number" }),
);
});
it("adds syntax highlighting tokens to Java code", () => {
const files = parseDiff(JAVA_DIFF);
const highlighted = highlightDiffFile(files[0]);
const hunk = highlighted.hunks[0];
expect(hunk.lines[1].tokens).toContainEqual(
expect.objectContaining({ text: "public", style: "keyword" }),
);
const addedLine = hunk.lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toContainEqual(
expect.objectContaining({ text: "2", style: "number" }),
);
});
it("adds syntax highlighting tokens to Objective-C file extensions", () => {
const files = parseDiff(OBJECTIVE_C_DIFF);
const highlighted = highlightDiffFile(files[0]);
const hunk = highlighted.hunks[0];
const addedLine = hunk.lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toContainEqual(
expect.objectContaining({ text: "2", style: "number" }),
);
});
it("adds syntax highlighting tokens to Go code", () => {
const files = parseDiff(GO_DIFF);
const highlighted = highlightDiffFile(files[0]);
const addedLine = highlighted.hunks[0].lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toContainEqual(
expect.objectContaining({ text: "2", style: "number" }),
);
});
it("adds syntax highlighting tokens to PHP code", () => {
const files = parseDiff(PHP_DIFF);
const highlighted = highlightDiffFile(files[0]);
const addedLine = highlighted.hunks[0].lines.find(
(line) => line.type === "add" && line.content.includes("$version"),
);
expect(addedLine?.tokens).toContainEqual(
expect.objectContaining({ text: "2", style: "number" }),
);
});
it("adds syntax highlighting tokens to YAML code", () => {
const files = parseDiff(YAML_DIFF);
const highlighted = highlightDiffFile(files[0]);
const addedLine = highlighted.hunks[0].lines.find(
(line) => line.type === "add" && line.content.includes("count"),
);
expect(addedLine?.tokens?.length).toBeGreaterThan(0);
});
it("adds syntax highlighting tokens to XML code", () => {
const files = parseDiff(XML_DIFF);
const highlighted = highlightDiffFile(files[0]);
const addedLine = highlighted.hunks[0].lines.find(
(line) => line.type === "add" && line.content.includes("<count>"),
);
expect(addedLine?.tokens).toContainEqual(expect.objectContaining({ style: "tag" }));
});
});
describe("parseAndHighlightDiff", () => {
it("parses and highlights in one step", () => {
const files = parseAndHighlightDiff(SIMPLE_DIFF);
expect(files).toHaveLength(1);
expect(files[0].hunks[0].lines[1].tokens).toContainEqual(
expect.objectContaining({ text: "const", style: "keyword" }),
);
});
it("handles multiple files", () => {
const multiFileDiff = SIMPLE_DIFF + "\n" + NEW_FILE_DIFF;
const files = parseAndHighlightDiff(multiFileDiff);
expect(files).toHaveLength(2);
expect(files[0].path).toBe("example.ts");
expect(files[1].path).toBe("newfile.ts");
});
});
-313
View File
@@ -1,313 +0,0 @@
import { highlightCode, isLanguageSupported, type HighlightToken } from "@getpaseo/highlight";
export interface DiffLine {
type: "add" | "remove" | "context" | "header";
content: string;
lineNumber?: number; // Line number in the original/new file
tokens?: HighlightToken[];
}
export interface DiffHunk {
oldStart: number;
oldCount: number;
newStart: number;
newCount: number;
lines: DiffLine[];
}
export interface ParsedDiffFile {
path: string;
isNew: boolean;
isDeleted: boolean;
additions: number;
deletions: number;
hunks: DiffHunk[];
}
const DIFF_METADATA_PREFIXES = ["index ", "--- ", "+++ ", "new file mode", "deleted file mode"];
function isDiffMetadataLine(line: string): boolean {
return DIFF_METADATA_PREFIXES.some((prefix) => line.startsWith(prefix));
}
// Git's default patch headers use paired a/path and b/path prefixes, while
// diff.noprefix emits plain paths that may legitimately start with a/ or b/.
function usesDiffPathPrefixes(oldPath: string, newPath: string): boolean {
return oldPath.startsWith("a/") && newPath.startsWith("b/");
}
function extractPathFromMetadata(lines: string[], prefix: "--- " | "+++ "): string | null {
const line = lines.find((candidate) => candidate.startsWith(prefix));
if (!line) {
return null;
}
const path = line.slice(prefix.length).replace(/\t.*$/, "").trimEnd();
return path === "/dev/null" ? null : path;
}
function extractDiffPath(lines: string[]): string {
const firstLine = lines[0] ?? "";
const prefixedPathMatch = firstLine.match(/^a\/(.+) b\/(.+)$/);
if (prefixedPathMatch) {
return prefixedPathMatch[2];
}
const metadataPath =
extractPathFromMetadata(lines, "+++ ") ?? extractPathFromMetadata(lines, "--- ");
if (metadataPath) {
return metadataPath;
}
const pathMatch = firstLine.match(/^(\S+)\s+(\S+)$/);
if (pathMatch) {
const [, oldPath, newPath] = pathMatch;
const path = newPath === "/dev/null" ? oldPath : newPath;
return usesDiffPathPrefixes(oldPath, newPath) ? path.slice(2) : path;
}
return "unknown";
}
function pushContentLine(hunk: DiffHunk, line: string): { addition: number; deletion: number } {
if (line.startsWith("+")) {
hunk.lines.push({ type: "add", content: line.slice(1) });
return { addition: 1, deletion: 0 };
}
if (line.startsWith("-")) {
hunk.lines.push({ type: "remove", content: line.slice(1) });
return { addition: 0, deletion: 1 };
}
if (line.startsWith(" ")) {
hunk.lines.push({ type: "context", content: line.slice(1) });
return { addition: 0, deletion: 0 };
}
if (line.length > 0 && !line.startsWith("\\")) {
hunk.lines.push({ type: "context", content: line });
}
return { addition: 0, deletion: 0 };
}
interface HunkParseResult {
hunks: DiffHunk[];
additions: number;
deletions: number;
}
function parseHunks(lines: string[]): HunkParseResult {
const hunks: DiffHunk[] = [];
let currentHunk: DiffHunk | null = null;
let additions = 0;
let deletions = 0;
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (isDiffMetadataLine(line)) continue;
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
if (hunkMatch) {
if (currentHunk) {
hunks.push(currentHunk);
}
currentHunk = {
oldStart: parseInt(hunkMatch[1], 10),
oldCount: parseInt(hunkMatch[2] ?? "1", 10),
newStart: parseInt(hunkMatch[3], 10),
newCount: parseInt(hunkMatch[4] ?? "1", 10),
lines: [{ type: "header", content: line.match(/^(@@ .+? @@)/)?.[1] ?? line }],
};
continue;
}
if (!currentHunk) continue;
const delta = pushContentLine(currentHunk, line);
additions += delta.addition;
deletions += delta.deletion;
}
if (currentHunk) {
hunks.push(currentHunk);
}
return { hunks, additions, deletions };
}
/**
* Parse a unified diff into structured data
*/
export function parseDiff(diffText: string): ParsedDiffFile[] {
if (!diffText || diffText.trim().length === 0) {
return [];
}
const files: ParsedDiffFile[] = [];
const fileSections = diffText.split(/^diff --git /m).filter(Boolean);
for (const section of fileSections) {
const lines = section.split("\n");
const isNew = section.includes("new file mode") || section.includes("--- /dev/null");
const isDeleted = section.includes("deleted file mode") || section.includes("+++ /dev/null");
const path = extractDiffPath(lines);
const { hunks, additions, deletions } = parseHunks(lines);
files.push({ path, isNew, isDeleted, additions, deletions, hunks });
}
return files;
}
/**
* Reconstruct the "new" version of a file from diff hunks.
* Returns a map of new line numbers to their content.
*/
export function reconstructNewFile(hunks: DiffHunk[]): Map<number, string> {
const lines = new Map<number, string>();
for (const hunk of hunks) {
let newLineNum = hunk.newStart;
for (const line of hunk.lines) {
if (line.type === "header") continue;
if (line.type === "add" || line.type === "context") {
lines.set(newLineNum, line.content);
newLineNum++;
}
// Remove lines don't appear in the new file
}
}
return lines;
}
/**
* Reconstruct the "old" version of a file from diff hunks.
* Returns a map of old line numbers to their content.
*/
export function reconstructOldFile(hunks: DiffHunk[]): Map<number, string> {
const lines = new Map<number, string>();
for (const hunk of hunks) {
let oldLineNum = hunk.oldStart;
for (const line of hunk.lines) {
if (line.type === "header") continue;
if (line.type === "remove" || line.type === "context") {
lines.set(oldLineNum, line.content);
oldLineNum++;
}
// Add lines don't appear in the old file
}
}
return lines;
}
/**
* Apply syntax highlighting to diff hunks.
*
* Strategy:
* 1. Reconstruct both old and new file versions from the hunks
* 2. Highlight each version as a complete file
* 3. Map highlighted tokens back to diff lines using line numbers
*/
export function highlightDiffFile(file: ParsedDiffFile): ParsedDiffFile {
if (!isLanguageSupported(file.path)) {
return file;
}
// Reconstruct both versions
const newFileLines = reconstructNewFile(file.hunks);
const oldFileLines = reconstructOldFile(file.hunks);
// Build complete file content strings for highlighting
const newFileContent = buildFileContent(newFileLines);
const oldFileContent = buildFileContent(oldFileLines);
// Highlight both versions
const newHighlighted = highlightCode(newFileContent, file.path);
const oldHighlighted = highlightCode(oldFileContent, file.path);
// Build lookup maps: line number -> tokens
const newTokensByLine = buildTokenLookup(newFileLines, newHighlighted);
const oldTokensByLine = buildTokenLookup(oldFileLines, oldHighlighted);
// Apply tokens to hunks
const highlightedHunks = file.hunks.map((hunk) => {
let oldLineNum = hunk.oldStart;
let newLineNum = hunk.newStart;
const highlightedLines = hunk.lines.map((line): DiffLine => {
if (line.type === "header") {
return line;
}
let tokens: HighlightToken[] | undefined;
if (line.type === "add") {
tokens = newTokensByLine.get(newLineNum);
newLineNum++;
} else if (line.type === "remove") {
tokens = oldTokensByLine.get(oldLineNum);
oldLineNum++;
} else if (line.type === "context") {
// Context lines exist in both - use new file version
tokens = newTokensByLine.get(newLineNum);
oldLineNum++;
newLineNum++;
}
return tokens ? { ...line, tokens } : line;
});
return { ...hunk, lines: highlightedLines };
});
return { ...file, hunks: highlightedHunks };
}
function buildFileContent(lineMap: Map<number, string>): string {
if (lineMap.size === 0) return "";
const lineNumbers = Array.from(lineMap.keys()).sort((a, b) => a - b);
const minLine = lineNumbers[0];
const maxLine = lineNumbers[lineNumbers.length - 1];
const lines: string[] = [];
for (let i = minLine; i <= maxLine; i++) {
lines.push(lineMap.get(i) ?? "");
}
return lines.join("\n");
}
function buildTokenLookup(
lineMap: Map<number, string>,
highlighted: HighlightToken[][],
): Map<number, HighlightToken[]> {
const lookup = new Map<number, HighlightToken[]>();
if (lineMap.size === 0) return lookup;
const lineNumbers = Array.from(lineMap.keys()).sort((a, b) => a - b);
const minLine = lineNumbers[0];
// highlighted array is 0-indexed, line numbers are 1-indexed
for (let i = 0; i < highlighted.length; i++) {
const lineNum = minLine + i;
if (lineMap.has(lineNum)) {
lookup.set(lineNum, highlighted[i]);
}
}
return lookup;
}
/**
* Parse and highlight a complete diff
*/
export function parseAndHighlightDiff(diffText: string): ParsedDiffFile[] {
const files = parseDiff(diffText);
return files.map(highlightDiffFile);
}
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
compactHighlightTokens,
formatDiffContentText,
formatDiffGutterText,
hasVisibleDiffTokens,
@@ -28,4 +29,23 @@ describe("diff-rendering", () => {
expect(hasVisibleDiffTokens([{ text: "" }])).toBe(false);
expect(hasVisibleDiffTokens([{ text: "const value = 1;" }])).toBe(true);
});
it("compacts adjacent syntax tokens without changing their text or styles", () => {
expect(
compactHighlightTokens([
{ text: "const", style: "keyword" },
{ text: " ", style: null },
{ text: "value", style: null },
{ text: " ", style: null },
{ text: "=", style: "operator" },
{ text: "", style: "operator" },
{ text: " 1", style: "number" },
]),
).toEqual([
{ text: "const", style: "keyword" },
{ text: " value ", style: null },
{ text: "=", style: "operator" },
{ text: " 1", style: "number" },
]);
});
});
+22
View File
@@ -2,6 +2,28 @@ interface HighlightLikeToken {
text: string;
}
interface StyledHighlightToken extends HighlightLikeToken {
style: string | null;
}
export function compactHighlightTokens(
tokens: readonly StyledHighlightToken[],
): StyledHighlightToken[] {
const compacted: StyledHighlightToken[] = [];
for (const token of tokens) {
if (token.text.length === 0) {
continue;
}
const previous = compacted.at(-1);
if (previous?.style === token.style) {
previous.text += token.text;
continue;
}
compacted.push({ text: token.text, style: token.style });
}
return compacted;
}
// Preserve row height when a gutter or diff cell is intentionally blank.
// Non-breaking space because the gutter <Text> uses numberOfLines={1}, which
// collapses a plain ASCII space to zero height on web.