|
|
@@ -451,6 +451,189 @@ describe("tool.edit", () => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
+ describe("line endings", () => {
|
|
|
+ const old = "alpha\nbeta\ngamma"
|
|
|
+ const next = "alpha\nbeta-updated\ngamma"
|
|
|
+ const alt = "alpha\nbeta\nomega"
|
|
|
+
|
|
|
+ const normalize = (text: string, ending: "\n" | "\r\n") => {
|
|
|
+ const normalized = text.replaceAll("\r\n", "\n")
|
|
|
+ if (ending === "\n") return normalized
|
|
|
+ return normalized.replaceAll("\n", "\r\n")
|
|
|
+ }
|
|
|
+
|
|
|
+ const count = (content: string) => {
|
|
|
+ const crlf = content.match(/\r\n/g)?.length ?? 0
|
|
|
+ const lf = content.match(/\n/g)?.length ?? 0
|
|
|
+ return {
|
|
|
+ crlf,
|
|
|
+ lf: lf - crlf,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const expectLf = (content: string) => {
|
|
|
+ const counts = count(content)
|
|
|
+ expect(counts.crlf).toBe(0)
|
|
|
+ expect(counts.lf).toBeGreaterThan(0)
|
|
|
+ }
|
|
|
+
|
|
|
+ const expectCrlf = (content: string) => {
|
|
|
+ const counts = count(content)
|
|
|
+ expect(counts.lf).toBe(0)
|
|
|
+ expect(counts.crlf).toBeGreaterThan(0)
|
|
|
+ }
|
|
|
+
|
|
|
+ type Input = {
|
|
|
+ content: string
|
|
|
+ oldString: string
|
|
|
+ newString: string
|
|
|
+ replaceAll?: boolean
|
|
|
+ }
|
|
|
+
|
|
|
+ const apply = async (input: Input) => {
|
|
|
+ await using tmp = await tmpdir({
|
|
|
+ init: async (dir) => {
|
|
|
+ await Bun.write(path.join(dir, "test.txt"), input.content)
|
|
|
+ },
|
|
|
+ })
|
|
|
+
|
|
|
+ return await Instance.provide({
|
|
|
+ directory: tmp.path,
|
|
|
+ fn: async () => {
|
|
|
+ const edit = await EditTool.init()
|
|
|
+ const filePath = path.join(tmp.path, "test.txt")
|
|
|
+ FileTime.read(ctx.sessionID, filePath)
|
|
|
+ await edit.execute(
|
|
|
+ {
|
|
|
+ filePath,
|
|
|
+ oldString: input.oldString,
|
|
|
+ newString: input.newString,
|
|
|
+ replaceAll: input.replaceAll,
|
|
|
+ },
|
|
|
+ ctx,
|
|
|
+ )
|
|
|
+ return await Bun.file(filePath).text()
|
|
|
+ },
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ test("preserves LF with LF multi-line strings", async () => {
|
|
|
+ const content = normalize(old + "\n", "\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: normalize(old, "\n"),
|
|
|
+ newString: normalize(next, "\n"),
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(next + "\n", "\n"))
|
|
|
+ expectLf(output)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("preserves CRLF with CRLF multi-line strings", async () => {
|
|
|
+ const content = normalize(old + "\n", "\r\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: normalize(old, "\r\n"),
|
|
|
+ newString: normalize(next, "\r\n"),
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(next + "\n", "\r\n"))
|
|
|
+ expectCrlf(output)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("preserves LF when old/new use CRLF", async () => {
|
|
|
+ const content = normalize(old + "\n", "\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: normalize(old, "\r\n"),
|
|
|
+ newString: normalize(next, "\r\n"),
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(next + "\n", "\n"))
|
|
|
+ expectLf(output)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("preserves CRLF when old/new use LF", async () => {
|
|
|
+ const content = normalize(old + "\n", "\r\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: normalize(old, "\n"),
|
|
|
+ newString: normalize(next, "\n"),
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(next + "\n", "\r\n"))
|
|
|
+ expectCrlf(output)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("preserves LF when newString uses CRLF", async () => {
|
|
|
+ const content = normalize(old + "\n", "\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: normalize(old, "\n"),
|
|
|
+ newString: normalize(next, "\r\n"),
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(next + "\n", "\n"))
|
|
|
+ expectLf(output)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("preserves CRLF when newString uses LF", async () => {
|
|
|
+ const content = normalize(old + "\n", "\r\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: normalize(old, "\r\n"),
|
|
|
+ newString: normalize(next, "\n"),
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(next + "\n", "\r\n"))
|
|
|
+ expectCrlf(output)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("preserves LF with mixed old/new line endings", async () => {
|
|
|
+ const content = normalize(old + "\n", "\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: "alpha\nbeta\r\ngamma",
|
|
|
+ newString: "alpha\r\nbeta\nomega",
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(alt + "\n", "\n"))
|
|
|
+ expectLf(output)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("preserves CRLF with mixed old/new line endings", async () => {
|
|
|
+ const content = normalize(old + "\n", "\r\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: "alpha\r\nbeta\ngamma",
|
|
|
+ newString: "alpha\nbeta\r\nomega",
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(alt + "\n", "\r\n"))
|
|
|
+ expectCrlf(output)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("replaceAll preserves LF for multi-line blocks", async () => {
|
|
|
+ const blockOld = "alpha\nbeta"
|
|
|
+ const blockNew = "alpha\nbeta-updated"
|
|
|
+ const content = normalize(blockOld + "\n" + blockOld + "\n", "\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: normalize(blockOld, "\n"),
|
|
|
+ newString: normalize(blockNew, "\n"),
|
|
|
+ replaceAll: true,
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\n"))
|
|
|
+ expectLf(output)
|
|
|
+ })
|
|
|
+
|
|
|
+ test("replaceAll preserves CRLF for multi-line blocks", async () => {
|
|
|
+ const blockOld = "alpha\nbeta"
|
|
|
+ const blockNew = "alpha\nbeta-updated"
|
|
|
+ const content = normalize(blockOld + "\n" + blockOld + "\n", "\r\n")
|
|
|
+ const output = await apply({
|
|
|
+ content,
|
|
|
+ oldString: normalize(blockOld, "\r\n"),
|
|
|
+ newString: normalize(blockNew, "\r\n"),
|
|
|
+ replaceAll: true,
|
|
|
+ })
|
|
|
+ expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\r\n"))
|
|
|
+ expectCrlf(output)
|
|
|
+ })
|
|
|
+ })
|
|
|
+
|
|
|
describe("concurrent editing", () => {
|
|
|
test("serializes concurrent edits to same file", async () => {
|
|
|
await using tmp = await tmpdir()
|