From 950045bf5d4229b0d2b38eb1c705171bdff3acde Mon Sep 17 00:00:00 2001 From: wuyangfan <1102042793@qq.com> Date: Sun, 17 May 2026 20:07:39 +0800 Subject: [PATCH] fix(textarea): cursor navigation and wrap at exact line width When soft-wrapped content exactly filled the textarea width, wrap() used >= and created a phantom blank line, and vertical cursor movement stopped one column early (offset >= CharWidth-1) (#887). Co-authored-by: Cursor --- textarea/textarea.go | 4 +-- textarea/textarea_test.go | 66 ++++++++++++++++++++++++++++----------- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/textarea/textarea.go b/textarea/textarea.go index f0c0ca54..f9fa58a1 100644 --- a/textarea/textarea.go +++ b/textarea/textarea.go @@ -699,7 +699,7 @@ func (m *Model) setCursorLineRelative(delta int) { offset := 0 for offset < charOffset { - if m.row >= len(m.value) || m.col >= len(m.value[m.row]) || offset >= nli.CharWidth-1 { + if m.row >= len(m.value) || m.col >= len(m.value[m.row]) || offset >= nli.CharWidth { break } offset += rw.RuneWidth(m.value[m.row][m.col]) @@ -1849,7 +1849,7 @@ func wrap(runes []rune, width int) [][]rune { } } - if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces >= width { + if uniseg.StringWidth(string(lines[row]))+uniseg.StringWidth(string(word))+spaces > width { lines = append(lines, []rune{}) lines[row+1] = append(lines[row+1], word...) // We add an extra space at the end of the line to account for the diff --git a/textarea/textarea_test.go b/textarea/textarea_test.go index 41d51f74..55c81635 100644 --- a/textarea/textarea_test.go +++ b/textarea/textarea_test.go @@ -743,8 +743,8 @@ func TestView(t *testing.T) { > > `), - cursorRow: 1, - cursorCol: 0, + cursorRow: 0, + cursorCol: 4, }, }, { @@ -814,8 +814,8 @@ func TestView(t *testing.T) { > > `), - cursorRow: 1, - cursorCol: 0, + cursorRow: 0, + cursorCol: 4, }, }, { @@ -861,8 +861,8 @@ func TestView(t *testing.T) { > > `), - cursorRow: 3, - cursorCol: 0, + cursorRow: 2, + cursorCol: 1, }, }, { @@ -884,8 +884,8 @@ func TestView(t *testing.T) { > > `), - cursorRow: 3, - cursorCol: 0, + cursorRow: 2, + cursorCol: 1, }, }, { @@ -908,8 +908,8 @@ func TestView(t *testing.T) { > > `), - cursorRow: 3, - cursorCol: 0, + cursorRow: 2, + cursorCol: 1, }, }, { @@ -933,8 +933,8 @@ func TestView(t *testing.T) { `), - cursorRow: 3, - cursorCol: 0, + cursorRow: 2, + cursorCol: 1, }, }, { @@ -1004,8 +1004,8 @@ func TestView(t *testing.T) { > > `), - cursorRow: 1, - cursorCol: 0, + cursorRow: 0, + cursorCol: 4, }, }, { @@ -1118,8 +1118,8 @@ func TestView(t *testing.T) { │> │ └──────────┘ `), - cursorRow: 1, - cursorCol: 0, + cursorRow: 0, + cursorCol: 4, }, }, { @@ -1241,8 +1241,8 @@ func TestView(t *testing.T) { │> │ └──────────┘ `), - cursorRow: 1, - cursorCol: 0, + cursorRow: 0, + cursorCol: 8, }, }, { @@ -2403,6 +2403,36 @@ func newTextArea() Model { return textarea } +func TestWrapExactWidthNoPhantomLine(t *testing.T) { + const width = 10 + input := []rune("0123456789") + + lines := wrap(input, width) + if len(lines) > 1 { + for i, line := range lines { + if i > 0 && len(strings.TrimSpace(string(line))) == 0 { + t.Fatalf("phantom wrap line at index %d: %#v", i, lines) + } + } + } +} + +func TestCursorDownPastExactWidthLine(t *testing.T) { + textarea := newTextArea() + textarea.SetWidth(20) + + line1 := strings.Repeat("x", 20) + textarea.SetValue(line1 + "\nsecond") + textarea.row = 0 + textarea.col = len(line1) + textarea.lastCharOffset = textarea.LineInfo().CharOffset + + textarea.CursorDown() + if textarea.row != 1 { + t.Fatalf("row %d want 1 after moving down from a full-width line", textarea.row) + } +} + func keyPress(key rune) tea.Msg { return tea.KeyPressMsg{Code: key, Text: string(key)} }