From 8b5527e7e6f7ed1b745aea25ec637cb74f030193 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 6 Nov 2026 08:14:39 +0100 Subject: [PATCH] fix(diff): align highlight tokens with normalized patch lines Git can normalize CRLF worktrees while highlighting reads their original line endings. Match tokens to the emitted patch text and trim only a normalization CR before encoding runs; mismatched source tokens are not attached. --- .../src/server/diff/subscriptions.test.ts | 49 +++++++++++++++++++ .../src/server/utils/diff-highlighter.ts | 22 ++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/diff/subscriptions.test.ts b/packages/server/src/server/diff/subscriptions.test.ts index fce926a3fc..8df95de909 100644 --- a/packages/server/src/server/diff/subscriptions.test.ts +++ b/packages/server/src/server/diff/subscriptions.test.ts @@ -485,3 +485,52 @@ test.each([true, false])( } }, ); + +test.each([true, false])( + "autocrlf=%s highlighted CRLF worktrees keep the revision readable", + async (autocrlf) => { + const cwd = await mkdtemp(join(tmpdir(), "diff-crlf-")); + const git = (...args: string[]) => execFileSync("git", args, { cwd, encoding: "utf8" }).trim(); + const streams = new DiffSubscriptions({ + async requestWorkingTreeWatch(root) { + return { repoRoot: root, unsubscribe: async () => {} }; + }, + }); + const frames: DiffFrame[] = []; + try { + git("init", "-b", "main"); + git("config", "user.name", "Test"); + git("config", "user.email", "test@example.com"); + git("config", "core.autocrlf", String(autocrlf)); + await writeFile(join(cwd, "a.ts"), "export const value = 1;\r\n"); + await writeFile(join(cwd, "b.txt"), "before\n"); + git("add", "."); + git("-c", "commit.gpgsign=false", "commit", "-m", "base"); + await writeFile(join(cwd, "a.ts"), "export const value = 2;\r\n"); + await writeFile(join(cwd, "b.txt"), "after\n"); + const subscription = await streams.subscribe( + { cwd, base: { kind: "rev", rev: "HEAD" }, head: { kind: "worktree" } }, + async (frame) => { + frames.push(frame); + }, + ); + await subscription.start(); + expect(frames.at(-1)).toMatchObject({ opcode: DiffOpcode.End, error: null }); + const files = frames.flatMap((frame) => + frame.opcode === DiffOpcode.Bodies ? frame.files : [], + ); + expect(files.map((file) => file.path)).toEqual(["a.ts", "b.txt"]); + const line = files[0].hunks + .flatMap((hunk) => hunk.lines) + .find((candidate) => candidate.type === "add"); + expect(line?.content).toBe(`export const value = 2;${autocrlf ? "" : "\r"}`); + expect(line?.runs?.reduce((length, run) => length + run.length, 0)).toBe( + line?.content.length, + ); + await subscription.release(); + } finally { + await streams.close(); + await rm(cwd, { recursive: true, force: true }); + } + }, +); diff --git a/packages/server/src/server/utils/diff-highlighter.ts b/packages/server/src/server/utils/diff-highlighter.ts index 58d381c468..6404ce2381 100644 --- a/packages/server/src/server/utils/diff-highlighter.ts +++ b/packages/server/src/server/utils/diff-highlighter.ts @@ -468,7 +468,8 @@ function applyTokensToHunks( newLineNum++; } - return tokens ? { ...line, tokens } : line; + const matchingTokens = tokensForPatchLine(tokens, line.content); + return matchingTokens ? { ...line, tokens: matchingTokens } : line; }); return { ...hunk, lines: highlightedLines }; @@ -477,6 +478,25 @@ function applyTokensToHunks( return { ...file, hunks: highlightedHunks }; } +function tokensForPatchLine( + tokens: HighlightToken[] | undefined, + content: string, +): HighlightToken[] | undefined { + if (!tokens) return undefined; + const source = tokens.map((token) => token.text).join(""); + if (source === content) return tokens; + // Git can normalize a CRLF worktree while source highlighting retains its CR. + if (source !== `${content}\r`) return undefined; + const aligned: HighlightToken[] = []; + let remaining = content.length; + for (const token of tokens) { + const text = token.text.slice(0, remaining); + if (text) aligned.push({ text, style: token.style }); + remaining -= text.length; + } + return aligned; +} + /** * Parse and highlight a complete diff, using actual file content when available. */