diff --git a/src/core/diff/strategies/__tests__/html-entity-handling.spec.ts b/src/core/diff/strategies/__tests__/html-entity-handling.spec.ts new file mode 100644 index 0000000000..9c964e4e85 --- /dev/null +++ b/src/core/diff/strategies/__tests__/html-entity-handling.spec.ts @@ -0,0 +1,172 @@ +import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace" + +describe("HTML entity handling", () => { + let strategy: MultiSearchReplaceDiffStrategy + + beforeEach(() => { + strategy = new MultiSearchReplaceDiffStrategy() + }) + + it("should distinguish between HTML entities and their literal characters", async () => { + const originalContent = `.FilterBatch<int>(batch => batch.Count == 3) +.MapBatch<int, int>(batch => batch.Sum())` + + const diffContent = ` +<<<<<<< SEARCH +.FilterBatch<int>(batch => batch.Count == 3) +======= +.FilterBatch(batch => batch.Count == 3) +>>>>>>> REPLACE + +<<<<<<< SEARCH +.MapBatch<int, int>(batch => batch.Sum()) +======= +.MapBatch(batch => batch.Sum()) +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toBe(`.FilterBatch(batch => batch.Count == 3) +.MapBatch(batch => batch.Sum())`) + } + }) + + it("should not treat < and < as identical in search/replace comparison", async () => { + const originalContent = `public List<string> GetItems() { + return new List<string>(); +}` + + const diffContent = ` +<<<<<<< SEARCH +public List<string> GetItems() { + return new List<string>(); +} +======= +public List GetItems() { + return new List(); +} +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toBe(`public List GetItems() { + return new List(); +}`) + } + }) + + it("should handle mixed HTML entities correctly", async () => { + const originalContent = `<div class="container"> + <p>Hello & welcome</p> +</div>` + + const diffContent = ` +<<<<<<< SEARCH +<div class="container"> + <p>Hello & welcome</p> +</div> +======= +
+

Hello & welcome

+
+>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toBe(`
+

Hello & welcome

+
`) + } + }) + + it("should reject when search and replace are identical (including HTML entities)", async () => { + const originalContent = `function test() { + return value; +}` + + // Both search and replace have the same content (literal angle brackets) + const diffContent = ` +<<<<<<< SEARCH +function test() { + return value; +} +======= +function test() { + return value; +} +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(false) + if (!result.success && result.error) { + expect(result.error).toContain("Search and replace content are identical") + } + }) + + it("should handle apostrophes and quotes with HTML entities", async () => { + const originalContent = `const message = 'It's a "test" message';` + + const diffContent = ` +<<<<<<< SEARCH +const message = 'It's a "test" message'; +======= +const message = 'It\'s a "test" message'; +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toBe(`const message = 'It\'s a "test" message';`) + } + }) + + it("should handle C# generics with escaped HTML entities", async () => { + const originalContent = `var dict = new Dictionary<string, List<int>>(); +dict.Add("key", new List<int> { 1, 2, 3 });` + + const diffContent = ` +<<<<<<< SEARCH +var dict = new Dictionary<string, List<int>>(); +dict.Add("key", new List<int> { 1, 2, 3 }); +======= +var dict = new Dictionary>(); +dict.Add("key", new List { 1, 2, 3 }); +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toBe(`var dict = new Dictionary>(); +dict.Add("key", new List { 1, 2, 3 });`) + } + }) + + it("should handle the exact issue from bug report", async () => { + const originalContent = ` .FilterBatch<int>(batch => batch.Count == 3) + .MapBatch<int, int>(batch => batch.Sum())` + + // This is the exact diff that was failing before the fix + const diffContent = ` +<<<<<<< SEARCH + .FilterBatch<int>(batch => batch.Count == 3) +======= + .FilterBatch(batch => batch.Count == 3) +>>>>>>> REPLACE + +<<<<<<< SEARCH + .MapBatch<int, int>(batch => batch.Sum()) +======= + .MapBatch(batch => batch.Sum()) +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toBe(` .FilterBatch(batch => batch.Count == 3) + .MapBatch(batch => batch.Sum())`) + } + }) +}) diff --git a/src/core/diff/strategies/multi-file-search-replace.ts b/src/core/diff/strategies/multi-file-search-replace.ts index a212cf2b8e..57b90326aa 100644 --- a/src/core/diff/strategies/multi-file-search-replace.ts +++ b/src/core/diff/strategies/multi-file-search-replace.ts @@ -492,6 +492,10 @@ Each file requires its own path, start_line, and diff elements. let { searchContent, replaceContent } = replacement let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta) + // Store original content for comparison before any transformations + const originalSearchContent = searchContent + const originalReplaceContent = replaceContent + // First unescape any escaped markers in the content searchContent = this.unescapeMarkers(searchContent) replaceContent = this.unescapeMarkers(replaceContent) @@ -511,7 +515,8 @@ Each file requires its own path, start_line, and diff elements. } // Validate that search and replace content are not identical - if (searchContent === replaceContent) { + // Compare the original content to preserve HTML entities distinction + if (originalSearchContent === originalReplaceContent) { diffResults.push({ success: false, error: diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index a6a9913203..87c40ad9a1 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -409,6 +409,10 @@ Only use a single line of '=======' between search and replacement content, beca let { searchContent, replaceContent } = replacement let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta) + // Store original content for comparison before any transformations + const originalSearchContent = searchContent + const originalReplaceContent = replaceContent + // First unescape any escaped markers in the content searchContent = this.unescapeMarkers(searchContent) replaceContent = this.unescapeMarkers(replaceContent) @@ -428,7 +432,8 @@ Only use a single line of '=======' between search and replacement content, beca } // Validate that search and replace content are not identical - if (searchContent === replaceContent) { + // Compare the original content to preserve HTML entities distinction + if (originalSearchContent === originalReplaceContent) { diffResults.push({ success: false, error: