Skip to content

fix(textarea): preserve newline when deleting the last character - #1072

Open
kimjooyoon wants to merge 1 commit into
charmbracelet:mainfrom
kimjooyoon:fix-delete-forward-newline
Open

kimjooyoon wants to merge 1 commit into
charmbracelet:mainfrom
kimjooyoon:fix-delete-forward-newline

Conversation

@kimjooyoon

@kimjooyoon kimjooyoon commented Oct 21, 2026 •

Copy link
Copy Markdown

Fixes #1071.

Deleting the last character of a non-final textarea line also deleted the following newline: Delete on b in ab\ncd returned acd. Check whether the cursor is already at line end before removing the rune, so a keypress either deletes a character or joins lines. Delete at an existing line end, empty lines and final-line behavior stay unchanged.

The new eight-case regression fails in three cases on main c3bde0d1 and passes after the three-line reorder. It covers ASCII, a one-character line, a wide character and five boundary controls. The exact public API reproducer in the issue was also executed before and after the change.

Local validation (macOS arm64, Go 1.26.0):

  • go test ./...
  • go test -race ./textarea
  • go build ./...
  • go vet ./...
  • golangci-lint v2.13.1 run --timeout 3m — 0 issues
  • gofmt and git diff --check

The eight-case regression also passes on Go 1.27.3. Upstream cross-platform CI remains separate from these local checks.

This touches the same forward-delete block as #1067 but does not change rune/grapheme boundaries or depend on that proposal; the ordering correction applies independently.

AI disclosure: a Codex agent found and reproduced this bug, prepared the patch/tests and ran the listed checks. This description is AI-generated. No human-only review or upstream acceptance is implied.

Independent reproduction: Linux Go 1.27.3 evidence replays the exact original and submitted revisions with eight focused cases: three original failures, eight patched/race passes. A separate Gooo edit also matches all eight. The downloaded original events and all 41 source/log hashes were checked against the exact sources. The reproduction guide lists inputs, cursor units and commands. This focused run does not replace upstream cross-platform CI or approval.

Public evidence: the source-bound snapshot preserves the raw forward-delete/ logs and summaries from Linux CI 39691446155 at the exact submitted revision above. The published copy can be downloaded without GitHub login and checked offline with Python 3.9+, without a Gooo compiler. It retains intentional original failures and scoped results; upstream approval and acceptance remain separate.

Check for a cursor already at line end before deleting a rune, so one Delete press cannot also merge the next line. Add eight regression and boundary cases.

Prepared and locally verified with Codex assistance.

Fixes charmbracelet#1071

@by-ae by-ae left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked this against current main. With main's textarea.go, the new test fails in exactly three cases (last_character, only_character, wide_character). With this branch it passes, along with the rest of go test ./textarea/. The reorder is the right fix. The old code deleted the rune first, and then m.col >= len(...) was true for the now-shorter line, so the same keypress also joined the next line.

Two small follow-ups:

  1. Overlap with #1067. That PR rewrites this same block for graphemes, and it keeps the old order: delete first, then if m.col >= len(m.value[m.row]) { m.mergeLineBelow(...) } (#1067 diff, textarea.go around L1327). So whichever lands second needs to keep the "check line end first" order, or the bug comes back for grapheme deletes. Your wide_character case covers a single wide rune, but a multi-rune grapheme case (for example "ae\u0301\nx" at col 1, expecting "a\nx" once #1067 is in) would catch that regression directly.

  2. The guard is redundant now. Once the line-end case breaks out early, m.col < len(m.value[m.row]) is always true, and so is len(...) > 0. The block can be just:

    if m.col >= len(m.value[m.row]) {
        m.mergeLineBelow(m.row)
        break
    }
    m.value[m.row] = slices.Delete(m.value[m.row], m.col, m.col+1)

    The sibling cases (DeleteAfterCursor, DeleteCharacterBackward) also clamp m.col before the boundary check (textarea.go#L1281). Doing the same here would keep the three paths consistent, though >= already handles an out-of-range column.

@kimjooyoon

Copy link
Copy Markdown
Author

Thanks @by-ae for checking both revisions. Agreed: whichever PR lands second must keep the line-end check before deletion. The combining-mark regression belongs with the #1067 integration; this patch keeps its current rune scope. I also agree the inner guard is redundant after the early break. I'll track that cleanup and review the sibling-path clamping separately.

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.

textarea: Delete on the last character also removes the following newline

2 participants