Просмотр исходного кода

fix: handle Windows CRLF line endings in grep tool (#5948)

Co-authored-by: Claude <[email protected]>
lif 2 месяцев назад
Родитель
Сommit
5af35117db

+ 4 - 2
packages/opencode/src/file/ripgrep.ts

@@ -240,7 +240,8 @@ export namespace Ripgrep {
         if (done) break
 
         buffer += decoder.decode(value, { stream: true })
-        const lines = buffer.split("\n")
+        // Handle both Unix (\n) and Windows (\r\n) line endings
+        const lines = buffer.split(/\r?\n/)
         buffer = lines.pop() || ""
 
         for (const line of lines) {
@@ -379,7 +380,8 @@ export namespace Ripgrep {
       return []
     }
 
-    const lines = result.text().trim().split("\n").filter(Boolean)
+    // Handle both Unix (\n) and Windows (\r\n) line endings
+    const lines = result.text().trim().split(/\r?\n/).filter(Boolean)
     // Parse JSON lines from ripgrep output
 
     return lines

+ 2 - 1
packages/opencode/src/tool/grep.ts

@@ -49,7 +49,8 @@ export const GrepTool = Tool.define("grep", {
       throw new Error(`ripgrep failed: ${errorOutput}`)
     }
 
-    const lines = output.trim().split("\n")
+    // Handle both Unix (\n) and Windows (\r\n) line endings
+    const lines = output.trim().split(/\r?\n/)
     const matches = []
 
     for (const line of lines) {

+ 108 - 0
packages/opencode/test/tool/grep.test.ts

@@ -0,0 +1,108 @@
+import { describe, expect, test } from "bun:test"
+import path from "path"
+import { GrepTool } from "../../src/tool/grep"
+import { Instance } from "../../src/project/instance"
+import { tmpdir } from "../fixture/fixture"
+
+const ctx = {
+  sessionID: "test",
+  messageID: "",
+  callID: "",
+  agent: "build",
+  abort: AbortSignal.any([]),
+  metadata: () => {},
+}
+
+const projectRoot = path.join(__dirname, "../..")
+
+describe("tool.grep", () => {
+  test("basic search", async () => {
+    await Instance.provide({
+      directory: projectRoot,
+      fn: async () => {
+        const grep = await GrepTool.init()
+        const result = await grep.execute(
+          {
+            pattern: "export",
+            path: path.join(projectRoot, "src/tool"),
+            include: "*.ts",
+          },
+          ctx,
+        )
+        expect(result.metadata.matches).toBeGreaterThan(0)
+        expect(result.output).toContain("Found")
+      },
+    })
+  })
+
+  test("no matches returns correct output", async () => {
+    await using tmp = await tmpdir({
+      init: async (dir) => {
+        await Bun.write(path.join(dir, "test.txt"), "hello world")
+      },
+    })
+    await Instance.provide({
+      directory: tmp.path,
+      fn: async () => {
+        const grep = await GrepTool.init()
+        const result = await grep.execute(
+          {
+            pattern: "xyznonexistentpatternxyz123",
+            path: tmp.path,
+          },
+          ctx,
+        )
+        expect(result.metadata.matches).toBe(0)
+        expect(result.output).toBe("No files found")
+      },
+    })
+  })
+
+  test("handles CRLF line endings in output", async () => {
+    // This test verifies the regex split handles both \n and \r\n
+    await using tmp = await tmpdir({
+      init: async (dir) => {
+        // Create a test file with content
+        await Bun.write(path.join(dir, "test.txt"), "line1\nline2\nline3")
+      },
+    })
+    await Instance.provide({
+      directory: tmp.path,
+      fn: async () => {
+        const grep = await GrepTool.init()
+        const result = await grep.execute(
+          {
+            pattern: "line",
+            path: tmp.path,
+          },
+          ctx,
+        )
+        expect(result.metadata.matches).toBeGreaterThan(0)
+      },
+    })
+  })
+})
+
+describe("CRLF regex handling", () => {
+  test("regex correctly splits Unix line endings", () => {
+    const unixOutput = "file1.txt|1|content1\nfile2.txt|2|content2\nfile3.txt|3|content3"
+    const lines = unixOutput.trim().split(/\r?\n/)
+    expect(lines.length).toBe(3)
+    expect(lines[0]).toBe("file1.txt|1|content1")
+    expect(lines[2]).toBe("file3.txt|3|content3")
+  })
+
+  test("regex correctly splits Windows CRLF line endings", () => {
+    const windowsOutput = "file1.txt|1|content1\r\nfile2.txt|2|content2\r\nfile3.txt|3|content3"
+    const lines = windowsOutput.trim().split(/\r?\n/)
+    expect(lines.length).toBe(3)
+    expect(lines[0]).toBe("file1.txt|1|content1")
+    expect(lines[2]).toBe("file3.txt|3|content3")
+  })
+
+  test("regex handles mixed line endings", () => {
+    const mixedOutput = "file1.txt|1|content1\nfile2.txt|2|content2\r\nfile3.txt|3|content3"
+    const lines = mixedOutput.trim().split(/\r?\n/)
+    expect(lines.length).toBe(3)
+  })
+})