Skip to content

Commit 837860e

Browse files
committed
fix(tools): create new files with the platform-native line ending
A newly created file used LF whenever the model's content carried no CRLF, so files written on Windows ended up with LF endings while native tooling uses CRLF. Existing files were unaffected: their recorded line endings already win. platformLineEnding() (file-utils) now supplies the default for created files, mirroring the existing detectLineEndings() helper for reads. On LF platforms this is a no-op. Tests cover the CRLF/LF branches of the helper, a created file matching the platform ending, and an existing CRLF file keeping its endings.
1 parent c42e68e commit 837860e

3 files changed

Lines changed: 88 additions & 1 deletion

File tree

‎packages/core/src/common/file-utils.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as fs from "fs";
2+
import * as os from "os";
23
import * as path from "path";
34
import type { FileState, FileLineEnding } from "./state";
45

@@ -17,6 +18,20 @@ export function detectLineEndings(value: string): FileLineEnding {
1718
return value.includes("\r\n") ? "CRLF" : "LF";
1819
}
1920

21+
/**
22+
* Line ending a newly created file should use: the platform-native one.
23+
*
24+
* Created files have no existing EOL to preserve, and models emit LF-only text.
25+
* Writing that verbatim produces LF files on Windows, where native tooling (and
26+
* the files the user's editor creates) use CRLF. Existing files are unaffected:
27+
* their recorded line endings still win in the write handler.
28+
*
29+
* @param eol platform line ending, injectable for tests
30+
*/
31+
export function platformLineEnding(eol: string = os.EOL): FileLineEnding {
32+
return eol === "\r\n" ? "CRLF" : "LF";
33+
}
34+
2035
export function detectEncoding(buffer: Buffer): BufferEncoding {
2136
if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) {
2237
return "utf16le";
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { afterEach, test } from "node:test";
2+
import assert from "node:assert/strict";
3+
import * as fs from "fs";
4+
import * as os from "os";
5+
import * as path from "path";
6+
import type { ToolExecutionContext } from "../tools/executor";
7+
import { handleReadTool } from "../tools/read-handler";
8+
import { handleWriteTool } from "../tools/write-handler";
9+
import { platformLineEnding } from "../common/file-utils";
10+
11+
const tempDirs: string[] = [];
12+
13+
afterEach(() => {
14+
while (tempDirs.length > 0) {
15+
const dir = tempDirs.pop();
16+
if (dir) {
17+
fs.rmSync(dir, { recursive: true, force: true });
18+
}
19+
}
20+
});
21+
22+
function createTempWorkspace(): string {
23+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "deepcode-line-endings-"));
24+
tempDirs.push(dir);
25+
return dir;
26+
}
27+
28+
function createContext(sessionId: string, projectRoot: string): ToolExecutionContext {
29+
return {
30+
sessionId,
31+
projectRoot,
32+
toolCall: {
33+
id: "test-tool-call",
34+
type: "function",
35+
function: {
36+
name: "write",
37+
arguments: "{}",
38+
},
39+
},
40+
};
41+
}
42+
43+
test("platformLineEnding reports CRLF only on Windows-style platforms", () => {
44+
assert.equal(platformLineEnding("\r\n"), "CRLF");
45+
assert.equal(platformLineEnding("\n"), "LF");
46+
});
47+
48+
test("a newly created file uses the platform-native line ending", async () => {
49+
// Models emit LF-only text. A created file has no existing EOL to preserve, so it
50+
// should follow the platform: CRLF on Windows, matching what native tooling writes.
51+
const workspace = createTempWorkspace();
52+
const filePath = path.join(workspace, "created.txt");
53+
54+
await handleWriteTool({ file_path: filePath, content: "one\ntwo" }, createContext("create-eol", workspace));
55+
56+
const expected = platformLineEnding() === "CRLF" ? "one\r\ntwo" : "one\ntwo";
57+
assert.equal(fs.readFileSync(filePath, "utf8"), expected);
58+
});
59+
60+
test("an existing CRLF file keeps its line endings when rewritten with LF content", async () => {
61+
const workspace = createTempWorkspace();
62+
const filePath = path.join(workspace, "existing.txt");
63+
fs.writeFileSync(filePath, "one\r\ntwo\r\n", "utf8");
64+
const context = createContext("keep-eol", workspace);
65+
66+
await handleReadTool({ file_path: filePath }, context);
67+
await handleWriteTool({ file_path: filePath, content: "one\ntwo\n" }, context);
68+
69+
assert.equal(fs.readFileSync(filePath, "utf8"), "one\r\ntwo\r\n");
70+
});

‎packages/core/src/tools/write-handler.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ensureParentDirectory,
77
hasFileChangedSinceState,
88
normalizeContent,
9+
platformLineEnding,
910
readTextFileWithMetadata,
1011
writeTextFile,
1112
} from "../common/file-utils";
@@ -96,7 +97,8 @@ export async function handleWriteTool(
9697

9798
const existingMetadata = existingFile ? readTextFileWithMetadata(filePath) : null;
9899
const encoding = existingMetadata?.encoding ?? "utf8";
99-
const lineEndings = existingMetadata?.lineEndings ?? (input.content.includes("\r\n") ? "CRLF" : "LF");
100+
const lineEndings =
101+
existingMetadata?.lineEndings ?? (input.content.includes("\r\n") ? "CRLF" : platformLineEnding());
100102
const diffPreview = buildDiffPreview(filePath, existingMetadata?.content ?? null, normalizedContent);
101103
context.signal?.throwIfAborted();
102104
context.onBeforeFileMutation?.(filePath);

0 commit comments

Comments
 (0)