From 82da719c80a71c59dbbe6601dea32ee3473f1639 Mon Sep 17 00:00:00 2001 From: leizd <1239373022@qq.com> Date: Mon, 3 Aug 2026 08:47:58 +0800 Subject: [PATCH] fix(opencode): match canonically equivalent unicode in patches --- packages/opencode/src/patch/index.ts | 12 +++++- packages/opencode/test/patch/patch.test.ts | 49 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/patch/index.ts b/packages/opencode/src/patch/index.ts index 66797d4f89ba..befbc74b5c85 100644 --- a/packages/opencode/src/patch/index.ts +++ b/packages/opencode/src/patch/index.ts @@ -480,7 +480,17 @@ function seekSequence(lines: string[], pattern: string[], startIndex: number, eo (a, b) => normalizeUnicode(a.trim()) === normalizeUnicode(b.trim()), eof, ) - return normalized + if (normalized !== -1) return normalized + + // Pass 5: NFC normalization (canonical Unicode equivalence) + const canonical = tryMatch( + lines, + pattern, + startIndex, + (a, b) => a.normalize("NFC").trim() === b.normalize("NFC").trim(), + eof, + ) + return canonical } function generateUnifiedDiff(oldContent: string, newContent: string): string { diff --git a/packages/opencode/test/patch/patch.test.ts b/packages/opencode/test/patch/patch.test.ts index 0a8dd5d8ad04..920adcf439d3 100644 --- a/packages/opencode/test/patch/patch.test.ts +++ b/packages/opencode/test/patch/patch.test.ts @@ -139,6 +139,55 @@ PATCH` }) }) + describe("canonical Unicode matching", () => { + test("matches Cyrillic text when the file is NFD and the patch is NFC", () => { + const original = "Район: Астана\n".normalize("NFD") + const result = Patch.deriveNewContentsFromChunks( + "summary.md", + [ + { + old_lines: ["Район: Астана".normalize("NFC")], + new_lines: ["Район: Алматы"], + }, + ], + original, + ) + + expect(result.content).toBe("Район: Алматы\n") + }) + + test("matches Latin text when the file is NFC and the patch is NFD", () => { + const replacement = "touché".normalize("NFD") + const result = Patch.deriveNewContentsFromChunks( + "notes.md", + [ + { + old_lines: ["café".normalize("NFD")], + new_lines: [replacement], + }, + ], + "café\n".normalize("NFC"), + ) + + expect(result.content).toBe(replacement + "\n") + }) + + test("does not match genuinely different Unicode text", () => { + expect(() => + Patch.deriveNewContentsFromChunks( + "notes.md", + [ + { + old_lines: ["cafe"], + new_lines: ["updated"], + }, + ], + "café\n", + ), + ).toThrow("Failed to find expected lines") + }) + }) + describe("applyPatch", () => { it.live("should add a new file", () => Effect.gen(function* () {