From 361dc3e6d9cd9d70c1d68c3ea889b35dc78b49aa Mon Sep 17 00:00:00 2001 From: kyu08 <49891479+kyu08@users.noreply.github.com> Date: Fri, 9 Aug 2024 18:01:26 +0900 Subject: [PATCH 01/11] fix(viewport): fix height calculation method --- go.mod | 4 ++++ go.sum | 10 ++++++++++ viewport/viewport.go | 16 ++++++++++++++- viewport/viewport_test.go | 41 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 viewport/viewport_test.go diff --git a/go.mod b/go.mod index 4d7b1bbcf..52e57dbb3 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/muesli/termenv v0.15.2 github.com/rivo/uniseg v0.4.7 github.com/sahilm/fuzzy v0.1.1 + github.com/stretchr/testify v1.9.0 ) require ( @@ -22,14 +23,17 @@ require ( github.com/charmbracelet/x/input v0.1.0 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.3.8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 41f2d1d22..a5ffa50ad 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXD github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -38,11 +40,15 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= @@ -54,3 +60,7 @@ golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/viewport/viewport.go b/viewport/viewport.go index e0a4cc33f..e56b77775 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -111,7 +111,8 @@ func (m *Model) SetContent(s string) { // maxYOffset returns the maximum possible value of the y-offset based on the // viewport's content and set height. func (m Model) maxYOffset() int { - return max(0, len(m.lines)-m.Height) + linesHeight := countHeightBasedOnWidth(m.lines, m.Width) + return max(0, linesHeight-m.Height) } // visibleLines returns the lines that should currently be visible in the @@ -403,3 +404,16 @@ func max(a, b int) int { } return b } + +func countHeightBasedOnWidth(lines []string, width int) int { + h := 0 + for _, line := range lines { + if len(line) <= width { + h++ + continue + } + h += int(math.Ceil(float64(len(line)) / float64(width))) + } + + return h +} diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go new file mode 100644 index 000000000..02d69eaa3 --- /dev/null +++ b/viewport/viewport_test.go @@ -0,0 +1,41 @@ +package viewport + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_countHeightBasedOnWidth(t *testing.T) { + t.Parallel() + tests := map[string]struct { + lines []string + width int + want int + }{ + "Empty lines": { + lines: []string{}, + width: 0, + want: 0, + }, + "Lines within width": { + lines: []string{"123", "123"}, + width: 5, + want: 2, + }, + "Lines over width": { + lines: []string{"1234567890", "123"}, + width: 5, + want: 3, + }, + } + + for name, tt := range tests { + tt := tt + t.Run(name, func(t *testing.T) { + t.Parallel() + got := countHeightBasedOnWidth(tt.lines, tt.width) + assert.Equal(t, tt.want, got) + }) + } +} From ee209540653a51302b2bf394e5b737d18e6c9d7e Mon Sep 17 00:00:00 2001 From: kyu08 <49891479+kyu08@users.noreply.github.com> Date: Wed, 14 Aug 2024 09:53:21 +0900 Subject: [PATCH 02/11] fixup! fix(viewport): fix height calculation method --- go.mod | 4 ---- go.sum | 10 ---------- viewport/viewport_test.go | 6 +++--- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 52e57dbb3..4d7b1bbcf 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,6 @@ require ( github.com/muesli/termenv v0.15.2 github.com/rivo/uniseg v0.4.7 github.com/sahilm/fuzzy v0.1.1 - github.com/stretchr/testify v1.9.0 ) require ( @@ -23,17 +22,14 @@ require ( github.com/charmbracelet/x/input v0.1.0 // indirect github.com/charmbracelet/x/term v0.1.1 // indirect github.com/charmbracelet/x/windows v0.1.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.20.0 // indirect golang.org/x/text v0.3.8 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index a5ffa50ad..41f2d1d22 100644 --- a/go.sum +++ b/go.sum @@ -18,8 +18,6 @@ github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXD github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= github.com/charmbracelet/x/windows v0.1.0 h1:gTaxdvzDM5oMa/I2ZNF7wN78X/atWemG9Wph7Ika2k4= github.com/charmbracelet/x/windows v0.1.0/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= @@ -40,15 +38,11 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= @@ -60,7 +54,3 @@ golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go index 02d69eaa3..dc200cedd 100644 --- a/viewport/viewport_test.go +++ b/viewport/viewport_test.go @@ -2,8 +2,6 @@ package viewport import ( "testing" - - "github.com/stretchr/testify/assert" ) func Test_countHeightBasedOnWidth(t *testing.T) { @@ -35,7 +33,9 @@ func Test_countHeightBasedOnWidth(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() got := countHeightBasedOnWidth(tt.lines, tt.width) - assert.Equal(t, tt.want, got) + if tt.want != got { + t.Errorf("expected %v, got %v", tt.want, got) + } }) } } From eaff7e7f766cb409f425cdbf6177f54d0a733e06 Mon Sep 17 00:00:00 2001 From: kyu08 <49891479+kyu08@users.noreply.github.com> Date: Sun, 25 Aug 2024 23:47:13 +0900 Subject: [PATCH 03/11] fix the panic caused by incorrect usage of lines --- viewport/viewport.go | 31 ++++++++++++++++++++----------- viewport/viewport_test.go | 38 ++++++++++++++++++++++---------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index e56b77775..ae044c22c 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -111,7 +111,7 @@ func (m *Model) SetContent(s string) { // maxYOffset returns the maximum possible value of the y-offset based on the // viewport's content and set height. func (m Model) maxYOffset() int { - linesHeight := countHeightBasedOnWidth(m.lines, m.Width) + linesHeight := len(linesToActualDisplayedLines(m.lines, m.Width)) return max(0, linesHeight-m.Height) } @@ -119,9 +119,11 @@ func (m Model) maxYOffset() int { // viewport. func (m Model) visibleLines() (lines []string) { if len(m.lines) > 0 { + actualDisplayedLines := linesToActualDisplayedLines(m.lines, m.Width) top := max(0, m.YOffset) - bottom := clamp(m.YOffset+m.Height, top, len(m.lines)) - lines = m.lines[top:bottom] + bottom := clamp(m.YOffset+m.Height, top, len(actualDisplayedLines)) + + lines = actualDisplayedLines[top:bottom] } return lines } @@ -192,7 +194,7 @@ func (m *Model) LineDown(n int) (lines []string) { // Gather lines to send off for performance scrolling. bottom := clamp(m.YOffset+m.Height, 0, len(m.lines)) top := clamp(m.YOffset+m.Height-n, 0, bottom) - return m.lines[top:bottom] + return linesToActualDisplayedLines(m.lines, m.Width)[top:bottom] } // LineUp moves the view down by the given number of lines. Returns the new @@ -209,7 +211,7 @@ func (m *Model) LineUp(n int) (lines []string) { // Gather lines to send off for performance scrolling. top := max(0, m.YOffset) bottom := clamp(m.YOffset+n, 0, m.maxYOffset()) - return m.lines[top:bottom] + return linesToActualDisplayedLines(m.lines, m.Width)[top:bottom] } // TotalLineCount returns the total number of lines (both hidden and visible) within the viewport. @@ -405,15 +407,22 @@ func max(a, b int) int { return b } -func countHeightBasedOnWidth(lines []string, width int) int { - h := 0 +// linesToActualDisplayedLines converts lines to the actual lines considering window width. +// If there is a line that is longer than the width, it will be displayed in multiple lines. +// For more details: https://github.com/charmbracelet/bubbles/pull/578 +// If you want to know actual behavior of this function, please check out the unit tests. +func linesToActualDisplayedLines(lines []string, width int) []string { + var actual []string for _, line := range lines { if len(line) <= width { - h++ + actual = append(actual, line) continue } - h += int(math.Ceil(float64(len(line)) / float64(width))) + for width < len(line) { + actual = append(actual, line[:width]) + line = line[width:] + } + actual = append(actual, line) } - - return h + return actual } diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go index dc200cedd..47cb2bcbd 100644 --- a/viewport/viewport_test.go +++ b/viewport/viewport_test.go @@ -4,27 +4,27 @@ import ( "testing" ) -func Test_countHeightBasedOnWidth(t *testing.T) { +func Test_linesToActualDisplayedLines(t *testing.T) { t.Parallel() tests := map[string]struct { lines []string width int - want int + want []string }{ - "Empty lines": { + "empty slice": { lines: []string{}, - width: 0, - want: 0, + width: 3, + want: []string{}, }, - "Lines within width": { - lines: []string{"123", "123"}, - width: 5, - want: 2, + "all lines are within width": { + lines: []string{"aaa", "bbb", "ccc"}, + width: 3, + want: []string{"aaa", "bbb", "ccc"}, }, - "Lines over width": { - lines: []string{"1234567890", "123"}, - width: 5, - want: 3, + "some lines exceeds width": { + lines: []string{"aaaaaa", "bbbbbbbb", "ccc"}, + width: 3, + want: []string{"aaa", "aaa", "bbb", "bbb", "bb", "ccc"}, }, } @@ -32,9 +32,15 @@ func Test_countHeightBasedOnWidth(t *testing.T) { tt := tt t.Run(name, func(t *testing.T) { t.Parallel() - got := countHeightBasedOnWidth(tt.lines, tt.width) - if tt.want != got { - t.Errorf("expected %v, got %v", tt.want, got) + got := linesToActualDisplayedLines(tt.lines, tt.width) + + if len(got) != len(tt.want) { + t.Errorf("expected len is %d but got %d", len(tt.want), len(got)) + } + for i := range tt.want { + if tt.want[i] != got[i] { + t.Errorf("expected %s but got %s", tt.want[i], got[i]) + } } }) } From 5e1cef42305edf5507e316dbda2cb3b5cd6cef18 Mon Sep 17 00:00:00 2001 From: kyu08 <49891479+kyu08@users.noreply.github.com> Date: Mon, 26 Aug 2024 00:44:25 +0900 Subject: [PATCH 04/11] fix ScrollPercent --- viewport/viewport.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index ae044c22c..dea456008 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -87,12 +87,14 @@ func (m Model) PastBottom() bool { // ScrollPercent returns the amount scrolled as a float between 0 and 1. func (m Model) ScrollPercent() float64 { - if m.Height >= len(m.lines) { + actualDisplayedLength := len(linesToActualDisplayedLines(m.lines, m.Width)) + + if m.Height >= actualDisplayedLength { return 1.0 } y := float64(m.YOffset) h := float64(m.Height) - t := float64(len(m.lines)) + t := float64(actualDisplayedLength) v := y / (t - h) return math.Max(0.0, math.Min(1.0, v)) } From 8d78f89c0e24b391a85190869eceda8ce4a5d1fa Mon Sep 17 00:00:00 2001 From: kyu08 <49891479+kyu08@users.noreply.github.com> Date: Mon, 26 Aug 2024 01:02:49 +0900 Subject: [PATCH 05/11] rename the function and variables --- viewport/viewport.go | 22 +++++++++++----------- viewport/viewport_test.go | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index dea456008..19245daad 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -87,14 +87,14 @@ func (m Model) PastBottom() bool { // ScrollPercent returns the amount scrolled as a float between 0 and 1. func (m Model) ScrollPercent() float64 { - actualDisplayedLength := len(linesToActualDisplayedLines(m.lines, m.Width)) + actuallyDisplayedLength := len(linesToActuallyDisplayedLines(m.lines, m.Width)) - if m.Height >= actualDisplayedLength { + if m.Height >= actuallyDisplayedLength { return 1.0 } y := float64(m.YOffset) h := float64(m.Height) - t := float64(actualDisplayedLength) + t := float64(actuallyDisplayedLength) v := y / (t - h) return math.Max(0.0, math.Min(1.0, v)) } @@ -113,7 +113,7 @@ func (m *Model) SetContent(s string) { // maxYOffset returns the maximum possible value of the y-offset based on the // viewport's content and set height. func (m Model) maxYOffset() int { - linesHeight := len(linesToActualDisplayedLines(m.lines, m.Width)) + linesHeight := len(linesToActuallyDisplayedLines(m.lines, m.Width)) return max(0, linesHeight-m.Height) } @@ -121,11 +121,11 @@ func (m Model) maxYOffset() int { // viewport. func (m Model) visibleLines() (lines []string) { if len(m.lines) > 0 { - actualDisplayedLines := linesToActualDisplayedLines(m.lines, m.Width) + actuallyDisplayedLines := linesToActuallyDisplayedLines(m.lines, m.Width) top := max(0, m.YOffset) - bottom := clamp(m.YOffset+m.Height, top, len(actualDisplayedLines)) + bottom := clamp(m.YOffset+m.Height, top, len(actuallyDisplayedLines)) - lines = actualDisplayedLines[top:bottom] + lines = actuallyDisplayedLines[top:bottom] } return lines } @@ -196,7 +196,7 @@ func (m *Model) LineDown(n int) (lines []string) { // Gather lines to send off for performance scrolling. bottom := clamp(m.YOffset+m.Height, 0, len(m.lines)) top := clamp(m.YOffset+m.Height-n, 0, bottom) - return linesToActualDisplayedLines(m.lines, m.Width)[top:bottom] + return linesToActuallyDisplayedLines(m.lines, m.Width)[top:bottom] } // LineUp moves the view down by the given number of lines. Returns the new @@ -213,7 +213,7 @@ func (m *Model) LineUp(n int) (lines []string) { // Gather lines to send off for performance scrolling. top := max(0, m.YOffset) bottom := clamp(m.YOffset+n, 0, m.maxYOffset()) - return linesToActualDisplayedLines(m.lines, m.Width)[top:bottom] + return linesToActuallyDisplayedLines(m.lines, m.Width)[top:bottom] } // TotalLineCount returns the total number of lines (both hidden and visible) within the viewport. @@ -409,11 +409,11 @@ func max(a, b int) int { return b } -// linesToActualDisplayedLines converts lines to the actual lines considering window width. +// linesToActuallyDisplayedLines converts lines to the lines actually displayed considering window width. // If there is a line that is longer than the width, it will be displayed in multiple lines. // For more details: https://github.com/charmbracelet/bubbles/pull/578 // If you want to know actual behavior of this function, please check out the unit tests. -func linesToActualDisplayedLines(lines []string, width int) []string { +func linesToActuallyDisplayedLines(lines []string, width int) []string { var actual []string for _, line := range lines { if len(line) <= width { diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go index 47cb2bcbd..a94cf3e0a 100644 --- a/viewport/viewport_test.go +++ b/viewport/viewport_test.go @@ -32,7 +32,7 @@ func Test_linesToActualDisplayedLines(t *testing.T) { tt := tt t.Run(name, func(t *testing.T) { t.Parallel() - got := linesToActualDisplayedLines(tt.lines, tt.width) + got := linesToActuallyDisplayedLines(tt.lines, tt.width) if len(got) != len(tt.want) { t.Errorf("expected len is %d but got %d", len(tt.want), len(got)) From eb7b9d5a0e1ffe012be413ca4445a9b91fdf033f Mon Sep 17 00:00:00 2001 From: kyu08 <49891479+kyu08@users.noreply.github.com> Date: Mon, 26 Aug 2024 10:35:51 +0900 Subject: [PATCH 06/11] fix: use actual lines in `SetContent` `GoToBottom()` should be called when the offset exceeds the number of actual lines, not the number of content lines. --- viewport/viewport.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index 19245daad..4a62623fd 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -105,7 +105,7 @@ func (m *Model) SetContent(s string) { s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings m.lines = strings.Split(s, "\n") - if m.YOffset > len(m.lines)-1 { + if m.YOffset > len(linesToActuallyDisplayedLines(m.lines, m.Width))-1 { m.GotoBottom() } } From 6f9f512c617447b7d23eb52ee8ac1af2803426b7 Mon Sep 17 00:00:00 2001 From: kyu08 <49891479+kyu08@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:01:17 +0900 Subject: [PATCH 07/11] fix: the test function's name --- viewport/viewport_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go index a94cf3e0a..d7cdbfda7 100644 --- a/viewport/viewport_test.go +++ b/viewport/viewport_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func Test_linesToActualDisplayedLines(t *testing.T) { +func Test_linesToActuallyDisplayedLines(t *testing.T) { t.Parallel() tests := map[string]struct { lines []string From a3e0450b7e45b79a8a5c6bc6c3df9283e0631311 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Sat, 7 Sep 2024 09:01:13 -0700 Subject: [PATCH 08/11] refactor(viewport): clarify naming for wrapped lines --- viewport/viewport.go | 37 +++++++++++++++++-------------------- viewport/viewport_test.go | 4 ++-- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index 4a62623fd..1f60ec19e 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -87,14 +87,14 @@ func (m Model) PastBottom() bool { // ScrollPercent returns the amount scrolled as a float between 0 and 1. func (m Model) ScrollPercent() float64 { - actuallyDisplayedLength := len(linesToActuallyDisplayedLines(m.lines, m.Width)) + wrappedLines := len(wrap(m.lines, m.Width)) - if m.Height >= actuallyDisplayedLength { + if m.Height >= wrappedLines { return 1.0 } y := float64(m.YOffset) h := float64(m.Height) - t := float64(actuallyDisplayedLength) + t := float64(wrappedLines) v := y / (t - h) return math.Max(0.0, math.Min(1.0, v)) } @@ -105,7 +105,7 @@ func (m *Model) SetContent(s string) { s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings m.lines = strings.Split(s, "\n") - if m.YOffset > len(linesToActuallyDisplayedLines(m.lines, m.Width))-1 { + if m.YOffset > len(wrap(m.lines, m.Width))-1 { m.GotoBottom() } } @@ -113,7 +113,7 @@ func (m *Model) SetContent(s string) { // maxYOffset returns the maximum possible value of the y-offset based on the // viewport's content and set height. func (m Model) maxYOffset() int { - linesHeight := len(linesToActuallyDisplayedLines(m.lines, m.Width)) + linesHeight := len(wrap(m.lines, m.Width)) return max(0, linesHeight-m.Height) } @@ -121,11 +121,11 @@ func (m Model) maxYOffset() int { // viewport. func (m Model) visibleLines() (lines []string) { if len(m.lines) > 0 { - actuallyDisplayedLines := linesToActuallyDisplayedLines(m.lines, m.Width) + wrappedLines := wrap(m.lines, m.Width) top := max(0, m.YOffset) - bottom := clamp(m.YOffset+m.Height, top, len(actuallyDisplayedLines)) + bottom := clamp(m.YOffset+m.Height, top, len(wrappedLines)) - lines = actuallyDisplayedLines[top:bottom] + lines = wrappedLines[top:bottom] } return lines } @@ -196,7 +196,7 @@ func (m *Model) LineDown(n int) (lines []string) { // Gather lines to send off for performance scrolling. bottom := clamp(m.YOffset+m.Height, 0, len(m.lines)) top := clamp(m.YOffset+m.Height-n, 0, bottom) - return linesToActuallyDisplayedLines(m.lines, m.Width)[top:bottom] + return wrap(m.lines, m.Width)[top:bottom] } // LineUp moves the view down by the given number of lines. Returns the new @@ -213,7 +213,7 @@ func (m *Model) LineUp(n int) (lines []string) { // Gather lines to send off for performance scrolling. top := max(0, m.YOffset) bottom := clamp(m.YOffset+n, 0, m.maxYOffset()) - return linesToActuallyDisplayedLines(m.lines, m.Width)[top:bottom] + return wrap(m.lines, m.Width)[top:bottom] } // TotalLineCount returns the total number of lines (both hidden and visible) within the viewport. @@ -409,22 +409,19 @@ func max(a, b int) int { return b } -// linesToActuallyDisplayedLines converts lines to the lines actually displayed considering window width. -// If there is a line that is longer than the width, it will be displayed in multiple lines. -// For more details: https://github.com/charmbracelet/bubbles/pull/578 -// If you want to know actual behavior of this function, please check out the unit tests. -func linesToActuallyDisplayedLines(lines []string, width int) []string { - var actual []string +// wrap returns lines wrapped to the given width. +func wrap(lines []string, width int) []string { + var out []string for _, line := range lines { if len(line) <= width { - actual = append(actual, line) + out = append(out, line) continue } for width < len(line) { - actual = append(actual, line[:width]) + out = append(out, line[:width]) line = line[width:] } - actual = append(actual, line) + out = append(out, line) } - return actual + return out } diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go index d7cdbfda7..3993a3c8a 100644 --- a/viewport/viewport_test.go +++ b/viewport/viewport_test.go @@ -4,7 +4,7 @@ import ( "testing" ) -func Test_linesToActuallyDisplayedLines(t *testing.T) { +func TestWrap(t *testing.T) { t.Parallel() tests := map[string]struct { lines []string @@ -32,7 +32,7 @@ func Test_linesToActuallyDisplayedLines(t *testing.T) { tt := tt t.Run(name, func(t *testing.T) { t.Parallel() - got := linesToActuallyDisplayedLines(tt.lines, tt.width) + got := wrap(tt.lines, tt.width) if len(got) != len(tt.want) { t.Errorf("expected len is %d but got %d", len(tt.want), len(got)) From ac3e89b62cf14c6161ef12d511bdcc6933e76d28 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Sat, 7 Sep 2024 10:19:16 -0700 Subject: [PATCH 09/11] refactor(test): wrap words when possible --- viewport/viewport.go | 18 +++++++++--------- viewport/viewport_test.go | 11 +++++++---- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index 1f60ec19e..539457ccb 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -7,6 +7,7 @@ import ( "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/ansi" ) // New returns a new model with the given width and height as well as default @@ -413,15 +414,14 @@ func max(a, b int) int { func wrap(lines []string, width int) []string { var out []string for _, line := range lines { - if len(line) <= width { - out = append(out, line) - continue - } - for width < len(line) { - out = append(out, line[:width]) - line = line[width:] - } - out = append(out, line) + // word wrap lines + wrapWords := ansi.Wordwrap(line, width, "") + // wrap lines (handles lines that could not be word wrapped) + wrap := ansi.Hardwrap(wrapWords, width, true) + // split string by new lines + wrapLines := strings.Split(strings.TrimSpace(wrap), "\n") + + out = append(out, wrapLines...) } return out } diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go index 3993a3c8a..0dc797462 100644 --- a/viewport/viewport_test.go +++ b/viewport/viewport_test.go @@ -26,20 +26,23 @@ func TestWrap(t *testing.T) { width: 3, want: []string{"aaa", "aaa", "bbb", "bbb", "bb", "ccc"}, }, + "full sentence exceeding width": { + lines: []string{"hello bob, I like yogurt in the mornings."}, + width: 12, + want: []string{"hello bob, I", "like yogurt", "in the", "mornings."}, + }, } for name, tt := range tests { - tt := tt t.Run(name, func(t *testing.T) { - t.Parallel() got := wrap(tt.lines, tt.width) if len(got) != len(tt.want) { - t.Errorf("expected len is %d but got %d", len(tt.want), len(got)) + t.Fatalf("expected len is %d but got %d", len(tt.want), len(got)) } for i := range tt.want { if tt.want[i] != got[i] { - t.Errorf("expected %s but got %s", tt.want[i], got[i]) + t.Fatalf("expected %s but got %s", tt.want[i], got[i]) } } }) From 6ba0f57d3ed1da0f3bcecccc83d0e20815753173 Mon Sep 17 00:00:00 2001 From: bashbunni Date: Sat, 7 Sep 2024 20:42:47 -0700 Subject: [PATCH 10/11] fix(viewport): do not allow YOffset to exceed its max --- viewport/viewport.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/viewport/viewport.go b/viewport/viewport.go index 539457ccb..05e93ae19 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -298,6 +298,10 @@ func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { + case tea.WindowSizeMsg: + if m.PastBottom() { + m.SetYOffset(m.maxYOffset()) + } case tea.KeyMsg: switch { case key.Matches(msg, m.KeyMap.PageDown): From d8159865dcb527b829da87c916895767c936c5ea Mon Sep 17 00:00:00 2001 From: kyu08 <49891479+kyu08@users.noreply.github.com> Date: Sat, 12 Oct 2024 13:23:17 +0900 Subject: [PATCH 11/11] fix(viewport): remove `strings.TrimSpace` --- viewport/viewport.go | 2 +- viewport/viewport_test.go | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/viewport/viewport.go b/viewport/viewport.go index 05e93ae19..332134b05 100644 --- a/viewport/viewport.go +++ b/viewport/viewport.go @@ -423,7 +423,7 @@ func wrap(lines []string, width int) []string { // wrap lines (handles lines that could not be word wrapped) wrap := ansi.Hardwrap(wrapWords, width, true) // split string by new lines - wrapLines := strings.Split(strings.TrimSpace(wrap), "\n") + wrapLines := strings.Split(wrap, "\n") out = append(out, wrapLines...) } diff --git a/viewport/viewport_test.go b/viewport/viewport_test.go index 0dc797462..636163719 100644 --- a/viewport/viewport_test.go +++ b/viewport/viewport_test.go @@ -31,6 +31,11 @@ func TestWrap(t *testing.T) { width: 12, want: []string{"hello bob, I", "like yogurt", "in the", "mornings."}, }, + "whitespace of head of line is preserved": { + lines: []string{" aaa", "bbb", "ccc"}, + width: 5, + want: []string{" aaa", "bbb", "ccc"}, + }, } for name, tt := range tests { @@ -42,7 +47,7 @@ func TestWrap(t *testing.T) { } for i := range tt.want { if tt.want[i] != got[i] { - t.Fatalf("expected %s but got %s", tt.want[i], got[i]) + t.Fatalf("expected %q but got %q", tt.want[i], got[i]) } } })