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) <[email protected]>
This commit is contained in:
Stefan Haller
2026-10-10 13:40:23 +02:00
co-authored by Claude Opus 5.5
parent 07995e630b
commit fda5abdfb3
3 changed files with 11 additions and 13 deletions
+1 -1
View File
@@ -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 {
+10 -3
View File
@@ -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 {
-9
View File
@@ -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)
}