Ver Fonte

fix(core): reuse shared patch diff (#41186)

Kit Langton há 1 semana atrás
pai
commit
8c758e443b

+ 9 - 15
packages/core/src/tool/plugin/patch.ts

@@ -3,7 +3,6 @@ export * as PatchTool from "./patch"
 import type { Context as PluginContext } from "@opencode-ai/plugin/effect/plugin"
 import { ToolFailure } from "@opencode-ai/ai"
 import { FileDiff } from "@opencode-ai/schema/file-diff"
-import { createTwoFilesPatch, diffLines } from "diff"
 import { Effect, Result, Schema } from "effect"
 import path from "path"
 import { Bom } from "@opencode-ai/util/bom"
@@ -15,6 +14,7 @@ import { Location } from "../../location"
 import { Patch } from "@opencode-ai/util/patch"
 import { Permission } from "../../permission"
 import DESCRIPTION from "../patch.txt"
+import { fileDiff } from "./file-diff"
 
 export const name = "patch"
 
@@ -353,22 +353,16 @@ function errorMessage(error: unknown) {
 
 function patchFile(change: Prepared, after = change.after): typeof FileDiff.Info.Type {
   const target = (change.type === "update" ? change.moveTarget : undefined)?.resource ?? change.target.resource
-  const patch = trimDiff(createTwoFilesPatch(change.target.absolute, change.target.absolute, change.before, after))
-  const counts =
-    change.type === "delete"
-      ? { additions: 0, deletions: change.before.split("\n").length }
-      : diffLines(change.before, after).reduce(
-          (result, item) => ({
-            additions: result.additions + (item.added ? (item.count ?? 0) : 0),
-            deletions: result.deletions + (item.removed ? (item.count ?? 0) : 0),
-          }),
-          { additions: 0, deletions: 0 },
-        )
+  const diff = fileDiff(
+    change.target.absolute,
+    change.before,
+    after,
+    change.type === "add" ? "added" : change.type === "delete" ? "deleted" : "modified",
+  )
   return {
+    ...diff,
     file: target,
-    patch,
-    status: change.type === "add" ? "added" : change.type === "delete" ? "deleted" : "modified",
-    ...counts,
+    patch: trimDiff(diff.patch),
   }
 }
 

+ 25 - 2
packages/core/test/tool-patch.test.ts

@@ -215,7 +215,7 @@ describe("PatchTool", () => {
                       file: "remove.txt",
                       status: "deleted",
                       additions: 0,
-                      deletions: 2,
+                      deletions: 1,
                       patch: expect.stringContaining("-remove"),
                     },
                   ],
@@ -248,6 +248,29 @@ describe("PatchTool", () => {
     ),
   )
 
+  it.live("counts deleted lines with and without a trailing newline", () =>
+    withTempTool((directory, registry) =>
+      Effect.gen(function* () {
+        yield* Effect.promise(() =>
+          Promise.all([
+            fs.writeFile(path.join(directory, "trailing.txt"), "remove\n"),
+            fs.writeFile(path.join(directory, "unterminated.txt"), "remove"),
+          ]),
+        )
+        const settled = yield* executeTool(
+          registry,
+          call("*** Begin Patch\n*** Delete File: trailing.txt\n*** Delete File: unterminated.txt\n*** End Patch"),
+        )
+        expect(settled.status).toBe("completed")
+        if (settled.status !== "completed") return
+        expect(settled.output.files).toMatchObject([
+          { file: "trailing.txt", additions: 0, deletions: 1 },
+          { file: "unterminated.txt", additions: 0, deletions: 1 },
+        ])
+      }),
+    ),
+  )
+
   it.live("serializes concurrent patch transactions", () =>
     withTempTool((directory, registry) => {
       const target = path.join(directory, "concurrent.txt")
@@ -446,7 +469,7 @@ describe("PatchTool", () => {
             {
               file: "renamed/dir/name.txt",
               status: "modified",
-              patch: expect.stringContaining("-old content\n+new content"),
+              patch: expect.stringContaining(`Index: ${source}`),
             },
           ],
         })