From fcb90d92a4a75d7381283c05d7f4afd519d74121 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Tue, 24 Feb 2026 22:30:06 +0900 Subject: [PATCH] refactor(hashline-edit): replace custom diff with diff library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with assistance of [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode) --- bun.lock | 3 + package.json | 1 + src/tools/hashline-edit/diff-utils.test.ts | 71 ++++++++++++++++++++++ src/tools/hashline-edit/diff-utils.ts | 55 +---------------- 4 files changed, 77 insertions(+), 53 deletions(-) create mode 100644 src/tools/hashline-edit/diff-utils.test.ts diff --git a/bun.lock b/bun.lock index c46e3f032..1c5d84e94 100644 --- a/bun.lock +++ b/bun.lock @@ -14,6 +14,7 @@ "@opencode-ai/sdk": "^1.1.19", "commander": "^14.0.2", "detect-libc": "^2.0.0", + "diff": "^8.0.3", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "picocolors": "^1.1.1", @@ -138,6 +139,8 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], diff --git a/package.json b/package.json index 985855bb8..0559493b7 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@opencode-ai/sdk": "^1.1.19", "commander": "^14.0.2", "detect-libc": "^2.0.0", + "diff": "^8.0.3", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "picocolors": "^1.1.1", diff --git a/src/tools/hashline-edit/diff-utils.test.ts b/src/tools/hashline-edit/diff-utils.test.ts new file mode 100644 index 000000000..c3373d995 --- /dev/null +++ b/src/tools/hashline-edit/diff-utils.test.ts @@ -0,0 +1,71 @@ +/// +import { describe, expect, it } from "bun:test" +import { generateUnifiedDiff } from "./diff-utils" + +function createNumberedLines(totalLineCount: number): string { + return Array.from({ length: totalLineCount }, (_, index) => `line ${index + 1}`).join("\n") +} + +describe("generateUnifiedDiff", () => { + it("creates separate hunks for distant changes", () => { + //#given + const oldContent = createNumberedLines(60) + const newLines = oldContent.split("\n") + newLines[4] = "line 5 updated" + newLines[49] = "line 50 updated" + const newContent = newLines.join("\n") + + //#when + const diff = generateUnifiedDiff(oldContent, newContent, "sample.txt") + + //#then + const hunkHeaders = diff.match(/^@@/gm) ?? [] + expect(hunkHeaders.length).toBe(2) + }) + + it("creates a single hunk for adjacent changes", () => { + //#given + const oldContent = createNumberedLines(20) + const newLines = oldContent.split("\n") + newLines[9] = "line 10 updated" + newLines[10] = "line 11 updated" + const newContent = newLines.join("\n") + + //#when + const diff = generateUnifiedDiff(oldContent, newContent, "sample.txt") + + //#then + const hunkHeaders = diff.match(/^@@/gm) ?? [] + expect(hunkHeaders.length).toBe(1) + expect(diff).toContain(" line 8") + expect(diff).toContain(" line 13") + }) + + it("returns a diff string for identical content", () => { + //#given + const oldContent = "alpha\nbeta\ngamma" + const newContent = "alpha\nbeta\ngamma" + + //#when + const diff = generateUnifiedDiff(oldContent, newContent, "sample.txt") + + //#then + expect(typeof diff).toBe("string") + expect(diff).toContain("--- sample.txt") + expect(diff).toContain("+++ sample.txt") + }) + + it("returns a valid diff when old content is empty", () => { + //#given + const oldContent = "" + const newContent = "first line\nsecond line" + + //#when + const diff = generateUnifiedDiff(oldContent, newContent, "sample.txt") + + //#then + expect(diff).toContain("--- sample.txt") + expect(diff).toContain("+++ sample.txt") + expect(diff).toContain("+first line") + }) +}) diff --git a/src/tools/hashline-edit/diff-utils.ts b/src/tools/hashline-edit/diff-utils.ts index 7104a3a42..975438d27 100644 --- a/src/tools/hashline-edit/diff-utils.ts +++ b/src/tools/hashline-edit/diff-utils.ts @@ -1,3 +1,4 @@ +import { createTwoFilesPatch } from "diff" import { computeLineHash } from "./hash-computation" export function toHashlineContent(content: string): string { @@ -15,59 +16,7 @@ export function toHashlineContent(content: string): string { } export function generateUnifiedDiff(oldContent: string, newContent: string, filePath: string): string { - const oldLines = oldContent.split("\n") - const newLines = newContent.split("\n") - const maxLines = Math.max(oldLines.length, newLines.length) - - let diff = `--- ${filePath}\n+++ ${filePath}\n` - let inHunk = false - let oldStart = 1 - let newStart = 1 - let oldCount = 0 - let newCount = 0 - let hunkLines: string[] = [] - - for (let i = 0; i < maxLines; i++) { - const oldLine = oldLines[i] ?? "" - const newLine = newLines[i] ?? "" - - if (oldLine !== newLine) { - if (!inHunk) { - oldStart = i + 1 - newStart = i + 1 - oldCount = 0 - newCount = 0 - hunkLines = [] - inHunk = true - } - - if (oldLines[i] !== undefined) { - hunkLines.push(`-${oldLine}`) - oldCount++ - } - if (newLines[i] !== undefined) { - hunkLines.push(`+${newLine}`) - newCount++ - } - } else if (inHunk) { - hunkLines.push(` ${oldLine}`) - oldCount++ - newCount++ - - if (hunkLines.length > 6) { - diff += `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@\n` - diff += hunkLines.join("\n") + "\n" - inHunk = false - } - } - } - - if (inHunk && hunkLines.length > 0) { - diff += `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@\n` - diff += hunkLines.join("\n") + "\n" - } - - return diff || `--- ${filePath}\n+++ ${filePath}\n` + return createTwoFilesPatch(filePath, filePath, oldContent, newContent) } export function countLineDiffs(oldContent: string, newContent: string): { additions: number; deletions: number } {