From d17828c2bb83129f46dad95ec0202dff933fa58c Mon Sep 17 00:00:00 2001 From: Gautam Kumar Date: Wed, 17 Jun 2026 08:31:51 +0530 Subject: [PATCH] Fix input field horizontal scroll offset not clamping after text deletion When text in an input field overflows the visible width, the viewport scrolls right to follow the cursor. After deleting characters with backspace, the scroll origin was not clamped back, leaving empty space on the right while earlier text remained hidden. Add GetContentWidth() to TextArea and clamp the horizontal scroll origin in RenderTextArea and the view resize path so the viewport never scrolls past the actual content width. Fixes #5633 Signed-off-by: Gautam Kumar --- pkg/gocui/gui.go | 11 +++++++++++ pkg/gocui/text_area.go | 8 ++++++++ pkg/gocui/view.go | 11 +++++++++++ pkg/gocui/view_test.go | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+) diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index 7b691f6d74f..6bdce303948 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -327,6 +327,17 @@ func (g *Gui) SetView(name string, x0, y0, x1, y1 int, overlaps byte) (*View, er newViewCursorX, newOriginX := updatedCursorAndOrigin(0, v.InnerWidth(), cursorX) newViewCursorY, newOriginY := updatedCursorAndOrigin(0, v.InnerHeight(), cursorY) + contentWidth := v.TextArea.GetContentWidth() + usableWidth := v.InnerWidth() - 1 + maxOriginX := contentWidth - usableWidth + if maxOriginX < 0 { + maxOriginX = 0 + } + if newOriginX > maxOriginX { + newOriginX = maxOriginX + newViewCursorX = cursorX - newOriginX + } + v.SetCursor(newViewCursorX, newViewCursorY) v.SetOrigin(newOriginX, newOriginY) } diff --git a/pkg/gocui/text_area.go b/pkg/gocui/text_area.go index 7aeb6220a37..6fe6f0f74d6 100644 --- a/pkg/gocui/text_area.go +++ b/pkg/gocui/text_area.go @@ -392,6 +392,14 @@ func (self *TextArea) GetUnwrappedContent() string { return self.content } +func (self *TextArea) GetContentWidth() int { + if len(self.cells) == 0 { + return 0 + } + last := self.cells[len(self.cells)-1] + return last.x + last.width +} + func (self *TextArea) ToggleOverwrite() { self.overwrite = !self.overwrite } diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 166cb0e2cf8..570b8aad15d 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -1722,6 +1722,17 @@ func (v *View) RenderTextArea() { newViewCursorX, newOriginX := updatedCursorAndOrigin(prevOriginX, width, cursorX) newViewCursorY, newOriginY := updatedCursorAndOrigin(prevOriginY, height, cursorY) + contentWidth := v.TextArea.GetContentWidth() + usableWidth := width - 1 + maxOriginX := contentWidth - usableWidth + if maxOriginX < 0 { + maxOriginX = 0 + } + if newOriginX > maxOriginX { + newOriginX = maxOriginX + newViewCursorX = cursorX - newOriginX + } + v.SetCursor(newViewCursorX, newViewCursorY) v.SetOrigin(newOriginX, newOriginY) } diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index a7023be4328..01c4e62a387 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -120,6 +120,40 @@ func TestUpdatedCursorAndOrigin(t *testing.T) { } } +func TestRenderTextAreaClampsScrollOffset(t *testing.T) { + // View with inner width 10: x0=0, x1=11 → Width=12, InnerWidth=10 + v := NewView("name", 0, 0, 11, 0, OutputNormal) + v.Editable = true + + // Type 15 characters to overflow the viewport + for i := 0; i < 15; i++ { + v.TextArea.TypeCharacter("a") + } + // Cursor is at position 15, content width is 15 + cursorX, _ := v.TextArea.GetCursorXY() + assert.Equal(t, 15, cursorX) + + // Simulate scrolling: set origin so viewport shows positions 6-15 + v.SetOrigin(6, 0) + v.RenderTextArea() + originX, _ := v.Origin() + assert.Equal(t, 6, originX) + + // Now delete 11 characters so content is only 4 chars wide + for i := 0; i < 11; i++ { + v.TextArea.BackSpaceChar() + } + // Cursor is at position 4, content width is 4 + cursorX, _ = v.TextArea.GetCursorXY() + assert.Equal(t, 4, cursorX) + assert.Equal(t, 4, v.TextArea.GetContentWidth()) + + // After rendering, origin should be clamped to 0 since content (4) < viewport (10) + v.RenderTextArea() + originX, _ = v.Origin() + assert.Equal(t, 0, originX, "origin should be clamped to 0 when content is narrower than viewport") +} + func TestAutoRenderingHyperlinks(t *testing.T) { v := NewView("name", 0, 0, 10, 10, OutputNormal) v.AutoRenderHyperLinks = true