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