From 14ad5503c6d562743ee3817d09a44dec18d9f920 Mon Sep 17 00:00:00 2001 From: Jon Anderson Date: Sat, 4 Nov 2023 19:20:16 -0700 Subject: [PATCH] Wrap (#190) * Added ability to wrap absolutely. * Added wraping on spaces. * Added more tests. * Organized tests. * Fixed comment typo. * Cleaned up escape support. Tests. * Fixed issue where wrap didn't push words lower. * Minor clean up. * Go mod tidy. * Fixed results tests. * Removed unnecessary base case. --- go.mod | 4 +- internal/app/fullscreen/results/results.go | 20 ++-- .../app/fullscreen/results/results_test.go | 10 +- internal/app/fullscreen/results/wrap/wrap.go | 103 ++++++++++++++++++ .../app/fullscreen/results/wrap/wrap_test.go | 52 +++++++++ 5 files changed, 170 insertions(+), 19 deletions(-) create mode 100644 internal/app/fullscreen/results/wrap/wrap.go create mode 100644 internal/app/fullscreen/results/wrap/wrap_test.go diff --git a/go.mod b/go.mod index 928d0e59..4df58ab3 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,11 @@ go 1.19 require ( github.com/apple/foundationdb/bindings/go v0.0.0-20210510203748-af616f980733 + github.com/brianvoe/gofakeit/v6 v6.23.1 github.com/charmbracelet/bubbles v0.15.0 github.com/charmbracelet/bubbletea v0.24.0 github.com/charmbracelet/lipgloss v0.7.1 + github.com/mattn/go-runewidth v0.0.14 github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.21.0 github.com/spf13/cobra v1.3.0 @@ -16,14 +18,12 @@ require ( require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/brianvoe/gofakeit/v6 v6.23.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect diff --git a/internal/app/fullscreen/results/results.go b/internal/app/fullscreen/results/results.go index e7bef59c..25062acd 100644 --- a/internal/app/fullscreen/results/results.go +++ b/internal/app/fullscreen/results/results.go @@ -3,13 +3,12 @@ package results import ( "container/list" "fmt" - "strings" - "github.com/apple/foundationdb/bindings/go/src/fdb/directory" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" - "github.com/muesli/reflow/wrap" + "github.com/janderland/fdbq/internal/app/fullscreen/results/wrap" "github.com/rs/zerolog" + "strings" "github.com/janderland/fdbq/engine/stream" "github.com/janderland/fdbq/keyval" @@ -272,10 +271,7 @@ func (x *Model) render(e *list.Element) []string { res := e.Value.(result) prefix := fmt.Sprintf("%d ", res.i) indent := strings.Repeat(" ", len(prefix)) - - str := x.str(res.value) - str = wrap.String(str, x.wrapWidth-len(prefix)) - lines := strings.Split(str, "\n") + lines := wrap.Wrap(x.str(res.value), x.wrapWidth-len(prefix)) // If spaced is enabled, add an extra blank // line after each item except the newest. @@ -409,12 +405,12 @@ func (x *Model) scrollUpItems(n int) bool { break } newCursor := x.cursor.Next() - // TODO: Do we need this check? - // Won't endCursor always be properly - // set, so we should never encounter - // this case? + // This check is for detecting bugs. + // endCursor should always be set, + // so we should never encounter + // this case. if newCursor == nil { - log.Log().Int("i", i).Msg("up items unreachable?") + log.Error().Int("i", i).Msg("up items unreachable?") break } x.cursor = newCursor diff --git a/internal/app/fullscreen/results/results_test.go b/internal/app/fullscreen/results/results_test.go index bff12dcf..b60bb7b0 100644 --- a/internal/app/fullscreen/results/results_test.go +++ b/internal/app/fullscreen/results/results_test.go @@ -46,10 +46,10 @@ func TestWrapWidth(t *testing.T) { x.WrapWidth(8) expected = ` -1 # xxx +1 # xxxxx - xx xx - x`[1:] + xxxxx + xxx`[1:] require.Equal(t, expected, x.View()) } @@ -187,10 +187,10 @@ func TestLineScroll(t *testing.T) { x.WrapWidth(10) x.Push("xxx xxx") - require.Equal(t, "1 # xxx x\n xx", x.View()) + require.Equal(t, "1 # xxx \n xxx", x.View()) x.scrollUpLines(1) - require.Equal(t, "1 # xxx x\n xx", x.View()) + require.Equal(t, "1 # xxx \n xxx", x.View()) } func setup() Model { diff --git a/internal/app/fullscreen/results/wrap/wrap.go b/internal/app/fullscreen/results/wrap/wrap.go new file mode 100644 index 00000000..7777298f --- /dev/null +++ b/internal/app/fullscreen/results/wrap/wrap.go @@ -0,0 +1,103 @@ +package wrap + +import ( + rw "github.com/mattn/go-runewidth" + "strings" + "unicode" +) + +func Wrap(str string, limit int) []string { + if limit <= 0 { + return strings.Split(str, "\n") + } + + var ( + word builder + line builder + lines []string + ansiCode = false + ) + + for _, c := range str { + switch { + case c == 0x1b: + word.WriteCode(c) + ansiCode = true + + case ansiCode: + word.WriteCode(c) + if (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') { + ansiCode = false + } + + case unicode.IsSpace(c): + line.WriteString(word.String()) + word.Reset() + + if line.Width() < limit { + line.WriteRune(' ') + continue + } + + lines = append(lines, line.String()) + line.Reset() + + default: + if word.Width() == 0 { + word.WriteRune(c) + continue + } + + if line.Width()+word.Width()+rw.RuneWidth(c) > limit { + if line.Width() == 0 { + line.WriteString(word.String()) + word.Reset() + } + + lines = append(lines, line.String()) + line.Reset() + } + + word.WriteRune(c) + } + } + + line.WriteString(word.String()) + lines = append(lines, line.String()) + + return lines +} + +type builder struct { + builder strings.Builder + width int +} + +func (x *builder) Width() int { + return x.width +} + +func (x *builder) Reset() { + x.width = 0 + x.builder.Reset() +} + +func (x *builder) String() string { + return x.builder.String() +} + +func (x *builder) WriteCode(r rune) { + _, _ = x.builder.WriteRune(r) +} + +func (x *builder) WriteRune(r rune) { + x.width += rw.RuneWidth(r) + _, _ = x.builder.WriteRune(r) +} + +func (x *builder) WriteString(s string) { + for _, c := range s { + x.width += rw.RuneWidth(c) + } + _, _ = x.builder.WriteString(s) +} diff --git a/internal/app/fullscreen/results/wrap/wrap_test.go b/internal/app/fullscreen/results/wrap/wrap_test.go new file mode 100644 index 00000000..85e8ae78 --- /dev/null +++ b/internal/app/fullscreen/results/wrap/wrap_test.go @@ -0,0 +1,52 @@ +package wrap + +import ( + "github.com/stretchr/testify/require" + "testing" +) + +func TestWrap(t *testing.T) { + tests := []struct { + name string + limit int + input string + output []string + }{ + { + "break word", + 4, + "foobar", + []string{"foob", "ar"}, + }, + { + "wrap between two words", + 3, + "foo bar", + []string{"foo", "bar"}, + }, + { + "wrap between many words", + 7, + "foo bar bing baz", + []string{"foo bar", "bing ", "baz"}, + }, + { + "break and wrap words", + 4, + "funder i knew go there", + []string{"fund", "er i", "knew", "go ", "ther", "e"}, + }, + { + "ignore ascii escape codes", + 4, + "ba\x1b[1;31mll", + []string{"ba\x1b[1;31mll"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.output, Wrap(tt.input, tt.limit)) + }) + } +}