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.
This commit is contained in:
Mohamed Boudra
2026-11-06 08:14:39 +01:00
parent 04a7d1b3a7
commit 8b5527e7e6
2 changed files with 70 additions and 1 deletions
@@ -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 });
}
},
);
@@ -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.
*/