mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-06 13:00:59 -04:00
Fix search match highlighting when a custom patch is active (#6107)
Search matches beside the inclusion gutter were highlighted in the wrong place, and hovering over or clicking a hyperlink there acted 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 added the horizontal origin and ignores the gutter. Add a helper that does both, and use it for search highlights, hover and clicks.
This commit is contained in:
+1
-1
@@ -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
-80
@@ -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,39 +2029,12 @@ 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
|
||||
}
|
||||
|
||||
// realPosition returns the position in the internal buffer corresponding to the
|
||||
// point (x, y) of the view.
|
||||
func (v *View) realPosition(vx, vy int) (x, y int, ok bool) {
|
||||
vx = v.ox + vx
|
||||
vy = v.oy + vy
|
||||
|
||||
if vx < 0 || vy < 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
if len(v.viewLines) == 0 {
|
||||
return vx, vy, true
|
||||
}
|
||||
|
||||
if vy < len(v.viewLines) {
|
||||
vline := v.viewLines[vy]
|
||||
x = vline.linesX + vx
|
||||
y = vline.linesY
|
||||
} else {
|
||||
vline := v.viewLines[len(v.viewLines)-1]
|
||||
x = vx
|
||||
y = vline.linesY + vy - len(v.viewLines) + 1
|
||||
}
|
||||
|
||||
return x, y, true
|
||||
}
|
||||
|
||||
// clearRunes erases all the cells in the view.
|
||||
func (v *View) clearRunes() {
|
||||
maxX, maxY := v.InnerSize()
|
||||
@@ -2283,56 +2263,6 @@ func (v *View) ViewBuffer() string {
|
||||
return strings.Join(strs, "\n")
|
||||
}
|
||||
|
||||
// Line returns a string with the line of the view's internal buffer
|
||||
// at the position corresponding to the point (x, y).
|
||||
func (v *View) Line(y int) (string, bool) {
|
||||
_, y, ok := v.realPosition(0, y)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if y < 0 || y >= len(v.buf.lines) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return v.buf.lines[y].cells.String(), true
|
||||
}
|
||||
|
||||
// Word returns a string with the word of the view's internal buffer
|
||||
// at the position corresponding to the point (x, y).
|
||||
func (v *View) Word(x, y int) (string, bool) {
|
||||
x, y, ok := v.realPosition(x, y)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if x < 0 || y < 0 || y >= len(v.buf.lines) || x >= len(v.buf.lines[y].cells) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
str := v.buf.lines[y].cells.String()
|
||||
|
||||
nl := strings.LastIndexFunc(str[:x], indexFunc)
|
||||
if nl == -1 {
|
||||
nl = 0
|
||||
} else {
|
||||
nl = nl + 1
|
||||
}
|
||||
nr := strings.IndexFunc(str[x:], indexFunc)
|
||||
if nr == -1 {
|
||||
nr = len(str)
|
||||
} else {
|
||||
nr = nr + x
|
||||
}
|
||||
return str[nl:nr], true
|
||||
}
|
||||
|
||||
// indexFunc allows to split lines by words taking into account spaces
|
||||
// and 0.
|
||||
func indexFunc(r rune) bool {
|
||||
return r == ' ' || r == 0
|
||||
}
|
||||
|
||||
// applySelTextColor adds the attributes of selTextColor to fgColor, and
|
||||
// replaces the color of fgColor with that of selTextColor if it has one.
|
||||
func applySelTextColor(fgColor, selTextColor Attribute) Attribute {
|
||||
@@ -2846,7 +2776,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 {
|
||||
|
||||
@@ -1245,6 +1245,87 @@ func TestInclusionGutter(t *testing.T) {
|
||||
assert.Equal(t, "a", chr)
|
||||
}
|
||||
|
||||
// A search match beside the inclusion gutter is highlighted where it is drawn.
|
||||
func TestSearchMatchBesideTheInclusionGutter(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 6)
|
||||
|
||||
v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10
|
||||
v.InclusionGutterMarker = "✓"
|
||||
|
||||
v.writeString("0123456789\n")
|
||||
v.SetInclusionGutter(true, []bool{true})
|
||||
v.UpdateSearchResults("34", nil)
|
||||
v.draw(true)
|
||||
|
||||
highlightedColumns := func(y int) string {
|
||||
s := ""
|
||||
for x := 1; x <= 10; x++ {
|
||||
_, style, _ := Screen.Get(x, y)
|
||||
s += lo.Ternary(style.GetBackground() != tcell.ColorDefault, "#", ".")
|
||||
}
|
||||
return s
|
||||
}
|
||||
// The row reads "✓ 01234567", so the match is at view columns 5 and 6.
|
||||
assert.Equal(t,
|
||||
".....##...",
|
||||
highlightedColumns(1))
|
||||
}
|
||||
|
||||
// A hyperlink beside the inclusion gutter is underlined while the mouse is over it.
|
||||
func TestHoveredHyperlinkBesideTheInclusionGutter(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 6)
|
||||
|
||||
v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10
|
||||
v.InclusionGutterMarker = "✓"
|
||||
v.UnderlineHyperLinksOnlyOnHover = true
|
||||
|
||||
v.writeString("ab\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\cd\n")
|
||||
v.SetInclusionGutter(true, []bool{true})
|
||||
|
||||
// The row reads "✓ ablinkcd". Point at the "l" of "link", at view column 4,
|
||||
// which the frame puts at screen column 5.
|
||||
v.onMouseMove(5, 1)
|
||||
v.draw(true)
|
||||
|
||||
underlinedColumns := func(y int) string {
|
||||
s := ""
|
||||
for x := 1; x <= 10; x++ {
|
||||
_, style, _ := Screen.Get(x, y)
|
||||
s += lo.Ternary(style.HasUnderline(), "_", ".")
|
||||
}
|
||||
return s
|
||||
}
|
||||
assert.Equal(t,
|
||||
"....____..",
|
||||
underlinedColumns(1))
|
||||
}
|
||||
|
||||
// Clicking a hyperlink beside the inclusion gutter opens it.
|
||||
func TestClickHyperlinkBesideTheInclusionGutter(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
v, _ := g.SetView("main", 0, 0, 20, 10, 0)
|
||||
v.InclusionGutterMarker = "✓"
|
||||
|
||||
v.writeString("\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\ text\n")
|
||||
v.SetInclusionGutter(true, []bool{true})
|
||||
|
||||
opened := []string{}
|
||||
g.SetOpenHyperlinkFunc(func(link string, _ string) error {
|
||||
opened = append(opened, link)
|
||||
return nil
|
||||
})
|
||||
|
||||
// The row reads "✓ link text". Click the "k" of "link", at view column 5, which
|
||||
// the frame puts at screen column 6.
|
||||
assert.NoError(t, g.onKey(&GocuiEvent{
|
||||
Type: eventMouse, MouseX: 6, MouseY: 1,
|
||||
Key: NewKeyName(MouseLeft),
|
||||
}))
|
||||
assert.Equal(t,
|
||||
[]string{"https://example.com"},
|
||||
opened)
|
||||
}
|
||||
|
||||
// A marked line the view wraps is marked on every segment it is drawn as, so that
|
||||
// the mark doesn't look like it belongs to the first part of the line alone. The
|
||||
// gutter takes its columns out of the width the content wraps in.
|
||||
|
||||
Reference in New Issue
Block a user