package utils import ( "bufio" "strings" "testing" "github.com/jesseduffield/gocui" "github.com/stretchr/testify/assert" ) // TestSplitLines is a function. func TestSplitLines(t *testing.T) { type scenario struct { multilineString string expected []string } scenarios := []scenario{ { "", []string{}, }, { "\n", []string{}, }, { "hello world !\nhello universe !\n", []string{ "hello world !", "hello universe !", }, }, } for _, s := range scenarios { assert.EqualValues(t, s.expected, SplitLines(s.multilineString)) } } func TestSplitNul(t *testing.T) { type scenario struct { multilineString string expected []string } scenarios := []scenario{ { "", []string{}, }, { "\x00", []string{ "", }, }, { "hello world !\x00hello universe !\x00", []string{ "hello world !", "hello universe !", }, }, } for _, s := range scenarios { assert.EqualValues(t, s.expected, SplitNul(s.multilineString)) } } // TestNormalizeLinefeeds is a function. func TestNormalizeLinefeeds(t *testing.T) { type scenario struct { byteArray []byte expected []byte } scenarios := []scenario{ { // \r\n []byte{97, 115, 100, 102, 13, 10}, []byte{97, 115, 100, 102, 10}, }, { // bash\r\nblah []byte{97, 115, 100, 102, 13, 10, 97, 115, 100, 102}, []byte{97, 115, 100, 102, 10, 97, 115, 100, 102}, }, { // \r []byte{97, 115, 100, 102, 13}, []byte{97, 115, 100, 102}, }, { // \n []byte{97, 115, 100, 102, 10}, []byte{97, 115, 100, 102, 10}, }, } for _, s := range scenarios { assert.EqualValues(t, string(s.expected), NormalizeLinefeeds(string(s.byteArray))) } } func TestScanLinesAndTruncateWhenLongerThanBuffer(t *testing.T) { type scenario struct { input string expectedLines []string } scenarios := []scenario{ { "", []string{}, }, { "\n", []string{""}, }, { "abc", []string{"abc"}, }, { "abc\ndef", []string{"abc", "def"}, }, { "abc\n\ndef", []string{"abc", "", "def"}, }, { "abc\r\ndef\r", []string{"abc", "def"}, }, { "abcdef", []string{"abcde"}, }, { "abcdef\n", []string{"abcde"}, }, { "abcdef\nghijkl\nx", []string{"abcde", "ghijk", "x"}, }, { "abc\ndefghijklmnopqrstuvw\nx", []string{"abc", "defgh", "x"}, }, } for _, s := range scenarios { scanner := bufio.NewScanner(strings.NewReader(s.input)) scanner.Buffer(make([]byte, 5), 5) scanner.Split(ScanLinesAndTruncateWhenLongerThanBuffer(5)) result := []string{} for scanner.Scan() { result = append(result, scanner.Text()) } assert.NoError(t, scanner.Err()) assert.EqualValues(t, s.expectedLines, result) } } func TestWrapViewLinesToWidth(t *testing.T) { tests := []struct { name string wrap bool editable bool text string width int tabWidth int expectedWrappedLines []string expectedWrappedLinesIndices []int expectedOriginalLinesIndices []int }{ { name: "Wrap off", wrap: false, text: "1st line\n2nd line\n3rd line", width: 5, expectedWrappedLines: []string{ "1st line", "2nd line", "3rd line", }, expectedWrappedLinesIndices: []int{0, 1, 2}, expectedOriginalLinesIndices: []int{0, 1, 2}, }, { name: "Wrap on space", wrap: true, text: "Hello World", width: 5, expectedWrappedLines: []string{ "Hello", "World", }, expectedWrappedLinesIndices: []int{0}, expectedOriginalLinesIndices: []int{0, 0}, }, { 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", }, }, { name: "Tabs, width 4", wrap: true, text: "\ta\tbb\tccc\tdddd\teeeee", width: 50, tabWidth: 4, expectedWrappedLines: []string{ " a bb ccc dddd eeeee", }, }, { name: "Tabs, width 8", wrap: true, text: "\ta\tbb\tccc\tdddddddd\teeeee", width: 100, tabWidth: 8, expectedWrappedLines: []string{ " a bb ccc dddddddd eeeee", }, }, { name: "Multiple lines", wrap: true, text: "First paragraph\nThe second paragraph is a bit longer.\nThird paragraph\n", width: 10, expectedWrappedLines: []string{ "First", "paragraph", "The second", "paragraph", "is a bit", "longer.", "Third", "paragraph", }, expectedWrappedLinesIndices: []int{0, 2, 6}, expectedOriginalLinesIndices: []int{0, 0, 1, 1, 1, 1, 2, 2}, }, { name: "Avoid blank line at end if not editable", wrap: true, editable: false, text: "First\nSecond\nThird\n", width: 10, expectedWrappedLines: []string{ "First", "Second", "Third", }, expectedWrappedLinesIndices: []int{0, 1, 2}, expectedOriginalLinesIndices: []int{0, 1, 2}, }, { name: "Avoid blank line at end if not editable", wrap: true, editable: false, text: "First\nSecond\nThird\n", width: 10, expectedWrappedLines: []string{ "First", "Second", "Third", }, expectedWrappedLinesIndices: []int{0, 1, 2}, expectedOriginalLinesIndices: []int{0, 1, 2}, }, { name: "Keep blank line at end if editable", wrap: true, editable: true, text: "First\nSecond\nThird\n", width: 10, expectedWrappedLines: []string{ "First", "Second", "Third", "", }, expectedWrappedLinesIndices: []int{0, 1, 2, 3}, expectedOriginalLinesIndices: []int{0, 1, 2, 3}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tabWidth := tt.tabWidth if tabWidth == 0 { tabWidth = 4 } wrappedLines, wrappedLinesIndices, originalLinesIndices := WrapViewLinesToWidth(tt.wrap, tt.editable, tt.text, tt.width, tabWidth) assert.Equal(t, tt.expectedWrappedLines, wrappedLines) if tt.expectedWrappedLinesIndices != nil { assert.Equal(t, tt.expectedWrappedLinesIndices, wrappedLinesIndices) } if tt.expectedOriginalLinesIndices != nil { assert.Equal(t, tt.expectedOriginalLinesIndices, originalLinesIndices) } // 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) view.TabWidth = tabWidth assert.Equal(t, tt.width, view.InnerWidth()) view.Wrap = tt.wrap view.Editable = tt.editable view.SetContent(tt.text) assert.Equal(t, wrappedLines, view.ViewBufferLines()) }) } }