Merge branch 'tvopt' into 'master'

Store TextView buffer as [][]byte instead of []string

Closes #36

See merge request tslocum/cview!7
This commit is contained in:
Trevor Slocum 2020-10-04 19:50:46 +00:00
commit b5fba43a01
2 changed files with 113 additions and 52 deletions

View File

@ -3,7 +3,6 @@ package cview
import (
"bytes"
"regexp"
"strings"
"sync"
"unicode/utf8"
@ -26,7 +25,7 @@ var (
// view.
type textViewIndex struct {
Line int // The index into the "buffer" variable.
Pos int // The index into the "buffer" string (byte position).
Pos int // The index into the "buffer" line ([]byte position).
NextPos int // The (byte) index of the next character in this buffer line.
Width int // The screen width of this line.
ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset).
@ -100,7 +99,7 @@ type TextView struct {
*Box
// The text buffer.
buffer []string
buffer [][]byte
// The last bytes that have been received but are not part of the buffer yet.
recentBytes []byte
@ -317,38 +316,33 @@ func (t *TextView) SetText(text string) *TextView {
// GetText returns the current text of this text view. If "stripTags" is set
// to true, any region/color tags are stripped from the text.
func (t *TextView) GetText(stripTags bool) string {
func (t *TextView) GetText(stripTags bool) []byte {
t.RLock()
defer t.RUnlock()
// Get the buffer.
buffer := t.buffer
if !stripTags {
buffer = append(buffer, string(t.recentBytes))
if len(t.recentBytes) > 0 {
return bytes.Join(append(t.buffer, t.recentBytes), []byte("\n"))
}
return bytes.Join(t.buffer, []byte("\n"))
}
// Add newlines again.
text := strings.Join(buffer, "\n")
// Strip from tags if required.
if stripTags {
if t.regions {
text = regionPattern.ReplaceAllString(text, "")
}
if t.dynamicColors {
text = colorPattern.ReplaceAllStringFunc(text, func(match string) string {
if len(match) > 2 {
return ""
}
return match
})
}
if t.regions || t.dynamicColors {
text = escapePattern.ReplaceAllString(text, `[$1$2]`)
}
buffer := bytes.Join(t.buffer, []byte("\n"))
if t.regions {
buffer = regionPattern.ReplaceAll(buffer, nil)
}
return text
if t.dynamicColors {
buffer = colorPattern.ReplaceAllFunc(buffer, func(match []byte) []byte {
if len(match) > 2 {
return nil
}
return match
})
}
if t.regions || t.dynamicColors {
buffer = escapePattern.ReplaceAll(buffer, []byte(`[$1$2]`))
}
return buffer
}
// SetDynamicColors sets the flag that allows the text color to be changed
@ -636,7 +630,7 @@ func (t *TextView) GetRegionText(regionID string) string {
t.RLock()
defer t.RUnlock()
if !t.regions || regionID == "" {
if !t.regions || len(regionID) == 0 {
return ""
}
@ -649,17 +643,17 @@ func (t *TextView) GetRegionText(regionID string) string {
// Find all color tags in this line.
var colorTagIndices [][]int
if t.dynamicColors {
colorTagIndices = colorPattern.FindAllStringIndex(str, -1)
colorTagIndices = colorPattern.FindAllIndex(str, -1)
}
// Find all regions in this line.
var (
regionIndices [][]int
regions [][]string
regions [][][]byte
)
if t.regions {
regionIndices = regionPattern.FindAllStringIndex(str, -1)
regions = regionPattern.FindAllStringSubmatch(str, -1)
regionIndices = regionPattern.FindAllIndex(str, -1)
regions = regionPattern.FindAllSubmatch(str, -1)
}
// Analyze this line.
@ -682,7 +676,7 @@ func (t *TextView) GetRegionText(regionID string) string {
// This is the end of the requested region. We're done.
return buffer.String()
}
currentRegionID = regions[currentRegion][1]
currentRegionID = string(regions[currentRegion][1])
currentRegion++
}
continue
@ -690,7 +684,7 @@ func (t *TextView) GetRegionText(regionID string) string {
// Add this rune.
if currentRegionID == regionID {
buffer.WriteRune(ch)
buffer.WriteByte(ch)
}
}
@ -768,12 +762,12 @@ func (t *TextView) write(p []byte) (n int, err error) {
// Transform the new bytes into strings.
newBytes = bytes.Replace(newBytes, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1)
for index, line := range newLineRegex.Split(string(newBytes), -1) {
for index, line := range bytes.Split(newBytes, []byte("\n")) {
if index == 0 {
if len(t.buffer) == 0 {
t.buffer = []string{line}
t.buffer = [][]byte{line}
} else {
t.buffer[len(t.buffer)-1] += line
t.buffer[len(t.buffer)-1] = append(t.buffer[len(t.buffer)-1], line...)
}
} else {
t.buffer = append(t.buffer, line)
@ -831,7 +825,8 @@ func (t *TextView) reindexBuffer(width int) {
)
// Go through each line in the buffer.
for bufferIndex, str := range t.buffer {
for bufferIndex, buf := range t.buffer {
str := string(buf)
colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedStr, _ := decomposeString(str, t.dynamicColors, t.regions)
// Split the line if required.
@ -963,11 +958,11 @@ func (t *TextView) reindexBuffer(width int) {
if t.wrap && t.wordWrap {
for _, line := range t.index {
str := t.buffer[line.Line][line.Pos:line.NextPos]
spaces := spacePattern.FindAllStringIndex(str, -1)
spaces := spacePattern.FindAllIndex(str, -1)
if spaces != nil && spaces[len(spaces)-1][1] == len(str) {
oldNextPos := line.NextPos
line.NextPos -= spaces[len(spaces)-1][1] - spaces[len(spaces)-1][0]
line.Width -= stringWidth(t.buffer[line.Line][line.NextPos:oldNextPos])
line.Width -= stringWidth(string(t.buffer[line.Line][line.NextPos:oldNextPos]))
}
}
}
@ -1109,7 +1104,7 @@ func (t *TextView) Draw(screen tcell.Screen) {
}
// Process tags.
colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(text, t.dynamicColors, t.regions)
colorTagIndices, colorTags, regionIndices, regions, escapeIndices, strippedText, _ := decomposeString(string(text), t.dynamicColors, t.regions)
// Calculate the position of the line.
var skip, posX int

View File

@ -3,7 +3,6 @@ package cview
import (
"bytes"
"fmt"
"strings"
"testing"
)
@ -68,9 +67,6 @@ func TestTextViewWrite(t *testing.T) {
}
contents := tv.GetText(false)
if len(contents) > 0 {
contents = contents[0 : len(contents)-1] // Remove extra newline
}
if len(contents) != len(expectedData) {
t.Errorf("failed to write: incorrect contents: expected %d bytes, got %d", len(contents), len(expectedData))
} else if !bytes.Equal([]byte(contents), expectedData) {
@ -155,6 +151,76 @@ func BenchmarkTextViewIndex(b *testing.B) {
}
}
func TestTextViewGetText(t *testing.T) {
t.Parallel()
tv := NewTextView()
tv.SetDynamicColors(true)
tv.SetRegions(true)
n, err := tv.Write(randomData)
if err != nil {
t.Errorf("failed to write (successfully wrote %d) bytes: %s", n, err)
} else if n != randomDataSize {
t.Errorf("failed to write: expected to write %d bytes, wrote %d", randomDataSize, n)
}
suffix := []byte(`["start"]outer[b]inner[-]outer[""]`)
suffixStripped := []byte("outerinnerouter")
n, err = tv.Write(suffix)
if err != nil {
t.Errorf("failed to write (successfully wrote %d) bytes: %s", n, err)
}
if !bytes.Equal(tv.GetText(false), append(randomData, suffix...)) {
t.Error("failed to get un-stripped text: unexpected suffix")
}
if !bytes.Equal(tv.GetText(true), append(randomData, suffixStripped...)) {
t.Error("failed to get text stripped text: unexpected suffix")
}
}
func BenchmarkTextViewGetText(b *testing.B) {
for _, c := range textViewTestCases {
c := c // Capture
if c.app {
continue // Skip for this benchmark
}
b.Run(c.String(), func(b *testing.B) {
var (
tv = tvc(NewTextView(), c)
n int
err error
v []byte
)
_, err = prepareAppendTextView(tv)
if err != nil {
b.Errorf("failed to prepare append TextView: %s", err)
}
n, err = tv.Write(randomData)
if err != nil {
b.Errorf("failed to write (successfully wrote %d) bytes: %s", n, err)
} else if n != randomDataSize {
b.Errorf("failed to write: expected to write %d bytes, wrote %d", randomDataSize, n)
}
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
v = tv.GetText(true)
_ = v
}
})
}
}
func TestTextViewDraw(t *testing.T) {
t.Parallel()
@ -242,7 +308,7 @@ func TestTextViewMaxLines(t *testing.T) {
}
// retrieve the total text and see we have the 100 lines:
count := strings.Count(tv.GetText(true), "\n")
count := bytes.Count(tv.GetText(true), []byte("\n"))
if count != 100 {
t.Errorf("expected 100 lines, got %d", count)
}
@ -250,7 +316,7 @@ func TestTextViewMaxLines(t *testing.T) {
// now set the maximum lines to 20, this should clip the buffer:
tv.SetMaxLines(20)
// verify buffer was clipped:
count = len(strings.Split(tv.GetText(true), "\n"))
count = len(bytes.Split(tv.GetText(true), []byte("\n")))
if count != 20 {
t.Errorf("expected 20 lines, got %d", count)
}
@ -265,14 +331,14 @@ func TestTextViewMaxLines(t *testing.T) {
// Sice max lines is set to 20, we should still get 20 lines:
txt := tv.GetText(true)
lines := strings.Split(txt, "\n")
lines := bytes.Split(txt, []byte("\n"))
count = len(lines)
if count != 20 {
t.Errorf("expected 20 lines, got %d", count)
}
// and those 20 lines should be the last ones:
if lines[0] != "L181" {
if !bytes.Equal(lines[0], []byte("L181")) {
t.Errorf("expected to get L181, got %s", lines[0])
}
@ -335,9 +401,9 @@ func tvc(tv *TextView, c *textViewTestCase) *TextView {
func cl(v bool) rune {
if v {
return 'T'
return 'Y'
}
return 'F'
return 'N'
}
func prepareAppendTextView(t *TextView) ([]byte, error) {