diff --git a/internal/fsext/fileutil.go b/internal/fsext/fileutil.go index 27b5e48f62e388d707cbfdf62780779e54129dab..e68888452cdc190cb1e6cbdec8d87760dd8e432c 100644 --- a/internal/fsext/fileutil.go +++ b/internal/fsext/fileutil.go @@ -233,3 +233,19 @@ func HasPrefix(path, prefix string) bool { // If path is within prefix, Rel will not return a path starting with ".." return !strings.HasPrefix(rel, "..") } + +// ToUnixLineEndings converts Windows line endings (CRLF) to Unix line endings (LF). +func ToUnixLineEndings(content string) (string, bool) { + if strings.Contains(content, "\r\n") { + return strings.ReplaceAll(content, "\r\n", "\n"), true + } + return content, false +} + +// ToWindowsLineEndings converts Unix line endings (LF) to Windows line endings (CRLF). +func ToWindowsLineEndings(content string) (string, bool) { + if !strings.Contains(content, "\r\n") { + return strings.ReplaceAll(content, "\n", "\r\n"), true + } + return content, false +} diff --git a/internal/llm/tools/edit.go b/internal/llm/tools/edit.go index fd9ed81d55bc96e7b7767adad2f1186d88d97023..8cc3154ebab98ca34a49715d48b110caabc4ffe3 100644 --- a/internal/llm/tools/edit.go +++ b/internal/llm/tools/edit.go @@ -99,6 +99,7 @@ WINDOWS NOTES: - File paths should use forward slashes (/) for cross-platform compatibility - On Windows, absolute paths start with drive letters (C:/) but forward slashes work throughout - File permissions are handled automatically by the Go runtime +- Always assumes \n for line endings. The tool will handle \r\n conversion automatically if needed. Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.` ) @@ -299,7 +300,7 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string return ToolResponse{}, fmt.Errorf("failed to read file: %w", err) } - oldContent := string(content) + oldContent, isCrlf := fsext.ToUnixLineEndings(string(content)) var newContent string var deletionCount int @@ -356,6 +357,10 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string return ToolResponse{}, permission.ErrorPermissionDenied } + if isCrlf { + newContent, _ = fsext.ToWindowsLineEndings(newContent) + } + err = os.WriteFile(filePath, []byte(newContent), 0o644) if err != nil { return ToolResponse{}, fmt.Errorf("failed to write file: %w", err) @@ -428,7 +433,7 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS return ToolResponse{}, fmt.Errorf("failed to read file: %w", err) } - oldContent := string(content) + oldContent, isCrlf := fsext.ToUnixLineEndings(string(content)) var newContent string var replacementCount int @@ -487,6 +492,10 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS return ToolResponse{}, permission.ErrorPermissionDenied } + if isCrlf { + newContent, _ = fsext.ToWindowsLineEndings(newContent) + } + err = os.WriteFile(filePath, []byte(newContent), 0o644) if err != nil { return ToolResponse{}, fmt.Errorf("failed to write file: %w", err) diff --git a/internal/llm/tools/multiedit.go b/internal/llm/tools/multiedit.go index 271603a5b0f97b78bb7dbe53b93a060cff48b9cc..50070ca613464a280e53ab964155b8d1e205dde5 100644 --- a/internal/llm/tools/multiedit.go +++ b/internal/llm/tools/multiedit.go @@ -335,7 +335,7 @@ func (m *multiEditTool) processMultiEditExistingFile(ctx context.Context, params return ToolResponse{}, fmt.Errorf("failed to read file: %w", err) } - oldContent := string(content) + oldContent, isCrlf := fsext.ToUnixLineEndings(string(content)) currentContent := oldContent // Apply all edits sequentially @@ -377,6 +377,10 @@ func (m *multiEditTool) processMultiEditExistingFile(ctx context.Context, params return ToolResponse{}, permission.ErrorPermissionDenied } + if isCrlf { + currentContent, _ = fsext.ToWindowsLineEndings(currentContent) + } + // Write the updated content err = os.WriteFile(params.FilePath, []byte(currentContent), 0o644) if err != nil {