Files
paseo/packages/server/src/server/utils/diff-highlighter.test.ts
T
paseo-bot[bot]and见安 938c4e2632 Show Git diffs for filenames with quotes and control characters (#6583)
* fix(diff): decode quoted Git patch paths

* fix(server): preserve Git paths in diff and commit listings

* refactor(diff): make quoted-path conversions explicit

* test(server): verify quoted filename diffs through daemon RPC

Keep the checkout and commit-file regression on the public client interface, and align the large-file command assertion with NUL-delimited statistics.

Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com>

* test(server): run filename RPC regression in integration checks

---------

Co-authored-by: 见安 <73627805+Mar-garet@users.noreply.github.com>
Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
2026-10-14 12:09:47 +02:00

822 lines
25 KiB
TypeScript

import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
parseDiff,
reconstructNewFile,
reconstructOldFile,
highlightDiffFromHunks,
highlightDiffWithFileContent,
type ParsedDiffFile,
} from "./diff-highlighter.js";
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>
`;
const SVELTE_DIFF = `diff --git a/Counter.svelte b/Counter.svelte
index 1234567..abcdefg 100644
--- a/Counter.svelte
+++ b/Counter.svelte
@@ -1,7 +1,7 @@
<script lang="ts">
- let count: number = 1;
+ let count: number = 2;
</script>
{#if count > 0}
<span class="badge">{count}</span>
{/if}
`;
const ASTRO_DIFF = `diff --git a/Page.astro b/Page.astro
index 1234567..abcdefg 100644
--- a/Page.astro
+++ b/Page.astro
@@ -1,7 +1,7 @@
---
-const title = "Hello";
+const title = "Hello, Astro";
---
<main class="page">
<h1>{title}</h1>
</main>
`;
describe("parseDiff", () => {
it.each(["-- comment", "++ marker"])(
"preserves hunk content resembling patch headers: %s",
(content) => {
const files = parseDiff(`diff --git a/example.txt b/example.txt
index 1234567..abcdefg 100644
--- a/example.txt
+++ b/example.txt
@@ -1 +1 @@
-${content} before
+${content} after
`);
expect(files).toHaveLength(1);
expect(files[0]).toMatchObject({
path: "example.txt",
isNew: false,
isDeleted: false,
additions: 1,
deletions: 1,
});
expect(files[0].hunks[0].lines).toEqual([
{ type: "header", content: "@@ -1 +1 @@" },
{ type: "remove", content: `${content} before` },
{ type: "add", content: `${content} after` },
]);
},
);
it("decodes quoted Git patch paths before matching files", () => {
const files = parseDiff(
[
'diff --git "a/report \\"draft\\".txt" "b/report \\"draft\\".txt"',
"index 1234567..abcdefg 100644",
'--- "a/report \\"draft\\".txt"',
'+++ "b/report \\"draft\\".txt"',
"@@ -1 +1 @@",
"-before",
"+after",
"",
].join("\n"),
);
expect(files).toEqual([
{
path: 'report "draft".txt',
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: "before" },
{ type: "add", content: "after" },
],
},
],
},
]);
});
it.each([
["tab\\tname.txt", "tab\tname.txt"],
["line\\nname.txt", "line\nname.txt"],
["back\\\\slash.txt", "back\\slash.txt"],
["bell\\aname.txt", "bell\u0007name.txt"],
["vertical\\vname.txt", "vertical\u000bname.txt"],
["\\346\\265\\213\\350\\257\\225.txt", "测试.txt"],
['测试 \\"draft\\".txt', '测试 "draft".txt'],
])("decodes C-quoted path %s without changing its diff lines", (encoded, path) => {
const files = parseDiff(
[
'diff --git "a/' + encoded + '" "b/' + encoded + '"',
"index 1234567..abcdefg 100644",
'--- "a/' + encoded + '"',
'+++ "b/' + encoded + '"',
"@@ -1 +1 @@",
"-before",
"+after",
"",
].join("\n"),
);
expect(files).toHaveLength(1);
expect(files[0]).toMatchObject({ path, additions: 1, deletions: 1 });
expect(files[0].hunks[0].lines).toEqual([
{ type: "header", content: "@@ -1 +1 @@" },
{ type: "remove", content: "before" },
{ type: "add", content: "after" },
]);
});
it.each([
['"a/old \\"draft\\".txt" b/new with spaces.txt', "new with spaces.txt"],
['a/old with spaces.txt "b/new \\"draft\\".txt"', 'new "draft".txt'],
['"report \\"draft\\".txt" "report \\"draft\\".txt"', 'report "draft".txt'],
])("reads quoted paths from header-only patches: %s", (header, path) => {
expect(parseDiff("diff --git " + header + "\nsimilarity index 100%\n")).toEqual([
{ path, isNew: false, isDeleted: false, additions: 0, deletions: 0, hunks: [] },
]);
});
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 + content lines
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("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]);
// Check that tokens are added
const hunk = highlighted.hunks[0];
// First content line: "const foo = 1;"
const constLine = hunk.lines[1];
expect(constLine.tokens).toBeDefined();
expect(constLine.tokens!.length).toBeGreaterThan(0);
// Should have a "keyword" token for "const"
const constToken = constLine.tokens!.find((t) => t.text === "const");
expect(constToken).toBeDefined();
expect(constToken!.style).toBe("keyword");
// Should have a "number" token for "1"
const numberToken = constLine.tokens!.find((t) => t.text === "1");
expect(numberToken).toBeDefined();
expect(numberToken!.style).toBe("number");
});
it("highlights both added and removed lines correctly", () => {
const files = parseDiff(SIMPLE_DIFF);
const highlighted = highlightDiffFromHunks(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).toBeDefined();
const removedNumber = removedLine!.tokens!.find((t) => t.text === "2");
expect(removedNumber?.style).toBe("number");
// Added line: "const bar = 3;"
const addedLine = hunk.lines.find((l) => l.type === "add" && l.content.includes("bar"));
expect(addedLine?.tokens).toBeDefined();
const addedNumber = addedLine!.tokens!.find((t) => t.text === "3");
expect(addedNumber?.style).toBe("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 = highlightDiffFromHunks(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 = highlightDiffFromHunks(files[0]);
const hunk = highlighted.hunks[0];
const fnLine = hunk.lines[1];
expect(fnLine.tokens).toBeDefined();
expect(fnLine.tokens!.some((t) => t.text === "fn" && t.style === "keyword")).toBe(true);
const addedLine = hunk.lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toBeDefined();
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
});
it("adds syntax highlighting tokens to C code", () => {
const files = parseDiff(C_DIFF);
const highlighted = highlightDiffFromHunks(files[0]);
const hunk = highlighted.hunks[0];
const mainLine = hunk.lines[1];
expect(mainLine.tokens).toBeDefined();
expect(mainLine.tokens!.length).toBeGreaterThan(0);
const addedLine = hunk.lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toBeDefined();
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
});
it("adds syntax highlighting tokens to Java code", () => {
const files = parseDiff(JAVA_DIFF);
const highlighted = highlightDiffFromHunks(files[0]);
const hunk = highlighted.hunks[0];
const classLine = hunk.lines[1];
expect(classLine.tokens).toBeDefined();
expect(classLine.tokens!.some((t) => t.text === "public" && t.style === "keyword")).toBe(true);
const addedLine = hunk.lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toBeDefined();
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
});
it("adds syntax highlighting tokens to Svelte components", () => {
const files = parseDiff(SVELTE_DIFF);
const highlighted = highlightDiffFromHunks(files[0]);
const hunk = highlighted.hunks[0];
const scriptLine = hunk.lines[1];
expect(scriptLine.tokens).toBeDefined();
expect(scriptLine.tokens!.some((t) => t.text === "script" && t.style === "tag")).toBe(true);
const addedLine = hunk.lines.find(
(line) => line.type === "add" && line.content.includes("count"),
);
expect(addedLine?.tokens).toBeDefined();
expect(addedLine!.tokens!.some((t) => t.text === "let" && t.style === "keyword")).toBe(true);
expect(addedLine!.tokens!.some((t) => t.text === "number" && t.style === "type")).toBe(true);
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
const blockLine = hunk.lines.find((line) => line.content.includes("{#if"));
expect(blockLine?.tokens).toBeDefined();
expect(blockLine!.tokens!.some((t) => t.text === "if" && t.style === "keyword")).toBe(true);
expect(blockLine!.tokens!.some((t) => t.text === "count" && t.style === "variable")).toBe(true);
const markupLine = hunk.lines.find((line) => line.content.includes("<span"));
expect(markupLine?.tokens).toBeDefined();
expect(markupLine!.tokens!.some((t) => t.text === "span" && t.style === "tag")).toBe(true);
});
it("adds syntax highlighting tokens to Astro components", () => {
const [file] = parseDiff(ASTRO_DIFF);
const highlighted = highlightDiffFromHunks(file);
const addedLine = highlighted.hunks[0].lines.find((line) => line.type === "add");
const markupLine = highlighted.hunks[0].lines.find((line) => line.content.includes("<main"));
expect(addedLine?.tokens).toContainEqual({ text: "const", style: "keyword" });
expect(addedLine?.tokens).toContainEqual({ text: '"Hello, Astro"', style: "string" });
expect(markupLine?.tokens).toContainEqual({ text: "main", style: "tag" });
expect(markupLine?.tokens).toContainEqual({ text: "class", style: "attribute" });
});
it("adds syntax highlighting tokens to Objective-C file extensions", () => {
const files = parseDiff(OBJECTIVE_C_DIFF);
const highlighted = highlightDiffFromHunks(files[0]);
const hunk = highlighted.hunks[0];
const addedLine = hunk.lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toBeDefined();
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
});
it("adds syntax highlighting tokens to Go code", () => {
const files = parseDiff(GO_DIFF);
const highlighted = highlightDiffFromHunks(files[0]);
const addedLine = highlighted.hunks[0].lines.find(
(line) => line.type === "add" && line.content.includes("version"),
);
expect(addedLine?.tokens).toBeDefined();
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
});
it("adds syntax highlighting tokens to PHP code", () => {
const files = parseDiff(PHP_DIFF);
const highlighted = highlightDiffFromHunks(files[0]);
const addedLine = highlighted.hunks[0].lines.find(
(line) => line.type === "add" && line.content.includes("$version"),
);
expect(addedLine?.tokens).toBeDefined();
expect(addedLine!.tokens!.some((t) => t.text === "2" && t.style === "number")).toBe(true);
});
it("adds syntax highlighting tokens to YAML code", () => {
const files = parseDiff(YAML_DIFF);
const highlighted = highlightDiffFromHunks(files[0]);
const addedLine = highlighted.hunks[0].lines.find(
(line) => line.type === "add" && line.content.includes("count"),
);
expect(addedLine?.tokens).toBeDefined();
expect(addedLine!.tokens!.length).toBeGreaterThan(0);
});
it("adds syntax highlighting tokens to XML code", () => {
const files = parseDiff(XML_DIFF);
const highlighted = highlightDiffFromHunks(files[0]);
const addedLine = highlighted.hunks[0].lines.find(
(line) => line.type === "add" && line.content.includes("<count>"),
);
expect(addedLine?.tokens).toBeDefined();
expect(addedLine!.tokens!.some((t) => t.style === "tag")).toBe(true);
});
});
describe("highlightDiffWithFileContent", () => {
it("renders an Astro diff with an empty expression and keeps surrounding highlighting", async () => {
const diff = ASTRO_DIFF.replace("{title}", "{title} {}");
const file = parseDiff(diff)[0];
const highlighted = await highlightDiffWithFileContent(file, ".", {
oldFileContent:
'---\nconst title = "Hello";\n---\n<main class="page">\n <h1>{title} {}</h1>\n</main>',
newFileContent:
'---\nconst title = "Hello, Astro";\n---\n<main class="page">\n <h1>{title} {}</h1>\n</main>',
});
expect(highlighted.hunks[0].lines.find((line) => line.type === "add")?.tokens).toContainEqual({
text: '"Hello, Astro"',
style: "string",
});
expect(highlighted.hunks[0].lines.map(({ content }) => content)).toEqual(
file.hunks[0].lines.map(({ content }) => content),
);
expect(
highlightDiffFromHunks(file).hunks[0].lines.find((line) => line.type === "add")?.tokens,
).toContainEqual({ text: '"Hello, Astro"', style: "string" });
});
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-"));
try {
const oldFileContent = `/*
comment line 1
comment line 2
comment line 3
comment line 4
comment line 5
comment line 6
old comment line
comment line 8
*/
const x = 1;
`;
const newFileContent = `/*
comment line 1
comment line 2
comment line 3
comment line 4
comment line 5
comment line 6
new comment line
comment line 8
*/
const x = 1;
`;
writeFileSync(join(cwd, "example.ts"), newFileContent, "utf8");
const diff = `diff --git a/example.ts b/example.ts
index 1111111..2222222 100644
--- a/example.ts
+++ b/example.ts
@@ -5,7 +5,7 @@
comment line 4
comment line 5
comment line 6
-old comment line
+new comment line
comment line 8
*/
const x = 1;
`;
const file = parseDiff(diff)[0];
const highlighted = await highlightDiffWithFileContent(file, cwd, {
oldFileContent,
});
const removedLine = highlighted.hunks[0].lines.find((line) => line.type === "remove");
const addedLine = highlighted.hunks[0].lines.find((line) => line.type === "add");
expect(addedLine?.tokens).toEqual([{ text: "new comment line", style: "comment" }]);
expect(removedLine?.tokens).toEqual([{ text: "old comment line", style: "comment" }]);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
});