diff --git a/bun.lock b/bun.lock index d72e1a4ec..1a64f9927 100644 --- a/bun.lock +++ b/bun.lock @@ -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", @@ -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", diff --git a/src/utils/highlighting/highlightDiffChunk.test.ts b/src/utils/highlighting/highlightDiffChunk.test.ts index 3245a55ae..43b28ff7e 100644 --- a/src/utils/highlighting/highlightDiffChunk.test.ts +++ b/src/utils/highlighting/highlightDiffChunk.test.ts @@ -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", () => { diff --git a/src/utils/highlighting/highlightDiffChunk.ts b/src/utils/highlighting/highlightDiffChunk.ts index cf1635331..ca41d361d 100644 --- a/src/utils/highlighting/highlightDiffChunk.ts +++ b/src/utils/highlighting/highlightDiffChunk.ts @@ -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) => ({ diff --git a/src/utils/highlighting/shikiHighlighter.ts b/src/utils/highlighting/shikiHighlighter.ts index ff8e7466e..b98a54e60 100644 --- a/src/utils/highlighting/shikiHighlighter.ts +++ b/src/utils/highlighting/shikiHighlighter.ts @@ -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