From 4f7ff1043df28a61f15b67d532ec181aad1151c2 Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Wed, 22 Jul 2026 03:39:37 +0800 Subject: [PATCH] fix(textarea): avoid phantom soft-wrap and unstick vertical cursor When a line exactly fills the textarea width, wrap() used >= and created a phantom blank line. Vertical navigation also stopped one display column early because setCursorLineRelative broke at CharWidth-1. Fixes #887 --- textarea/textarea.go | 4 +-- textarea/textarea_test.go | 67 ++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/textarea/textarea.go b/textarea/textarea.go index a6ed90b1a..cc0a95e1e 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]) @@ -1846,7 +1846,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 41d51f744..9d47ff0a1 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, }, }, { @@ -2429,3 +2429,34 @@ func stripString(str string) string { return strings.Join(lines, "\n") } + + +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) + } +}