mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-05-17 22:32:58 +02:00
Auto-render hyperlinks (#3914)
- **PR Description** Add a facility to gocui.View to enable auto-rendering of https hyperlinks. Then, use it for the Command Log panel (as an alternative approach to #3911), and also in the status view and in confirmation popups to get rid of some code that used to do this manually. - **Please check if the PR fulfills these requirements** * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [ ] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [ ] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [ ] If a new UserConfig entry was added, make sure it can be hot-reloaded (see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig)) * [ ] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
This commit is contained in:
commit
d11e11d179
2
go.mod
2
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.20240906064314-bfab49c720d7
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20240928100326-393cf89a5d3f
|
||||
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
|
||||
|
4
go.sum
4
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.20240906064314-bfab49c720d7 h1:QeLCKRAt4T6sBg5tSrOc4OojCuAcPxUA+4vNMPY4aH4=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20240906064314-bfab49c720d7/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20240928100326-393cf89a5d3f h1:ZzsAUDwPFLPITKLcJpMSqt/3rERdI8YRZKr2l0plrls=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20240928100326-393cf89a5d3f/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=
|
||||
|
@ -221,7 +221,7 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
|
||||
confirmationView.RenderTextArea()
|
||||
} else {
|
||||
self.c.ResetViewOrigin(confirmationView)
|
||||
self.c.SetViewContent(confirmationView, style.AttrBold.Sprint(underlineLinks(opts.Prompt)))
|
||||
self.c.SetViewContent(confirmationView, style.AttrBold.Sprint(opts.Prompt))
|
||||
}
|
||||
|
||||
self.setKeyBindings(cancel, opts)
|
||||
@ -233,28 +233,6 @@ func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts typ
|
||||
self.c.Context().Push(self.c.Contexts().Confirmation)
|
||||
}
|
||||
|
||||
func underlineLinks(text string) string {
|
||||
result := ""
|
||||
remaining := text
|
||||
for {
|
||||
linkStart := strings.Index(remaining, "https://")
|
||||
if linkStart == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
linkEnd := strings.IndexAny(remaining[linkStart:], " \n>")
|
||||
if linkEnd == -1 {
|
||||
linkEnd = len(remaining)
|
||||
} else {
|
||||
linkEnd += linkStart
|
||||
}
|
||||
underlinedLink := style.PrintSimpleHyperlink(remaining[linkStart:linkEnd])
|
||||
result += remaining[:linkStart] + underlinedLink
|
||||
remaining = remaining[linkEnd:]
|
||||
}
|
||||
return result + remaining
|
||||
}
|
||||
|
||||
func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts types.CreatePopupPanelOpts) {
|
||||
var onConfirm func() error
|
||||
if opts.HandleConfirmPrompt != nil {
|
||||
|
@ -1,63 +0,0 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gookit/color"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/xo/terminfo"
|
||||
)
|
||||
|
||||
func Test_underlineLinks(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
name string
|
||||
text string
|
||||
expectedResult string
|
||||
}{
|
||||
{
|
||||
name: "empty string",
|
||||
text: "",
|
||||
expectedResult: "",
|
||||
},
|
||||
{
|
||||
name: "no links",
|
||||
text: "abc",
|
||||
expectedResult: "abc",
|
||||
},
|
||||
{
|
||||
name: "entire string is a link",
|
||||
text: "https://example.com",
|
||||
expectedResult: "\x1b]8;;https://example.com\x1b\\https://example.com\x1b]8;;\x1b\\",
|
||||
},
|
||||
{
|
||||
name: "link preceded and followed by text",
|
||||
text: "bla https://example.com xyz",
|
||||
expectedResult: "bla \x1b]8;;https://example.com\x1b\\https://example.com\x1b]8;;\x1b\\ xyz",
|
||||
},
|
||||
{
|
||||
name: "more than one link",
|
||||
text: "bla https://link1 blubb https://link2 xyz",
|
||||
expectedResult: "bla \x1b]8;;https://link1\x1b\\https://link1\x1b]8;;\x1b\\ blubb \x1b]8;;https://link2\x1b\\https://link2\x1b]8;;\x1b\\ xyz",
|
||||
},
|
||||
{
|
||||
name: "link in angle brackets",
|
||||
text: "See <https://example.com> for details",
|
||||
expectedResult: "See <\x1b]8;;https://example.com\x1b\\https://example.com\x1b]8;;\x1b\\> for details",
|
||||
},
|
||||
{
|
||||
name: "link followed by newline",
|
||||
text: "URL: https://example.com\nNext line",
|
||||
expectedResult: "URL: \x1b]8;;https://example.com\x1b\\https://example.com\x1b]8;;\x1b\\\nNext line",
|
||||
},
|
||||
}
|
||||
|
||||
oldColorLevel := color.ForceSetColorLevel(terminfo.ColorLevelMillions)
|
||||
defer color.ForceSetColorLevel(oldColorLevel)
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
result := underlineLinks(s.text)
|
||||
assert.Equal(t, s.expectedResult, result)
|
||||
})
|
||||
}
|
||||
}
|
@ -208,12 +208,12 @@ func (self *StatusController) showDashboard() {
|
||||
[]string{
|
||||
lazygitTitle(),
|
||||
fmt.Sprintf("Copyright %d Jesse Duffield", time.Now().Year()),
|
||||
fmt.Sprintf("Keybindings: %s", style.PrintSimpleHyperlink(fmt.Sprintf(constants.Links.Docs.Keybindings, versionStr))),
|
||||
fmt.Sprintf("Config Options: %s", style.PrintSimpleHyperlink(fmt.Sprintf(constants.Links.Docs.Config, versionStr))),
|
||||
fmt.Sprintf("Tutorial: %s", style.PrintSimpleHyperlink(constants.Links.Docs.Tutorial)),
|
||||
fmt.Sprintf("Raise an Issue: %s", style.PrintSimpleHyperlink(constants.Links.Issues)),
|
||||
fmt.Sprintf("Release Notes: %s", style.PrintSimpleHyperlink(constants.Links.Releases)),
|
||||
style.FgMagenta.Sprintf("Become a sponsor: %s", style.PrintSimpleHyperlink(constants.Links.Donate)), // caffeine ain't free
|
||||
fmt.Sprintf("Keybindings: %s", fmt.Sprintf(constants.Links.Docs.Keybindings, versionStr)),
|
||||
fmt.Sprintf("Config Options: %s", fmt.Sprintf(constants.Links.Docs.Config, versionStr)),
|
||||
fmt.Sprintf("Tutorial: %s", constants.Links.Docs.Tutorial),
|
||||
fmt.Sprintf("Raise an Issue: %s", constants.Links.Issues),
|
||||
fmt.Sprintf("Release Notes: %s", constants.Links.Releases),
|
||||
style.FgMagenta.Sprintf("Become a sponsor: %s", constants.Links.Donate), // caffeine ain't free
|
||||
}, "\n\n") + "\n"
|
||||
|
||||
self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
|
@ -118,6 +118,7 @@ func (gui *Gui) createAllViews() error {
|
||||
view.Wrap = true
|
||||
view.IgnoreCarriageReturns = true
|
||||
view.UnderlineHyperLinksOnlyOnHover = true
|
||||
view.AutoRenderHyperLinks = true
|
||||
}
|
||||
|
||||
gui.Views.Staging.Title = gui.c.Tr.UnstagedChanges
|
||||
@ -157,6 +158,7 @@ func (gui *Gui) createAllViews() error {
|
||||
|
||||
gui.Views.Confirmation.Visible = false
|
||||
gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.promptEditor)
|
||||
gui.Views.Confirmation.AutoRenderHyperLinks = true
|
||||
|
||||
gui.Views.Suggestions.Visible = false
|
||||
|
||||
@ -171,6 +173,7 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.Extras.Title = gui.c.Tr.CommandLog
|
||||
gui.Views.Extras.Autoscroll = true
|
||||
gui.Views.Extras.Wrap = true
|
||||
gui.Views.Extras.AutoRenderHyperLinks = true
|
||||
|
||||
gui.Views.Snake.Title = gui.c.Tr.SnakeTitle
|
||||
gui.Views.Snake.FgColor = gocui.ColorGreen
|
||||
|
61
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
61
vendor/github.com/jesseduffield/gocui/view.go
generated
vendored
@ -183,6 +183,10 @@ 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 automatically recognize https: URLs in the content written to it and render
|
||||
// them as hyperlinks
|
||||
AutoRenderHyperLinks bool
|
||||
|
||||
// if true, the view will underline hyperlinks only when the cursor is on
|
||||
// them; otherwise, they will always be underlined
|
||||
UnderlineHyperLinksOnlyOnHover bool
|
||||
@ -780,6 +784,7 @@ func (v *View) writeRunes(p []rune) {
|
||||
for _, r := range p {
|
||||
switch r {
|
||||
case '\n':
|
||||
v.autoRenderHyperlinksInCurrentLine()
|
||||
if c, ok := v.readCell(v.wx+1, v.wy); !ok || c.chr == 0 {
|
||||
v.writeCells(v.wx, v.wy, []cell{{
|
||||
chr: 0,
|
||||
@ -793,6 +798,7 @@ func (v *View) writeRunes(p []rune) {
|
||||
v.lines = append(v.lines, nil)
|
||||
}
|
||||
case '\r':
|
||||
v.autoRenderHyperlinksInCurrentLine()
|
||||
if c, ok := v.readCell(v.wx, v.wy); !ok || c.chr == 0 {
|
||||
v.writeCells(v.wx, v.wy, []cell{{
|
||||
chr: 0,
|
||||
@ -829,6 +835,61 @@ func (v *View) writeString(s string) {
|
||||
v.writeRunes([]rune(s))
|
||||
}
|
||||
|
||||
func findSubstring(line []cell, substringToFind []rune) int {
|
||||
for i := 0; i < len(line)-len(substringToFind); i++ {
|
||||
for j := 0; j < len(substringToFind); j++ {
|
||||
if line[i+j].chr != substringToFind[j] {
|
||||
break
|
||||
}
|
||||
if j == len(substringToFind)-1 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (v *View) autoRenderHyperlinksInCurrentLine() {
|
||||
if !v.AutoRenderHyperLinks {
|
||||
return
|
||||
}
|
||||
|
||||
// We need a heuristic to find the end of a hyperlink. Searching for the
|
||||
// first character that is not a valid URI character is not quite good
|
||||
// enough, because in markdown it's common to have a hyperlink followed by a
|
||||
// ')', so we want to stop there. Hopefully URLs containing ')' are uncommon
|
||||
// enough that this is not a problem.
|
||||
lineEndCharacters := map[rune]bool{
|
||||
'\000': true,
|
||||
' ': true,
|
||||
'\n': true,
|
||||
'>': true,
|
||||
'"': true,
|
||||
')': true,
|
||||
}
|
||||
line := v.lines[v.wy]
|
||||
start := 0
|
||||
for {
|
||||
linkStart := findSubstring(line[start:], []rune("https://"))
|
||||
if linkStart == -1 {
|
||||
break
|
||||
}
|
||||
linkStart += start
|
||||
link := ""
|
||||
linkEnd := linkStart
|
||||
for ; linkEnd < len(line); linkEnd++ {
|
||||
if _, ok := lineEndCharacters[line[linkEnd].chr]; ok {
|
||||
break
|
||||
}
|
||||
link += string(line[linkEnd].chr)
|
||||
}
|
||||
for i := linkStart; i < linkEnd; i++ {
|
||||
v.lines[v.wy][i].hyperlink = link
|
||||
}
|
||||
start = linkEnd
|
||||
}
|
||||
}
|
||||
|
||||
// parseInput parses char by char the input written to the View. It returns nil
|
||||
// while processing ESC sequences. Otherwise, it returns a cell slice that
|
||||
// contains the processed data.
|
||||
|
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@ -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.20240906064314-bfab49c720d7
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20240928100326-393cf89a5d3f
|
||||
## explicit; go 1.12
|
||||
github.com/jesseduffield/gocui
|
||||
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
|
||||
|
Loading…
x
Reference in New Issue
Block a user