From fda5abdfb346d5b0ccc702d6dab0bbf2406a3d70 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 9 Oct 2026 09:01:11 +0200 Subject: [PATCH] Account for the inclusion gutter when mapping view columns to content Search matches beside the inclusion gutter are highlighted in the wrong place, and hovering over or clicking a hyperlink there acts on the wrong cell. The draw loop starts the content after the gutter, but the code that maps a view column back to a column of the content only adds the horizontal origin and ignores the gutter. Add a helper that does both, and use it for search highlights, hover and clicks. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/gocui/gui.go | 2 +- pkg/gocui/view.go | 13 ++++++++++--- pkg/gocui/view_test.go | 9 --------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index 465a7b48e..adaa1f650 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -1793,7 +1793,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error { newCx := mx - v.x0 - 1 newCy := my - v.y0 - 1 // newX and newY are relative to the view's content, independent of its scroll position - newX := newCx + v.ox + newX := v.contentX(newCx) newY := newCy + v.oy // if view is editable don't go further than the furthest character for that line if v.Editable { diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index ec20ab429..40e067f00 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -2008,10 +2008,17 @@ func (v *View) viewLineLengthIgnoringTrailingBlankLines() int { return 0 } +// contentX returns the column of the view's content that is drawn at column x of the +// view. The content starts after the inclusion gutter, and is scrolled by the view's +// horizontal origin. +func (v *View) contentX(x int) int { + return x - v.inclusionGutterWidth() + v.ox +} + func (v *View) isPatternMatchedRune(x, y int) (bool, bool) { for i, pos := range v.searcher.searchPositions { adjustedY := y + v.oy - adjustedX := x + v.ox + adjustedX := v.contentX(x) if adjustedY == pos.Y && adjustedX >= pos.XStart && adjustedX < pos.XEnd { return true, i == v.searcher.currentSearchIndex } @@ -2022,7 +2029,7 @@ func (v *View) isPatternMatchedRune(x, y int) (bool, bool) { func (v *View) isHoveredHyperlink(x, y int) bool { if v.UnderlineHyperLinksOnlyOnHover && v.hoveredHyperlink != nil { adjustedY := y + v.oy - adjustedX := x + v.ox + adjustedX := v.contentX(x) return adjustedY == v.hoveredHyperlink.Y && adjustedX >= v.hoveredHyperlink.XStart && adjustedX < v.hoveredHyperlink.XEnd } return false @@ -2846,7 +2853,7 @@ func (v *View) onMouseMove(x int, y int) { newCx := x - v.x0 - 1 newCy := y - v.y0 - 1 // newX and newY are relative to the view's content, independent of its scroll position - newX := newCx + v.ox + newX := v.contentX(newCx) newY := newCy + v.oy if newY >= 0 && newY <= len(v.viewLines)-1 && newX >= 0 && newX <= len(v.viewLines[newY].line)-1 { diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index 201249646..edc800990 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -1267,10 +1267,7 @@ func TestSearchMatchBesideTheInclusionGutter(t *testing.T) { } // The row reads "✓ 01234567", so the match is at view columns 5 and 6. assert.Equal(t, - /* EXPECTED: ".....##...", - ACTUAL: */ - "...##.....", highlightedColumns(1)) } @@ -1299,10 +1296,7 @@ func TestHoveredHyperlinkBesideTheInclusionGutter(t *testing.T) { return s } assert.Equal(t, - /* EXPECTED: "....____..", - ACTUAL: */ - "..____....", underlinedColumns(1)) } @@ -1328,10 +1322,7 @@ func TestClickHyperlinkBesideTheInclusionGutter(t *testing.T) { Key: NewKeyName(MouseLeft), })) assert.Equal(t, - /* EXPECTED: []string{"https://example.com"}, - ACTUAL: */ - []string{}, opened) }