Skip to content

fix(edit_file): range mode replaces whole lines (#543) - #547

Open
atlas-from-plumb wants to merge 1 commit into
mainfrom
atlas/fix-543-edit-range-newline
Open

atlas-from-plumb wants to merge 1 commit into
mainfrom
atlas/fix-543-edit-range-newline

Conversation

@atlas-from-plumb

Copy link
Copy Markdown
Collaborator

Why

edit_file range mode (edits: [{start_line, end_line, new_string}]) glued a non-empty new_string that had no trailing newline onto the line after the range. An empty new_string deleted its lines cleanly, so the two cases behaved differently. Replacing one struct field with two this way merged the second new field with the next one and broke the build, and nothing in the response flagged it (#543).

Change

  • internal/tools/range_edit.go: range mode now works in whole lines. A non-empty new_string without a trailing newline gets the line ending of the text it replaces, so a CRLF line stays \r\n. The EOF exception follows from the same rule. A range that runs to the end of a file with no final newline replaced unterminated text, so the replacement stays unterminated and the file still has no final newline. Append mode (start_line: -1) uses the same rule: a file that ends with a newline keeps one, and a file without one stays that way. The separator added before appending to such a file now uses the file's line ending (\r\n in a CRLF file) rather than a bare \n. Deletion (new_string: "") is unchanged. The atomic path and the apply_partial path both call applyRangeEdit, so both are covered. Anchor mode is not touched.
  • internal/tools/edit_file.go: the new_string schema text now says range mode replaces whole lines and adds a missing trailing newline except at EOF of a file that has none. The start_line text is shorter so the pinned tools/list payload stays within its budget: 44,960 of 45,000 bytes, 2 more than on main.
  • docs/tools.md: adds a short "Line-range edits" paragraph. CHANGELOG.md: one entry under 0.20.4 → Fixed.

Tests

New file internal/tools/range_edit_newline_test.go:

  • TestApplyRangeEdit_WholeLines: 1→1, 1→2, 2→1 and N→0 lines, each with and without a trailing newline; first line; last line of a file with and without a final newline, including end_line -1 and a capped end_line; CRLF; empty file.
  • TestApplyRangeEdit_AppendWholeLines: append to LF and CRLF files, with and without a final newline.
  • TestEditFile_RangeEdit_IssueRepro: the struct-field case from the issue, run end to end through Execute.
  • TestEditFile_RangeEdit_BatchAndCRLF: four sequential range edits (replace, grow, delete, append) in one call on an LF file and a CRLF file, run through both the atomic and the apply_partial paths.

With range_edit.go reverted to origin/main, 22 of the new subtests fail. The cases that pass there are the positive controls: explicit trailing newlines, deletions, and preserving a missing final newline. Ten hand-applied mutants were all killed: no terminator; no CRLF detection; always \n; no empty-string guard; LF-only separator; no empty-file branch; verbatim append; whole-file ending instead of the replaced range's ending; checking for a \r\n suffix instead of \n; always-CRLF separator.

go test ./... -count=1 and go test -tags=integration ./internal/cli/ -count=1 both exit 0. golangci-lint is clean.

Closes #543

🤖 Generated with Claude Code

A range edit whose non-empty new_string lacked a trailing newline was
glued onto the line after the range, while an empty new_string deleted
cleanly. Replacing one line with two this way merged the second new line
with the next one and broke the build, with no warning.

applyRangeEdit now gives such a new_string the line ending of the text
it replaces (\r\n in a CRLF line). A range to EOF in a file with no
final newline replaced unterminated text, so it stays unterminated and
the file keeps its missing final newline. Append mode follows the same
rule, and its separator uses the file's line ending. Both the atomic
and the apply_partial paths share applyRangeEdit.

The new_string schema text says so; start_line's is trimmed to keep the
pinned tools/list payload within budget (+2 bytes net).

Closes #543

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

edit_file range mode joins a multi-line new_string onto the next line when it lacks a trailing newline

1 participant