diff --git a/go.mod b/go.mod index a93d04e3e..36350778c 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,7 @@ require ( github.com/integrii/flaggy v1.4.0 github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 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/lazycore v0.0.0-20221012050358-03d2e40243c5 github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e diff --git a/go.sum b/go.sum index 4efe0c3b1..9793cd69b 100644 --- a/go.sum +++ b/go.sum @@ -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/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/gocui v0.3.1-0.20240824094505-8cce5f5d2511 h1:FN3QrzVxV3lM/SdvBCz2lUtfW0VOKLUMHj5xYWdv3Mc= -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 h1:2su9wjacqT/WxvNrzzdvA6rBJa6n/yZ/jvaS1r60HfM= +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/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo= github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY= diff --git a/pkg/gui/views.go b/pkg/gui/views.go index e76ed24d3..f561eac37 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -117,6 +117,7 @@ func (gui *Gui) createAllViews() error { view.Title = gui.c.Tr.DiffTitle view.Wrap = true view.IgnoreCarriageReturns = true + view.UnderlineHyperLinksOnlyOnHover = true } gui.Views.Staging.Title = gui.c.Tr.UnstagedChanges diff --git a/vendor/github.com/jesseduffield/gocui/gui.go b/vendor/github.com/jesseduffield/gocui/gui.go index 9d848d93d..c239d25a5 100644 --- a/vendor/github.com/jesseduffield/gocui/gui.go +++ b/vendor/github.com/jesseduffield/gocui/gui.go @@ -180,6 +180,8 @@ type Gui struct { suspended bool taskManager *TaskManager + + lastHoverView *View } type NewGuiOpts struct { @@ -836,7 +838,7 @@ func (g *Gui) processRemainingEvents() error { // etc.) func (g *Gui) handleEvent(ev *GocuiEvent) error { switch ev.Type { - case eventKey, eventMouse: + case eventKey, eventMouse, eventMouseMove: return g.onKey(ev) case eventError: return ev.Err @@ -1395,6 +1397,19 @@ func (g *Gui) onKey(ev *GocuiEvent) error { 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: } diff --git a/vendor/github.com/jesseduffield/gocui/tcell_driver.go b/vendor/github.com/jesseduffield/gocui/tcell_driver.go index 6665432c5..96e816b2f 100644 --- a/vendor/github.com/jesseduffield/gocui/tcell_driver.go +++ b/vendor/github.com/jesseduffield/gocui/tcell_driver.go @@ -176,6 +176,7 @@ const ( eventKey eventResize eventMouse + eventMouseMove // only used when no button is down, otherwise it's eventMouse eventFocus eventInterrupt eventError @@ -387,7 +388,11 @@ func (g *Gui) pollEvent() GocuiEvent { if !wheeling { switch dragState { 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 case MAYBE_DRAGGING: if x != lastX || y != lastY { diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index 8933c2c74..07dc08f25 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -56,6 +56,13 @@ type View struct { // tained is true if the viewLines must be updated 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 // 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 @@ -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 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 @@ -188,6 +203,7 @@ type View struct { func (v *View) clearViewLines() { v.tainted = true v.viewLines = nil + v.clearHover() } 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 if ch == 0 { ch = ' ' @@ -756,6 +776,7 @@ func (v *View) WriteRunes(p []rune) { // writeRunes copies slice of runes into internal lines buffer. func (v *View) writeRunes(p []rune) { v.tainted = true + v.clearHover() // Fill with empty cells, if writing outside current view buffer v.makeWriteable(v.wx, v.wy) @@ -1164,7 +1185,7 @@ func (v *View) draw() error { if bgColor == ColorDefault { bgColor = v.BgColor } - if c.hyperlink != "" { + if c.hyperlink != "" && !v.UnderlineHyperLinksOnlyOnHover { fgColor |= AttrUnderline } @@ -1236,6 +1257,15 @@ func (v *View) isPatternMatchedRune(x, y int) (bool, bool) { 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 // point (x, y) of the view. 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.lines[y] = cells + v.clearHover() return nil } @@ -1672,8 +1703,12 @@ func (v *View) ScrollUp(amount int) { amount = v.oy } - v.oy -= amount - v.cy += amount + if amount != 0 { + v.oy -= amount + v.cy += amount + + v.clearHover() + } } // 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 { v.oy += adjustedAmount v.cy -= adjustedAmount + + v.clearHover() } } @@ -1690,12 +1727,18 @@ func (v *View) ScrollLeft(amount int) { if newOx < 0 { newOx = 0 } - v.ox = newOx + if newOx != v.ox { + v.ox = newOx + + v.clearHover() + } } // not applying any limits to this func (v *View) ScrollRight(amount int) { v.ox += amount + + v.clearHover() } 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) } + +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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index f052e5c43..e08e23a02 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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/internal/frame 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 github.com/jesseduffield/gocui # github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10