Johan Walles
d5827bbc99
Parse lines on demand and only once
...
This improves line processing performance by 40%.
Fixes #36 .
diff --git m/ansiTokenizer.go m/ansiTokenizer.go
index d991e23..056a227 100644
--- m/ansiTokenizer.go
+++ m/ansiTokenizer.go
@@ -23,6 +23,44 @@ type Token struct {
Style tcell.Style
}
+// A Line represents a line of text that can / will be paged
+type Line struct {
+ raw *string
+ plain *string
+ tokens []Token
+}
+
+// NewLine creates a new Line from a (potentially ANSI / man page formatted) string
+func NewLine(raw string) *Line {
+ return &Line{
+ raw: &raw,
+ plain: nil,
+ tokens: nil,
+ }
+}
+
+// Tokens returns a representation of the string split into styled tokens
+func (line *Line) Tokens() []Token {
+ line.parse()
+ return line.tokens
+}
+
+// Plain returns a plain text representation of the initial string
+func (line *Line) Plain() string {
+ line.parse()
+ return *line.plain
+}
+
+func (line *Line) parse() {
+ if line.raw == nil {
+ // Already done
+ return
+ }
+
+ line.tokens, line.plain = tokensFromString(*line.raw)
+ line.raw = nil
+}
+
// SetManPageFormatFromEnv parses LESS_TERMCAP_xx environment variables and
// adapts the moar output accordingly.
func SetManPageFormatFromEnv() {
diff --git m/pager.go m/pager.go
index 412e05b..98efa9a 100644
--- m/pager.go
+++ m/pager.go
@@ -111,7 +111,7 @@ func NewPager(r *Reader) *Pager {
}
}
-func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line string) {
+func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNumber int, line *Line) {
screenWidth, _ := p.screen.Size()
prefixLength := 0
@@ -138,7 +138,7 @@ func (p *Pager) _AddLine(fileLineNumber *int, maxPrefixLength int, screenLineNum
func createScreenLine(
stringIndexAtColumnZero int,
screenColumnsCount int,
- line string,
+ line *Line,
search *regexp.Regexp,
) []Token {
var returnMe []Token
@@ -152,14 +152,14 @@ func createScreenLine(
searchHitDelta = -1
}
- tokens, plainString := tokensFromString(line)
- if stringIndexAtColumnZero >= len(tokens) {
+ if stringIndexAtColumnZero >= len(line.Tokens()) {
// Nothing (more) to display, never mind
return returnMe
}
- matchRanges := getMatchRanges(plainString, search)
- for _, token := range tokens[stringIndexAtColumnZero:] {
+ plain := line.Plain()
+ matchRanges := getMatchRanges(&plain, search)
+ for _, token := range line.Tokens()[stringIndexAtColumnZero:] {
if len(returnMe) >= screenColumnsCount {
// We are trying to add a character to the right of the screen.
// Indicate that this line continues to the right.
@@ -232,7 +232,8 @@ func (p *Pager) _AddLines(spinner string) {
// This happens when we're done
eofSpinner = "---"
}
- p._AddLine(nil, 0, screenLineNumber, _EofMarkerFormat+eofSpinner)
+ spinnerLine := NewLine(_EofMarkerFormat + eofSpinner)
+ p._AddLine(nil, 0, screenLineNumber, spinnerLine)
switch p.mode {
case _Searching:
@@ -329,8 +330,8 @@ func (p *Pager) _FindFirstHitLineOneBased(firstLineOneBased int, backwards bool)
return nil
}
- _, lineText := tokensFromString(*line)
- if p.searchPattern.MatchString(*lineText) {
+ lineText := line.Plain()
+ if p.searchPattern.MatchString(lineText) {
return &lineNumber
}
diff --git m/pager_test.go m/pager_test.go
index 65fa3c2..ce0f79b 100644
--- m/pager_test.go
+++ m/pager_test.go
@@ -265,13 +265,15 @@ func assertTokenRangesEqual(t *testing.T, actual []Token, expected []Token) {
}
func TestCreateScreenLineBase(t *testing.T) {
- line := createScreenLine(0, 3, "", nil)
- assert.Assert(t, len(line) == 0)
+ line := NewLine("")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assert.Assert(t, len(screenLine) == 0)
}
func TestCreateScreenLineOverflowRight(t *testing.T) {
- line := createScreenLine(0, 3, "012345", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012345")
+ screenLine := createScreenLine(0, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('0', tcell.StyleDefault),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('>', tcell.StyleDefault.Reverse(true)),
@@ -279,8 +281,9 @@ func TestCreateScreenLineOverflowRight(t *testing.T) {
}
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
- line := createScreenLine(1, 3, "012", nil)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("012")
+ screenLine := createScreenLine(1, 3, line, nil)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('1', tcell.StyleDefault),
createExpectedCell('2', tcell.StyleDefault),
@@ -293,8 +296,9 @@ func TestCreateScreenLineSearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "abc", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("abc")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('a', tcell.StyleDefault),
createExpectedCell('b', tcell.StyleDefault.Reverse(true)),
createExpectedCell('c', tcell.StyleDefault),
@@ -307,8 +311,9 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
panic(err)
}
- line := createScreenLine(0, 3, "åäö", pattern)
- assertTokenRangesEqual(t, line, []Token{
+ line := NewLine("åäö")
+ screenLine := createScreenLine(0, 3, line, pattern)
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
createExpectedCell('ö', tcell.StyleDefault),
@@ -318,9 +323,10 @@ func TestCreateScreenLineUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(1, 4, "ååäö", pattern)
+ line := NewLine("ååäö")
+ screenLine := createScreenLine(1, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
@@ -331,9 +337,10 @@ func TestCreateScreenLineScrolledUtf8SearchHit(t *testing.T) {
func TestCreateScreenLineScrolled2Utf8SearchHit(t *testing.T) {
pattern := regexp.MustCompile("ä")
- line := createScreenLine(2, 4, "åååäö", pattern)
+ line := NewLine("åååäö")
+ screenLine := createScreenLine(2, 4, line, pattern)
- assertTokenRangesEqual(t, line, []Token{
+ assertTokenRangesEqual(t, screenLine, []Token{
createExpectedCell('<', tcell.StyleDefault.Reverse(true)),
createExpectedCell('å', tcell.StyleDefault),
createExpectedCell('ä', tcell.StyleDefault.Reverse(true)),
diff --git m/reader.go m/reader.go
index 418c4c5..d47b710 100644
--- m/reader.go
+++ m/reader.go
@@ -29,7 +29,7 @@ import (
//
// This package provides query methods for the struct, no peeking!!
type Reader struct {
- lines []string
+ lines []*Line
name *string
lock *sync.Mutex
err error
@@ -41,7 +41,7 @@ type Reader struct {
// Lines contains a number of lines from the reader, plus metadata
type Lines struct {
- lines []string
+ lines []*Line
// One-based line number of the first line returned
firstLineOneBased int
@@ -136,7 +136,7 @@ func readStream(stream io.Reader, reader *Reader, fromFilter *exec.Cmd) {
}
reader.lock.Lock()
- reader.lines = append(reader.lines, string(completeLine))
+ reader.lines = append(reader.lines, NewLine(string(completeLine)))
reader.lock.Unlock()
// This is how to do a non-blocking write to a channel:
@@ -172,7 +172,7 @@ func NewReaderFromStream(name string, reader io.Reader) *Reader {
// If fromFilter is not nil this method will wait() for it,
// and effectively takes over ownership for it.
func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
- var lines []string
+ var lines []*Line
var lock = &sync.Mutex{}
done := make(chan bool, 1)
@@ -201,9 +201,11 @@ func newReaderFromStream(reader io.Reader, fromFilter *exec.Cmd) *Reader {
// Moar in the bottom left corner of the screen.
func NewReaderFromText(name string, text string) *Reader {
noExternalNewlines := strings.Trim(text, "\n")
- lines := []string{}
+ lines := []*Line{}
if len(noExternalNewlines) > 0 {
- lines = strings.Split(noExternalNewlines, "\n")
+ for _, line := range strings.Split(noExternalNewlines, "\n") {
+ lines = append(lines, NewLine(line))
+ }
}
done := make(chan bool, 1)
done <- true
@@ -380,7 +382,7 @@ func (r *Reader) GetLineCount() int {
}
// GetLine gets a line. If the requested line number is out of bounds, nil is returned.
-func (r *Reader) GetLine(lineNumberOneBased int) *string {
+func (r *Reader) GetLine(lineNumberOneBased int) *Line {
r.lock.Lock()
defer r.lock.Unlock()
@@ -390,7 +392,7 @@ func (r *Reader) GetLine(lineNumberOneBased int) *string {
if lineNumberOneBased > len(r.lines) {
return nil
}
- return &r.lines[lineNumberOneBased-1]
+ return r.lines[lineNumberOneBased-1]
}
// GetLines gets the indicated lines from the input
diff --git m/reader_test.go m/reader_test.go
index 2ba7326..0e2aed2 100644
--- m/reader_test.go
+++ m/reader_test.go
@@ -158,8 +158,8 @@ func TestGetLongLine(t *testing.T) {
assert.Equal(t, len(lines.lines), 1)
line := lines.lines[0]
- assert.Assert(t, strings.HasPrefix(line, "1 2 3 4"), "<%s>", line)
- assert.Assert(t, strings.HasSuffix(line, "0123456789"), line)
+ assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
+ assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)
stat, err := os.Stat(file)
if err != nil {
@@ -168,7 +168,7 @@ func TestGetLongLine(t *testing.T) {
fileSize := stat.Size()
// The "+1" is because the Reader strips off the ending linefeed
- assert.Equal(t, len(line)+1, int(fileSize))
+ assert.Equal(t, len(line.Plain())+1, int(fileSize))
}
func getReaderWithLineCount(totalLines int) *Reader {
@@ -219,7 +219,7 @@ func testCompressedFile(t *testing.T, filename string) {
panic(err)
}
- assert.Equal(t, reader.GetLines(1, 5).lines[0], "This is a compressed file", "%s", filename)
+ assert.Equal(t, reader.GetLines(1, 5).lines[0].Plain(), "This is a compressed file", "%s", filename)
}
func TestCompressedFiles(t *testing.T) {
Change-Id: Id8671001ec7c1038e2df0b87a45d346a1f1dd663
2021-01-11 10:42:34 +01:00