From 65b731f48463ebeb87e5b6e3be621f8195ac0625 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 28 Sep 2024 12:04:32 +0200 Subject: [PATCH 1/4] Bump gocui --- go.mod | 2 +- go.sum | 4 +- vendor/github.com/jesseduffield/gocui/view.go | 61 +++++++++++++++++++ vendor/modules.txt | 2 +- 4 files changed, 65 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 6836caeb9c9..95587f286ba 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.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 diff --git a/go.sum b/go.sum index 77fc93c8b88..12ce5def99d 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.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= diff --git a/vendor/github.com/jesseduffield/gocui/view.go b/vendor/github.com/jesseduffield/gocui/view.go index b84405e4a62..c3e183a87bf 100644 --- a/vendor/github.com/jesseduffield/gocui/view.go +++ b/vendor/github.com/jesseduffield/gocui/view.go @@ -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. diff --git a/vendor/modules.txt b/vendor/modules.txt index 0405dfd402f..720f145a6bd 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.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 From 1ceb5a6b37a4a1c6e98cb9c0c8c401dc2222099f Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 15 Sep 2024 14:08:56 +0200 Subject: [PATCH 2/4] Turn on AutoRenderHyperLinks in the Command Log panel Some commands output hyperlinks, and it's useful to be able to click them. --- pkg/gui/views.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/gui/views.go b/pkg/gui/views.go index f561eac3753..839fe15fc30 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -171,6 +171,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 From 26e3a93fc3ce88180e15224a1bdca54436045484 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 15 Sep 2024 14:12:41 +0200 Subject: [PATCH 3/4] Use AutoRenderHyperLinks in main views This allows clicking on links in commit messages, for examples. It also affects the status view, so we can get rid of the manual hyperlinking there. --- pkg/gui/controllers/status_controller.go | 12 ++++++------ pkg/gui/views.go | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/pkg/gui/controllers/status_controller.go b/pkg/gui/controllers/status_controller.go index c15e85f8c96..68028302f57 100644 --- a/pkg/gui/controllers/status_controller.go +++ b/pkg/gui/controllers/status_controller.go @@ -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{ diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 839fe15fc30..c7b5e40ffb0 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -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 From 825f5c0a91732028e26d9366ecae5d005ae908ef Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sun, 15 Sep 2024 14:16:50 +0200 Subject: [PATCH 4/4] Use AutoRenderHyperLinks in confirmation view This allows us to get rid of the underlineLinks function. --- .../helpers/confirmation_helper.go | 24 +------ .../helpers/confirmation_helper_test.go | 63 ------------------- pkg/gui/views.go | 1 + 3 files changed, 2 insertions(+), 86 deletions(-) delete mode 100644 pkg/gui/controllers/helpers/confirmation_helper_test.go diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 3ffdae6a179..89a150fca5e 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -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 { diff --git a/pkg/gui/controllers/helpers/confirmation_helper_test.go b/pkg/gui/controllers/helpers/confirmation_helper_test.go deleted file mode 100644 index de3db935899..00000000000 --- a/pkg/gui/controllers/helpers/confirmation_helper_test.go +++ /dev/null @@ -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 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) - }) - } -} diff --git a/pkg/gui/views.go b/pkg/gui/views.go index c7b5e40ffb0..526696b25b0 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -158,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