From 1f2cb35cc96da3496d4e4f56fd30f85e192b2a84 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 8 Nov 2024 22:17:26 +0100 Subject: [PATCH] Refactor: move wrapMessageToWidth to utils/lines.go to make it more generally usable by clients other than ConfirmationHelper, which we will do later in this branch. Rename it to WrapViewLinesToWidth while we're at it. Add tests; in particular, add a sanity check that we wrap lines the same way as gocui does. The tests that are added here are the same ones as in gocui for its lineWrap function, but we'll extend them a bit in later commits in this branch. --- .../helpers/confirmation_helper.go | 59 +----- pkg/utils/lines.go | 55 ++++++ pkg/utils/lines_test.go | 185 ++++++++++++++++++ 3 files changed, 244 insertions(+), 55 deletions(-) diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index dbdc985e8..bbeb07a45 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -3,12 +3,11 @@ package helpers import ( goContext "context" "fmt" - "strings" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/theme" - "github.com/mattn/go-runewidth" + "github.com/jesseduffield/lazygit/pkg/utils" ) type ConfirmationHelper struct { @@ -57,59 +56,9 @@ func (self *ConfirmationHelper) DeactivateConfirmationPrompt() { self.clearConfirmationViewKeyBindings() } -// Temporary hack: we're just duplicating the logic in `gocui.lineWrap` func getMessageHeight(wrap bool, message string, width int) int { - return len(wrapMessageToWidth(wrap, message, width)) -} - -func wrapMessageToWidth(wrap bool, message string, width int) []string { - lines := strings.Split(message, "\n") - if !wrap { - return lines - } - - wrappedLines := make([]string, 0, len(lines)) - - for _, line := range lines { - n := 0 - offset := 0 - lastWhitespaceIndex := -1 - for i, currChr := range line { - rw := runewidth.RuneWidth(currChr) - n += rw - - if n > width { - if currChr == ' ' { - wrappedLines = append(wrappedLines, line[offset:i]) - offset = i + 1 - n = 0 - } else if currChr == '-' { - wrappedLines = append(wrappedLines, line[offset:i]) - offset = i - n = rw - } else if lastWhitespaceIndex != -1 { - if line[lastWhitespaceIndex] == '-' { - wrappedLines = append(wrappedLines, line[offset:lastWhitespaceIndex+1]) - } else { - wrappedLines = append(wrappedLines, line[offset:lastWhitespaceIndex]) - } - offset = lastWhitespaceIndex + 1 - n = runewidth.StringWidth(line[offset : i+1]) - } else { - wrappedLines = append(wrappedLines, line[offset:i]) - offset = i - n = rw - } - lastWhitespaceIndex = -1 - } else if currChr == ' ' || currChr == '-' { - lastWhitespaceIndex = i - } - } - - wrappedLines = append(wrappedLines, line[offset:]) - } - - return wrappedLines + wrappedLines := utils.WrapViewLinesToWidth(wrap, message, width) + return len(wrappedLines) } func (self *ConfirmationHelper) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int, parentPopupContext types.Context) (int, int, int, int) { @@ -327,7 +276,7 @@ func (self *ConfirmationHelper) layoutMenuPrompt(contentWidth int) int { var promptLines []string prompt := self.c.Contexts().Menu.GetPrompt() if len(prompt) > 0 { - promptLines = wrapMessageToWidth(true, prompt, contentWidth) + promptLines = utils.WrapViewLinesToWidth(true, prompt, contentWidth) promptLines = append(promptLines, "") } self.c.Contexts().Menu.SetPromptLines(promptLines) diff --git a/pkg/utils/lines.go b/pkg/utils/lines.go index c70d02ffc..740d4a14c 100644 --- a/pkg/utils/lines.go +++ b/pkg/utils/lines.go @@ -3,6 +3,8 @@ package utils import ( "bytes" "strings" + + "github.com/mattn/go-runewidth" ) // SplitLines takes a multiline string and splits it on newlines @@ -100,3 +102,56 @@ func ScanLinesAndTruncateWhenLongerThanBuffer(maxBufferSize int) func(data []byt return 0, nil, nil } } + +// Wrap lines to a given width. +// If wrap is false, the text is returned as is. +// This code needs to behave the same as `gocui.lineWrap` does. +func WrapViewLinesToWidth(wrap bool, text string, width int) []string { + lines := strings.Split(text, "\n") + if !wrap { + return lines + } + + wrappedLines := make([]string, 0, len(lines)) + + for _, line := range lines { + n := 0 + offset := 0 + lastWhitespaceIndex := -1 + for i, currChr := range line { + rw := runewidth.RuneWidth(currChr) + n += rw + + if n > width { + if currChr == ' ' { + wrappedLines = append(wrappedLines, line[offset:i]) + offset = i + 1 + n = 0 + } else if currChr == '-' { + wrappedLines = append(wrappedLines, line[offset:i]) + offset = i + n = rw + } else if lastWhitespaceIndex != -1 { + if line[lastWhitespaceIndex] == '-' { + wrappedLines = append(wrappedLines, line[offset:lastWhitespaceIndex+1]) + } else { + wrappedLines = append(wrappedLines, line[offset:lastWhitespaceIndex]) + } + offset = lastWhitespaceIndex + 1 + n = runewidth.StringWidth(line[offset : i+1]) + } else { + wrappedLines = append(wrappedLines, line[offset:i]) + offset = i + n = rw + } + lastWhitespaceIndex = -1 + } else if currChr == ' ' || currChr == '-' { + lastWhitespaceIndex = i + } + } + + wrappedLines = append(wrappedLines, line[offset:]) + } + + return wrappedLines +} diff --git a/pkg/utils/lines_test.go b/pkg/utils/lines_test.go index 2192a3780..eb319d619 100644 --- a/pkg/utils/lines_test.go +++ b/pkg/utils/lines_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "github.com/jesseduffield/gocui" "github.com/stretchr/testify/assert" ) @@ -164,3 +165,187 @@ func TestScanLinesAndTruncateWhenLongerThanBuffer(t *testing.T) { assert.EqualValues(t, s.expectedLines, result) } } + +func TestWrapViewLinesToWidth(t *testing.T) { + tests := []struct { + name string + wrap bool + text string + width int + expectedWrappedLines []string + }{ + { + name: "Wrap on space", + wrap: true, + text: "Hello World", + width: 5, + expectedWrappedLines: []string{ + "Hello", + "World", + }, + }, + { + name: "Wrap on hyphen", + wrap: true, + text: "Hello-World", + width: 6, + expectedWrappedLines: []string{ + "Hello-", + "World", + }, + }, + { + name: "Wrap on hyphen 2", + wrap: true, + text: "Blah Hello-World", + width: 12, + expectedWrappedLines: []string{ + "Blah Hello-", + "World", + }, + }, + { + name: "Wrap on hyphen 3", + wrap: true, + text: "Blah Hello-World", + width: 11, + expectedWrappedLines: []string{ + "Blah Hello-", + "World", + }, + }, + { + name: "Wrap on hyphen 4", + wrap: true, + text: "Blah Hello-World", + width: 10, + expectedWrappedLines: []string{ + "Blah Hello", + "-World", + }, + }, + { + name: "Wrap on space 2", + wrap: true, + text: "Blah Hello World", + width: 10, + expectedWrappedLines: []string{ + "Blah Hello", + "World", + }, + }, + { + name: "Wrap on space with more words", + wrap: true, + text: "Longer word here", + width: 10, + expectedWrappedLines: []string{ + "Longer", + "word here", + }, + }, + { + name: "Split word that's too long", + wrap: true, + text: "ThisWordIsWayTooLong", + width: 10, + expectedWrappedLines: []string{ + "ThisWordIs", + "WayTooLong", + }, + }, + { + name: "Split word that's too long over multiple lines", + wrap: true, + text: "ThisWordIsWayTooLong", + width: 5, + expectedWrappedLines: []string{ + "ThisW", + "ordIs", + "WayTo", + "oLong", + }, + }, + { + name: "Lots of hyphens", + wrap: true, + text: "one-two-three-four-five", + width: 8, + expectedWrappedLines: []string{ + "one-two-", + "three-", + "four-", + "five", + }, + }, + { + name: "Several lines using all the available width", + wrap: true, + text: "aaa bb cc ddd-ee ff", + width: 5, + expectedWrappedLines: []string{ + "aaa", + "bb cc", + "ddd-", + "ee ff", + }, + }, + { + name: "Several lines using all the available width, with multi-cell runes", + wrap: true, + text: "ðŸĪðŸĪðŸĪ 🐝🐝 🙉🙉 ðŸĶŠðŸĶŠðŸĶŠ-🐎🐎 ðŸĶĒðŸĶĒ", + width: 9, + expectedWrappedLines: []string{ + "ðŸĪðŸĪðŸĪ", + "🐝🐝 🙉🙉", + "ðŸĶŠðŸĶŠðŸĶŠ-", + "🐎🐎 ðŸĶĒðŸĶĒ", + }, + }, + { + name: "Space in last column", + wrap: true, + text: "hello world", + width: 6, + expectedWrappedLines: []string{ + "hello", + "world", + }, + }, + { + name: "Hyphen in last column", + wrap: true, + text: "hello-world", + width: 6, + expectedWrappedLines: []string{ + "hello-", + "world", + }, + }, + { + name: "English text", + wrap: true, + text: "+The sea reach of the Thames stretched before us like the bedinnind of an interminable waterway. In the offind the sea and the sky were welded todether without a joint, and in the luminous space the tanned sails of the bardes drifting blah blah", + width: 81, + expectedWrappedLines: []string{ + "+The sea reach of the Thames stretched before us like the bedinnind of an", + "interminable waterway. In the offind the sea and the sky were welded todether", + "without a joint, and in the luminous space the tanned sails of the bardes", + "drifting blah blah", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + wrappedLines := WrapViewLinesToWidth(tt.wrap, tt.text, tt.width) + assert.Equal(t, tt.expectedWrappedLines, wrappedLines) + + // As a sanity check, also test that gocui's line wrapping behaves the same way + view := gocui.NewView("", 0, 0, tt.width+1, 1000, gocui.OutputNormal) + assert.Equal(t, tt.width, view.InnerWidth()) + view.Wrap = tt.wrap + view.SetContent(tt.text) + assert.Equal(t, wrappedLines, view.ViewBufferLines()) + }) + } +}