From 5c86164f22a4939a871810685cd161a2561deebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Fri, 3 Aug 2018 17:29:11 +0200 Subject: [PATCH] util: add a text wrapping function --- termui/error_popup.go | 26 +---------- util/text.go | 100 ++++++++++++++++++++++++++++++++++++++++++ util/text_test.go | 89 +++++++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 24 deletions(-) create mode 100644 util/text.go create mode 100644 util/text_test.go diff --git a/termui/error_popup.go b/termui/error_popup.go index e46a00e78524f5d2439b2d3a6f6ca555aabfc997..855bd05d5bbc091e07c3d4c21725892a9b65afa1 100644 --- a/termui/error_popup.go +++ b/termui/error_popup.go @@ -2,8 +2,8 @@ package termui import ( "fmt" + "github.com/MichaelMure/git-bug/util" "github.com/jroimartin/gocui" - "strings" ) const errorPopupView = "errorPopupView" @@ -37,7 +37,7 @@ func (ep *errorPopup) layout(g *gocui.Gui) error { maxX, maxY := g.Size() width := minInt(30, maxX) - wrapped, nblines := word_wrap(ep.message, width-2) + wrapped, nblines := util.WordWrap(ep.message, width-2) height := minInt(nblines+2, maxY) x0 := (maxX - width) / 2 y0 := (maxY - height) / 2 @@ -69,25 +69,3 @@ func (ep *errorPopup) close(g *gocui.Gui, v *gocui.View) error { func (ep *errorPopup) activate(message string) { ep.message = message } - -func word_wrap(text string, lineWidth int) (string, int) { - words := strings.Fields(strings.TrimSpace(text)) - if len(words) == 0 { - return text, 1 - } - lines := 1 - wrapped := words[0] - spaceLeft := lineWidth - len(wrapped) - for _, word := range words[1:] { - if len(word)+1 > spaceLeft { - wrapped += "\n" + word - spaceLeft = lineWidth - len(word) - lines++ - } else { - wrapped += " " + word - spaceLeft -= 1 + len(word) - } - } - - return wrapped, lines -} diff --git a/util/text.go b/util/text.go new file mode 100644 index 0000000000000000000000000000000000000000..83a101d36fe46a56bbf9c5e4366e88ff33c34cfa --- /dev/null +++ b/util/text.go @@ -0,0 +1,100 @@ +package util + +import ( + "bytes" + "strings" +) + +func WordWrap(text string, lineWidth int) (string, int) { + words := strings.Fields(strings.TrimSpace(text)) + if len(words) == 0 { + return "", 1 + } + lines := 1 + wrapped := words[0] + spaceLeft := lineWidth - len(wrapped) + for _, word := range words[1:] { + if len(word)+1 > spaceLeft { + wrapped += "\n" + word + spaceLeft = lineWidth - len(word) + lines++ + } else { + wrapped += " " + word + spaceLeft -= 1 + len(word) + } + } + + return wrapped, lines +} + +func TextWrap(text string, lineWidth int) (string, int) { + var textBuffer bytes.Buffer + var lineBuffer bytes.Buffer + nbLine := 1 + firstLine := true + + // tabs are formatted as 4 spaces + text = strings.Replace(text, "\t", " ", 4) + + for _, line := range strings.Split(text, "\n") { + spaceLeft := lineWidth + + if !firstLine { + textBuffer.WriteString("\n") + nbLine++ + } + + firstWord := true + + for _, word := range strings.Split(line, " ") { + if spaceLeft > len(word) { + if !firstWord { + lineBuffer.WriteString(" ") + spaceLeft -= 1 + } + lineBuffer.WriteString(word) + spaceLeft -= len(word) + firstWord = false + } else { + if len(word) > lineWidth { + for len(word) > 0 { + l := minInt(spaceLeft, len(word)) + part := word[:l] + word = word[l:] + + lineBuffer.WriteString(part) + textBuffer.Write(lineBuffer.Bytes()) + lineBuffer.Reset() + + if len(word) > 0 { + textBuffer.WriteString("\n") + nbLine++ + } + + spaceLeft = lineWidth + } + } else { + textBuffer.WriteString(strings.TrimRight(lineBuffer.String(), " ")) + textBuffer.WriteString("\n") + lineBuffer.Reset() + lineBuffer.WriteString(word) + firstWord = false + spaceLeft = lineWidth - len(word) + nbLine++ + } + } + } + textBuffer.WriteString(strings.TrimRight(lineBuffer.String(), " ")) + lineBuffer.Reset() + firstLine = false + } + + return textBuffer.String(), nbLine +} + +func minInt(a, b int) int { + if a > b { + return b + } + return a +} diff --git a/util/text_test.go b/util/text_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f7e1fabfe527fe14034c0161b02dc00b627ddc16 --- /dev/null +++ b/util/text_test.go @@ -0,0 +1,89 @@ +package util + +import ( + "strings" + "testing" +) + +func TestTextWrap(t *testing.T) { + cases := []struct { + Input, Output string + Lim int + }{ + // A simple word passes through. + { + "foo", + "foo", + 4, + }, + // Word breaking + { + "foobarbaz", + "foob\narba\nz", + 4, + }, + // Lines are broken at whitespace. + { + "foo bar baz", + "foo\nbar\nbaz", + 4, + }, + // Word breaking + { + "foo bars bazzes", + "foo\nbars\nbazz\nes", + 4, + }, + // A word that would run beyond the width is wrapped. + { + "fo sop", + "fo\nsop", + 4, + }, + // A tab counts as 4 characters. + { + "foo\nb\t r\n baz", + "foo\nb\n r\n baz", + 4, + }, + // Trailing whitespace is removed after used for wrapping. + // Runs of whitespace on which a line is broken are removed. + { + "foo \nb ar ", + "foo\n\nb\nar\n", + 4, + }, + // An explicit line break at the end of the input is preserved. + { + "foo bar baz\n", + "foo\nbar\nbaz\n", + 4, + }, + // Explicit break are always preserved. + { + "\nfoo bar\n\n\nbaz\n", + "\nfoo\nbar\n\n\nbaz\n", + 4, + }, + // Complete example: + { + " This is a list: \n\n\t* foo\n\t* bar\n\n\n\t* baz \nBAM ", + " This\nis a\nlist:\n\n *\nfoo\n *\nbar\n\n\n *\nbaz\nBAM\n", + 6, + }, + } + + for i, tc := range cases { + actual, lines := TextWrap(tc.Input, tc.Lim) + if actual != tc.Output { + t.Fatalf("Case %d Input:\n\n`%s`\n\nExpected Output:\n\n`%s`\n\nActual Output:\n\n`%s`", + i, tc.Input, tc.Output, actual) + } + + expected := len(strings.Split(tc.Output, "\n")) + if expected != lines { + t.Fatalf("Nb lines mismatch\nExpected:%d\nActual:%d", + expected, lines) + } + } +}