1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-02 23:27:32 +02:00

Underline hyperlinks only on mouse hover (#3856)

- **PR Description**

Followup to #3825: we decided there that we don't want to underline
links in delta diffs by default, but only on mouse hover. This PR does
that; it makes it possible to decide per view whether links should be
underlined always, or only on hover. We set this to only on hover for
the main views, so that links in diffs are not underlined (also affects
the status view though), but all other links we want to underline always
for better discoverability.
This commit is contained in:
Stefan Haller 2024-08-24 17:49:10 +02:00 committed by GitHub
commit 8a8490d97d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 120 additions and 10 deletions

2
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/integrii/flaggy v1.4.0 github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20240824094505-8cce5f5d2511 github.com/jesseduffield/gocui v0.3.1-0.20240824154427-0fc91d5098e4
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e

4
go.sum
View File

@ -188,8 +188,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk= github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE= github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o= github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20240824094505-8cce5f5d2511 h1:FN3QrzVxV3lM/SdvBCz2lUtfW0VOKLUMHj5xYWdv3Mc= github.com/jesseduffield/gocui v0.3.1-0.20240824154427-0fc91d5098e4 h1:2su9wjacqT/WxvNrzzdvA6rBJa6n/yZ/jvaS1r60HfM=
github.com/jesseduffield/gocui v0.3.1-0.20240824094505-8cce5f5d2511/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8= github.com/jesseduffield/gocui v0.3.1-0.20240824154427-0fc91d5098e4/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY= github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=

View File

@ -117,6 +117,7 @@ func (gui *Gui) createAllViews() error {
view.Title = gui.c.Tr.DiffTitle view.Title = gui.c.Tr.DiffTitle
view.Wrap = true view.Wrap = true
view.IgnoreCarriageReturns = true view.IgnoreCarriageReturns = true
view.UnderlineHyperLinksOnlyOnHover = true
} }
gui.Views.Staging.Title = gui.c.Tr.UnstagedChanges gui.Views.Staging.Title = gui.c.Tr.UnstagedChanges

View File

@ -180,6 +180,8 @@ type Gui struct {
suspended bool suspended bool
taskManager *TaskManager taskManager *TaskManager
lastHoverView *View
} }
type NewGuiOpts struct { type NewGuiOpts struct {
@ -836,7 +838,7 @@ func (g *Gui) processRemainingEvents() error {
// etc.) // etc.)
func (g *Gui) handleEvent(ev *GocuiEvent) error { func (g *Gui) handleEvent(ev *GocuiEvent) error {
switch ev.Type { switch ev.Type {
case eventKey, eventMouse: case eventKey, eventMouse, eventMouseMove:
return g.onKey(ev) return g.onKey(ev)
case eventError: case eventError:
return ev.Err return ev.Err
@ -1395,6 +1397,19 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
return err return err
} }
case eventMouseMove:
mx, my := ev.MouseX, ev.MouseY
v, err := g.VisibleViewByPosition(mx, my)
if err != nil {
break
}
if g.lastHoverView != nil && g.lastHoverView != v {
g.lastHoverView.lastHoverPosition = nil
g.lastHoverView.hoveredHyperlink = nil
}
g.lastHoverView = v
v.onMouseMove(mx, my)
default: default:
} }

View File

@ -176,6 +176,7 @@ const (
eventKey eventKey
eventResize eventResize
eventMouse eventMouse
eventMouseMove // only used when no button is down, otherwise it's eventMouse
eventFocus eventFocus
eventInterrupt eventInterrupt
eventError eventError
@ -387,7 +388,11 @@ func (g *Gui) pollEvent() GocuiEvent {
if !wheeling { if !wheeling {
switch dragState { switch dragState {
case NOT_DRAGGING: case NOT_DRAGGING:
return GocuiEvent{Type: eventNone} return GocuiEvent{
Type: eventMouseMove,
MouseX: x,
MouseY: y,
}
// if we haven't released the left mouse button and we've moved the cursor then we're dragging // if we haven't released the left mouse button and we've moved the cursor then we're dragging
case MAYBE_DRAGGING: case MAYBE_DRAGGING:
if x != lastX || y != lastY { if x != lastX || y != lastY {

View File

@ -56,6 +56,13 @@ type View struct {
// tained is true if the viewLines must be updated // tained is true if the viewLines must be updated
tainted bool tainted bool
// the last position that the mouse was hovering over; nil if the mouse is outside of
// this view, or not hovering over a cell
lastHoverPosition *pos
// the location of the hyperlink that the mouse is currently hovering over; nil if none
hoveredHyperlink *SearchPosition
// internal representation of the view's buffer. We will keep viewLines around // internal representation of the view's buffer. We will keep viewLines around
// from a previous render until we explicitly set them to nil, allowing us to // from a previous render until we explicitly set them to nil, allowing us to
// render the same content twice without flicker. Wherever we want to render // render the same content twice without flicker. Wherever we want to render
@ -180,6 +187,14 @@ type View struct {
// if true, the user can scroll all the way past the last item until it appears at the top of the view // if true, the user can scroll all the way past the last item until it appears at the top of the view
CanScrollPastBottom bool CanScrollPastBottom bool
// if true, the view will underline hyperlinks only when the cursor is on
// them; otherwise, they will always be underlined
UnderlineHyperLinksOnlyOnHover bool
}
type pos struct {
x, y int
} }
// call this in the event of a view resize, or if you want to render new content // call this in the event of a view resize, or if you want to render new content
@ -188,6 +203,7 @@ type View struct {
func (v *View) clearViewLines() { func (v *View) clearViewLines() {
v.tainted = true v.tainted = true
v.viewLines = nil v.viewLines = nil
v.clearHover()
} }
type searcher struct { type searcher struct {
@ -532,6 +548,10 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
} }
} }
if v.isHoveredHyperlink(x, y) {
fgColor |= AttrUnderline
}
// Don't display NUL characters // Don't display NUL characters
if ch == 0 { if ch == 0 {
ch = ' ' ch = ' '
@ -756,6 +776,7 @@ func (v *View) WriteRunes(p []rune) {
// writeRunes copies slice of runes into internal lines buffer. // writeRunes copies slice of runes into internal lines buffer.
func (v *View) writeRunes(p []rune) { func (v *View) writeRunes(p []rune) {
v.tainted = true v.tainted = true
v.clearHover()
// Fill with empty cells, if writing outside current view buffer // Fill with empty cells, if writing outside current view buffer
v.makeWriteable(v.wx, v.wy) v.makeWriteable(v.wx, v.wy)
@ -1164,7 +1185,7 @@ func (v *View) draw() error {
if bgColor == ColorDefault { if bgColor == ColorDefault {
bgColor = v.BgColor bgColor = v.BgColor
} }
if c.hyperlink != "" { if c.hyperlink != "" && !v.UnderlineHyperLinksOnlyOnHover {
fgColor |= AttrUnderline fgColor |= AttrUnderline
} }
@ -1236,6 +1257,15 @@ func (v *View) isPatternMatchedRune(x, y int) (bool, bool) {
return false, false return false, false
} }
func (v *View) isHoveredHyperlink(x, y int) bool {
if v.UnderlineHyperLinksOnlyOnHover && v.hoveredHyperlink != nil {
adjustedY := y + v.oy
adjustedX := x + v.ox
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 // realPosition returns the position in the internal buffer corresponding to the
// point (x, y) of the view. // point (x, y) of the view.
func (v *View) realPosition(vx, vy int) (x, y int, err error) { func (v *View) realPosition(vx, vy int) (x, y int, err error) {
@ -1406,6 +1436,7 @@ func (v *View) SetHighlight(y int, on bool) error {
} }
v.tainted = true v.tainted = true
v.lines[y] = cells v.lines[y] = cells
v.clearHover()
return nil return nil
} }
@ -1672,8 +1703,12 @@ func (v *View) ScrollUp(amount int) {
amount = v.oy amount = v.oy
} }
v.oy -= amount if amount != 0 {
v.cy += amount v.oy -= amount
v.cy += amount
v.clearHover()
}
} }
// ensures we don't scroll past the end of the view's content // ensures we don't scroll past the end of the view's content
@ -1682,6 +1717,8 @@ func (v *View) ScrollDown(amount int) {
if adjustedAmount > 0 { if adjustedAmount > 0 {
v.oy += adjustedAmount v.oy += adjustedAmount
v.cy -= adjustedAmount v.cy -= adjustedAmount
v.clearHover()
} }
} }
@ -1690,12 +1727,18 @@ func (v *View) ScrollLeft(amount int) {
if newOx < 0 { if newOx < 0 {
newOx = 0 newOx = 0
} }
v.ox = newOx if newOx != v.ox {
v.ox = newOx
v.clearHover()
}
} }
// not applying any limits to this // not applying any limits to this
func (v *View) ScrollRight(amount int) { func (v *View) ScrollRight(amount int) {
v.ox += amount v.ox += amount
v.clearHover()
} }
func (v *View) adjustDownwardScrollAmount(scrollHeight int) int { func (v *View) adjustDownwardScrollAmount(scrollHeight int) int {
@ -1769,3 +1812,49 @@ func containsColoredTextInLine(fgColorStr string, text string, line []cell) bool
return strings.Contains(currentMatch, text) return strings.Contains(currentMatch, text)
} }
func (v *View) onMouseMove(x int, y int) {
if v.Editable || !v.UnderlineHyperLinksOnlyOnHover {
return
}
// newCx and newCy are relative to the view port, i.e. to the visible area of the view
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
newY := newCy + v.oy
if newY >= 0 && newY <= len(v.viewLines)-1 && newX >= 0 && newX <= len(v.viewLines[newY].line)-1 {
if v.lastHoverPosition == nil || v.lastHoverPosition.x != newX || v.lastHoverPosition.y != newY {
v.hoveredHyperlink = v.findHyperlinkAt(newX, newY)
}
v.lastHoverPosition = &pos{x: newX, y: newY}
} else {
v.lastHoverPosition = nil
v.hoveredHyperlink = nil
}
}
func (v *View) findHyperlinkAt(x, y int) *SearchPosition {
linkStr := v.viewLines[y].line[x].hyperlink
if linkStr == "" {
return nil
}
xStart := x
for xStart > 0 && v.viewLines[y].line[xStart-1].hyperlink == linkStr {
xStart--
}
xEnd := x + 1
for xEnd < len(v.viewLines[y].line) && v.viewLines[y].line[xEnd].hyperlink == linkStr {
xEnd++
}
return &SearchPosition{XStart: xStart, XEnd: xEnd, Y: y}
}
func (v *View) clearHover() {
v.hoveredHyperlink = nil
v.lastHoverPosition = nil
}

2
vendor/modules.txt vendored
View File

@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
github.com/jesseduffield/go-git/v5/utils/merkletrie/index github.com/jesseduffield/go-git/v5/utils/merkletrie/index
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
# github.com/jesseduffield/gocui v0.3.1-0.20240824094505-8cce5f5d2511 # github.com/jesseduffield/gocui v0.3.1-0.20240824154427-0fc91d5098e4
## explicit; go 1.12 ## explicit; go 1.12
github.com/jesseduffield/gocui github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10