Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"crc-32": "^1.2.2",
"diff": "^8.0.2",
"disposablestack": "^1.1.7",
"electron": "^38.2.1",
"electron-updater": "^6.6.2",
"express": "^5.1.0",
"jsonc-parser": "^3.3.1",
Expand Down Expand Up @@ -61,6 +60,7 @@
"cmdk": "^1.0.0",
"concurrently": "^8.2.0",
"dotenv": "^17.2.3",
"electron": "^38.2.1",
"electron-builder": "^24.6.0",
"electron-devtools-installer": "^4.0.0",
"electron-mock-ipc": "^0.3.12",
Expand Down
61 changes: 61 additions & 0 deletions src/utils/highlighting/highlightDiffChunk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,67 @@ describe("highlightDiffChunk", () => {
expect(result.type).toBe("remove");
expect(result.lines[0].originalIndex).toBe(5);
});

it("should preserve leading whitespace in plain text", async () => {
const indentedChunk: DiffChunk = {
type: "add",
lines: [" const x = 1;", " const y = 2;", " const z = 3;"],
startIndex: 0,
lineNumbers: [1, 2, 3],
};

const result = await highlightDiffChunk(indentedChunk, "text");

expect(result.lines).toHaveLength(3);
// Leading spaces should be preserved
expect(result.lines[0].html).toMatch(/^(\s| ){4}/); // 4 leading spaces
expect(result.lines[1].html).toMatch(/^(\s| ){8}/); // 8 leading spaces
expect(result.lines[2].html).toMatch(/^(\s| ){2}/); // 2 leading spaces
});

it("should preserve internal whitespace in plain text", async () => {
const spacedChunk: DiffChunk = {
type: "add",
lines: ["const x = 1;", "if (x && y)"],
startIndex: 0,
lineNumbers: [1, 2],
};

const result = await highlightDiffChunk(spacedChunk, "text");

expect(result.lines).toHaveLength(2);
// Multiple internal spaces should be preserved
expect(result.lines[0].html).toContain("x");
expect(result.lines[0].html).toContain("1");
// Should have multiple spaces between x and = (2 spaces)
expect(result.lines[0].html).toMatch(/x(\s| ){2}=/);
});

it("should detect and fallback when HTML extraction returns empty strings", async () => {
// This is a regression test for the whitespace bug where extractLinesFromHtml
// could return empty strings without triggering fallback
// We can't easily mock Shiki to produce malformed HTML, but we can test
// that indented code preserves its whitespace
const chunk: DiffChunk = {
type: "add",
lines: [" const x = 1;", " const y = 2;", " const z = 3;"],
startIndex: 0,
lineNumbers: [1, 2, 3],
};

const result = await highlightDiffChunk(chunk, "typescript");

// Verify whitespace is preserved
expect(result.lines).toHaveLength(3);
expect(result.lines[0].html.length).toBeGreaterThan(0);
expect(result.lines[1].html.length).toBeGreaterThan(0);
expect(result.lines[2].html.length).toBeGreaterThan(0);

// All lines should contain "const"
expect(result.lines[0].html).toContain("const");
expect(result.lines[1].html).toContain("const");
expect(result.lines[2].html).toContain("const");
});
});

describe("with real Shiki syntax highlighting", () => {
Expand Down
9 changes: 9 additions & 0 deletions src/utils/highlighting/highlightDiffChunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ export async function highlightDiffChunk(
return createFallbackChunk(chunk);
}

// Check if any non-empty line became empty after extraction (indicates malformed HTML)
// This prevents rendering empty spans when original line had content (especially whitespace)
const hasEmptyExtraction = lines.some(
(extractedHtml, i) => extractedHtml.length === 0 && chunk.lines[i].length > 0
);
if (hasEmptyExtraction) {
return createFallbackChunk(chunk);
}

return {
type: chunk.type,
lines: lines.map((html, i) => ({
Expand Down
2 changes: 1 addition & 1 deletion src/utils/highlighting/shikiHighlighter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const SHIKI_THEME = "min-dark";

// Maximum diff size to highlight (in bytes)
// Diffs larger than this will fall back to plain text for performance
export const MAX_DIFF_SIZE_BYTES = 4096; // 4kb
export const MAX_DIFF_SIZE_BYTES = 32768; // 32kb

// Singleton promise (cached to prevent race conditions)
// Multiple concurrent calls will await the same Promise
Expand Down