summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorlif <[email protected]>2025-12-23 12:26:15 +0800
committerGitHub <[email protected]>2025-12-22 22:26:15 -0600
commit5af35117db705b71604cc3e6a4ed07fc9bbf28e7 (patch)
tree1731f88f335b49e5e2654f192af43e57fd63e2eb
parenteab177f5e7ae91bdd7679867b43f77a479aefb74 (diff)
downloadopencode-5af35117db705b71604cc3e6a4ed07fc9bbf28e7.tar.gz
opencode-5af35117db705b71604cc3e6a4ed07fc9bbf28e7.zip
fix: handle Windows CRLF line endings in grep tool (#5948)
Co-authored-by: Claude <[email protected]>
-rw-r--r--packages/opencode/src/file/ripgrep.ts6
-rw-r--r--packages/opencode/src/tool/grep.ts3
-rw-r--r--packages/opencode/test/tool/grep.test.ts108
3 files changed, 114 insertions, 3 deletions
diff --git a/packages/opencode/src/file/ripgrep.ts b/packages/opencode/src/file/ripgrep.ts
index 00d9e8c38..22b714b85 100644
--- a/packages/opencode/src/file/ripgrep.ts
+++ b/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
diff --git a/packages/opencode/src/tool/grep.ts b/packages/opencode/src/tool/grep.ts
index 99af448ba..d73bc1616 100644
--- a/packages/opencode/src/tool/grep.ts
+++ b/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) {
diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts
new file mode 100644
index 000000000..f3da666a0
--- /dev/null
+++ b/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)
+ })
+})