2019-06-13 21:04:51 +03:00
|
|
|
package m
|
|
|
|
|
2019-06-14 07:49:27 +03:00
|
|
|
import (
|
2019-06-16 22:54:25 +03:00
|
|
|
"math"
|
2021-05-03 20:15:43 +03:00
|
|
|
"os"
|
2019-07-15 19:49:25 +03:00
|
|
|
"os/exec"
|
2019-06-14 07:49:27 +03:00
|
|
|
"path"
|
|
|
|
"runtime"
|
2019-07-15 19:49:25 +03:00
|
|
|
"strconv"
|
2019-06-22 00:24:53 +03:00
|
|
|
"strings"
|
2019-06-15 18:13:10 +03:00
|
|
|
"testing"
|
2019-06-22 00:24:53 +03:00
|
|
|
|
2022-12-29 10:28:27 +03:00
|
|
|
"github.com/alecthomas/chroma/v2/formatters"
|
|
|
|
"github.com/alecthomas/chroma/v2/styles"
|
2022-09-25 10:06:46 +03:00
|
|
|
"gotest.tools/v3/assert"
|
2019-06-14 07:49:27 +03:00
|
|
|
)
|
2019-06-13 21:04:51 +03:00
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
func testGetLineCount(t *testing.T, reader *Reader) {
|
2019-07-10 01:21:36 +03:00
|
|
|
if strings.Contains(*reader.name, "compressed") {
|
|
|
|
// We are no good at counting lines of compressed files, never mind
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
cmd := exec.Command("wc", "-l", *reader.name)
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
if err != nil {
|
|
|
|
t.Error("Error calling wc -l to count lines of", *reader.name, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
wcNumberString := strings.Split(strings.TrimSpace(string(output)), " ")[0]
|
2021-05-03 20:20:21 +03:00
|
|
|
wcLineCount, err := strconv.Atoi(wcNumberString)
|
2019-07-10 01:21:36 +03:00
|
|
|
if err != nil {
|
|
|
|
t.Error("Error counting lines of", *reader.name, err)
|
|
|
|
}
|
Add another test case
diff --git m/reader_test.go m/reader_test.go
index 176192b..bed1e33 100644
--- m/reader_test.go
+++ m/reader_test.go
@@ -30,6 +30,12 @@ func _TestGetLineCount(t *testing.T, reader *Reader) {
if err != nil {
t.Error("Error counting lines of", *reader.name, err)
}
+
+ if strings.HasSuffix(*reader.name, "/line-without-newline.txt") {
+ // "wc -l" thinks this file contains zero lines
+ fileLineCount = 1
+ }
+
if reader.GetLineCount() != fileLineCount {
t.Errorf("Got %d lines but expected %d: <%s>",
reader.GetLineCount(), fileLineCount, *reader.name)
diff --git sample-files/line-without-newline.txt sample-files/line-without-newline.txt
new file mode 100644
index 0000000..2260c57
--- /dev/null
+++ sample-files/line-without-newline.txt
@@ -0,0 +1 @@
+This file contains no newlines
\ No newline at end of file
Change-Id: Ic2801ce3477a7afd4537d340385c884c5f2b7438
2019-11-19 16:47:44 +03:00
|
|
|
|
|
|
|
if strings.HasSuffix(*reader.name, "/line-without-newline.txt") {
|
|
|
|
// "wc -l" thinks this file contains zero lines
|
2021-05-03 20:20:21 +03:00
|
|
|
wcLineCount = 1
|
|
|
|
} else if strings.HasSuffix(*reader.name, "/two-lines-no-trailing-newline.txt") {
|
|
|
|
// "wc -l" thinks this file contains one line
|
|
|
|
wcLineCount = 2
|
Add another test case
diff --git m/reader_test.go m/reader_test.go
index 176192b..bed1e33 100644
--- m/reader_test.go
+++ m/reader_test.go
@@ -30,6 +30,12 @@ func _TestGetLineCount(t *testing.T, reader *Reader) {
if err != nil {
t.Error("Error counting lines of", *reader.name, err)
}
+
+ if strings.HasSuffix(*reader.name, "/line-without-newline.txt") {
+ // "wc -l" thinks this file contains zero lines
+ fileLineCount = 1
+ }
+
if reader.GetLineCount() != fileLineCount {
t.Errorf("Got %d lines but expected %d: <%s>",
reader.GetLineCount(), fileLineCount, *reader.name)
diff --git sample-files/line-without-newline.txt sample-files/line-without-newline.txt
new file mode 100644
index 0000000..2260c57
--- /dev/null
+++ sample-files/line-without-newline.txt
@@ -0,0 +1 @@
+This file contains no newlines
\ No newline at end of file
Change-Id: Ic2801ce3477a7afd4537d340385c884c5f2b7438
2019-11-19 16:47:44 +03:00
|
|
|
}
|
|
|
|
|
2021-05-03 20:20:21 +03:00
|
|
|
if reader.GetLineCount() != wcLineCount {
|
|
|
|
t.Errorf("Got %d lines from the reader but %d lines from wc -l: <%s>",
|
|
|
|
reader.GetLineCount(), wcLineCount, *reader.name)
|
|
|
|
}
|
|
|
|
|
|
|
|
countLinesCount, err := countLines(*reader.name)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
if countLinesCount != uint64(wcLineCount) {
|
|
|
|
t.Errorf("Got %d lines from wc -l, but %d lines from our countLines() function", wcLineCount, countLinesCount)
|
2019-07-10 01:21:36 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
func testGetLines(t *testing.T, reader *Reader) {
|
2019-06-22 00:24:53 +03:00
|
|
|
t.Logf("Testing file: %s...", *reader.name)
|
2019-06-13 21:04:51 +03:00
|
|
|
|
2023-05-18 10:01:14 +03:00
|
|
|
lines, _ := reader.GetLines(1, 10)
|
2019-06-13 21:04:51 +03:00
|
|
|
if len(lines.lines) > 10 {
|
|
|
|
t.Errorf("Asked for 10 lines, got too many: %d", len(lines.lines))
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(lines.lines) < 10 {
|
2019-07-10 01:21:36 +03:00
|
|
|
// No good plan for how to test short files, more than just
|
|
|
|
// querying them, which we just did
|
2019-06-13 21:04:51 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Test clipping at the end
|
2023-05-18 10:01:14 +03:00
|
|
|
lines, _ = reader.GetLines(math.MaxInt32, 10)
|
2019-06-13 21:04:51 +03:00
|
|
|
if len(lines.lines) != 10 {
|
2019-06-14 07:49:27 +03:00
|
|
|
t.Errorf("Asked for 10 lines but got %d", len(lines.lines))
|
2019-06-13 21:04:51 +03:00
|
|
|
return
|
|
|
|
}
|
2019-06-14 07:59:19 +03:00
|
|
|
|
|
|
|
startOfLastSection := lines.firstLineOneBased
|
2023-05-18 10:01:14 +03:00
|
|
|
lines, _ = reader.GetLines(startOfLastSection, 10)
|
2019-06-14 07:59:19 +03:00
|
|
|
if lines.firstLineOneBased != startOfLastSection {
|
|
|
|
t.Errorf("Expected start line %d when asking for the last 10 lines, got %d",
|
|
|
|
startOfLastSection, lines.firstLineOneBased)
|
|
|
|
return
|
|
|
|
}
|
2019-06-16 22:54:25 +03:00
|
|
|
if len(lines.lines) != 10 {
|
|
|
|
t.Errorf("Expected 10 lines when asking for the last 10 lines, got %d",
|
|
|
|
len(lines.lines))
|
|
|
|
return
|
|
|
|
}
|
2019-06-14 07:59:19 +03:00
|
|
|
|
2023-05-18 10:01:14 +03:00
|
|
|
lines, _ = reader.GetLines(startOfLastSection+1, 10)
|
2019-06-14 07:59:19 +03:00
|
|
|
if lines.firstLineOneBased != startOfLastSection {
|
|
|
|
t.Errorf("Expected start line %d when asking for the last+1 10 lines, got %d",
|
|
|
|
startOfLastSection, lines.firstLineOneBased)
|
|
|
|
return
|
|
|
|
}
|
2019-06-16 22:54:25 +03:00
|
|
|
if len(lines.lines) != 10 {
|
|
|
|
t.Errorf("Expected 10 lines when asking for the last+1 10 lines, got %d",
|
|
|
|
len(lines.lines))
|
|
|
|
return
|
|
|
|
}
|
2019-06-14 07:59:19 +03:00
|
|
|
|
2023-05-18 10:01:14 +03:00
|
|
|
lines, _ = reader.GetLines(startOfLastSection-1, 10)
|
2019-06-15 18:13:10 +03:00
|
|
|
if lines.firstLineOneBased != startOfLastSection-1 {
|
2019-06-14 07:59:19 +03:00
|
|
|
t.Errorf("Expected start line %d when asking for the last-1 10 lines, got %d",
|
|
|
|
startOfLastSection, lines.firstLineOneBased)
|
|
|
|
return
|
|
|
|
}
|
2019-06-16 22:54:25 +03:00
|
|
|
if len(lines.lines) != 10 {
|
|
|
|
t.Errorf("Expected 10 lines when asking for the last-1 10 lines, got %d",
|
|
|
|
len(lines.lines))
|
|
|
|
return
|
|
|
|
}
|
2019-06-13 21:04:51 +03:00
|
|
|
}
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
func getSamplesDir() string {
|
2019-06-14 07:49:27 +03:00
|
|
|
// From: https://coderwall.com/p/_fmbug/go-get-path-to-current-file
|
|
|
|
_, filename, _, ok := runtime.Caller(0)
|
|
|
|
if !ok {
|
|
|
|
panic("Getting current filename failed")
|
|
|
|
}
|
|
|
|
|
2019-06-23 22:30:11 +03:00
|
|
|
return path.Join(path.Dir(filename), "../sample-files")
|
|
|
|
}
|
2019-06-14 07:49:27 +03:00
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
func getTestFiles() []string {
|
2022-08-07 16:14:59 +03:00
|
|
|
files, err := os.ReadDir(getSamplesDir())
|
2019-06-15 18:13:10 +03:00
|
|
|
if err != nil {
|
2019-06-14 07:49:27 +03:00
|
|
|
panic(err)
|
2019-06-15 18:13:10 +03:00
|
|
|
}
|
2019-06-14 07:49:27 +03:00
|
|
|
|
|
|
|
var filenames []string
|
2019-06-15 18:13:10 +03:00
|
|
|
for _, file := range files {
|
|
|
|
filenames = append(filenames, "../sample-files/"+file.Name())
|
2019-06-14 07:49:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return filenames
|
|
|
|
}
|
|
|
|
|
2021-05-08 11:32:04 +03:00
|
|
|
// Wait for reader to finish reading and highlighting. Used by tests.
|
|
|
|
func (r *Reader) _wait() error {
|
|
|
|
// Wait for our goroutine to finish
|
2023-05-14 20:42:18 +03:00
|
|
|
for !r.done.Load() {
|
|
|
|
}
|
2023-05-14 20:56:03 +03:00
|
|
|
for !r.highlightingDone.Load() {
|
|
|
|
}
|
2021-05-08 11:32:04 +03:00
|
|
|
|
|
|
|
r.lock.Lock()
|
|
|
|
defer r.lock.Unlock()
|
|
|
|
return r.err
|
|
|
|
}
|
|
|
|
|
2019-06-13 21:04:51 +03:00
|
|
|
func TestGetLines(t *testing.T) {
|
2020-12-29 19:08:54 +03:00
|
|
|
for _, file := range getTestFiles() {
|
2021-04-15 15:57:56 +03:00
|
|
|
if strings.HasSuffix(file, ".xz") {
|
|
|
|
_, err := exec.LookPath("xz")
|
|
|
|
if err != nil {
|
|
|
|
t.Log("Not testing xz compressed file, xz not found in $PATH: ", file)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-19 05:14:43 +03:00
|
|
|
reader, err := NewReaderFromFilename(file, *styles.Get("native"), formatters.TTY16m)
|
2019-07-10 00:41:49 +03:00
|
|
|
if err != nil {
|
|
|
|
t.Errorf("Error opening file <%s>: %s", file, err.Error())
|
|
|
|
continue
|
|
|
|
}
|
2021-05-08 11:32:04 +03:00
|
|
|
if err := reader._wait(); err != nil {
|
2019-07-10 00:41:49 +03:00
|
|
|
t.Errorf("Error reading file <%s>: %s", file, err.Error())
|
2019-06-13 21:04:51 +03:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
testGetLines(t, reader)
|
|
|
|
testGetLineCount(t, reader)
|
2021-04-21 23:49:16 +03:00
|
|
|
testHighlightingLineCount(t, file)
|
2019-06-13 21:04:51 +03:00
|
|
|
}
|
2019-06-15 18:13:10 +03:00
|
|
|
}
|
2019-06-22 00:24:53 +03:00
|
|
|
|
2021-04-21 23:49:16 +03:00
|
|
|
func testHighlightingLineCount(t *testing.T, filenameWithPath string) {
|
|
|
|
// This won't work on compressed files
|
|
|
|
if strings.HasSuffix(filenameWithPath, ".xz") {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if strings.HasSuffix(filenameWithPath, ".bz2") {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if strings.HasSuffix(filenameWithPath, ".gz") {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Load the unformatted file
|
2022-08-07 16:14:59 +03:00
|
|
|
rawBytes, err := os.ReadFile(filenameWithPath)
|
2021-04-21 23:49:16 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
rawContents := string(rawBytes)
|
|
|
|
|
|
|
|
// Count its lines
|
|
|
|
rawLinefeedsCount := strings.Count(rawContents, "\n")
|
|
|
|
rawRunes := []rune(rawContents)
|
|
|
|
rawFileEndsWithNewline := true // Special case empty files
|
|
|
|
if len(rawRunes) > 0 {
|
|
|
|
rawFileEndsWithNewline = rawRunes[len(rawRunes)-1] == '\n'
|
|
|
|
}
|
|
|
|
rawLinesCount := rawLinefeedsCount
|
|
|
|
if !rawFileEndsWithNewline {
|
|
|
|
rawLinesCount += 1
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then load the same file using one of our Readers
|
2023-02-19 05:14:43 +03:00
|
|
|
reader, err := NewReaderFromFilename(filenameWithPath, *styles.Get("native"), formatters.TTY16m)
|
2021-04-21 23:49:16 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2021-05-08 11:32:04 +03:00
|
|
|
err = reader._wait()
|
2021-04-21 23:49:16 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
highlightedLinesCount := reader.GetLineCount()
|
|
|
|
assert.Check(t, rawLinesCount == highlightedLinesCount)
|
|
|
|
}
|
|
|
|
|
2019-11-19 18:53:17 +03:00
|
|
|
func TestGetLongLine(t *testing.T) {
|
|
|
|
file := "../sample-files/very-long-line.txt"
|
2023-02-19 05:14:43 +03:00
|
|
|
reader, err := NewReaderFromFilename(file, *styles.Get("native"), formatters.TTY16m)
|
2019-11-19 18:53:17 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2021-05-08 11:32:04 +03:00
|
|
|
if err := reader._wait(); err != nil {
|
2019-11-19 18:53:17 +03:00
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2023-05-18 10:01:14 +03:00
|
|
|
lines, overflow := reader.GetLines(1, 5)
|
2019-11-19 18:53:17 +03:00
|
|
|
assert.Equal(t, lines.firstLineOneBased, 1)
|
|
|
|
assert.Equal(t, len(lines.lines), 1)
|
2023-05-18 10:01:14 +03:00
|
|
|
assert.Equal(t, overflow, didOverflow)
|
2019-11-19 18:53:17 +03:00
|
|
|
|
2019-11-19 18:58:01 +03:00
|
|
|
line := lines.lines[0]
|
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 12:42:34 +03:00
|
|
|
assert.Assert(t, strings.HasPrefix(line.Plain(), "1 2 3 4"), "<%s>", line)
|
|
|
|
assert.Assert(t, strings.HasSuffix(line.Plain(), "0123456789"), line)
|
2019-11-19 18:58:01 +03:00
|
|
|
|
2021-04-15 15:51:51 +03:00
|
|
|
assert.Equal(t, len(line.Plain()), 100021)
|
2019-11-19 18:53:17 +03:00
|
|
|
}
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
func getReaderWithLineCount(totalLines int) *Reader {
|
2020-12-30 21:00:03 +03:00
|
|
|
reader := NewReaderFromStream("", strings.NewReader(strings.Repeat("x\n", totalLines)))
|
2021-05-08 11:32:04 +03:00
|
|
|
if err := reader._wait(); err != nil {
|
2019-07-10 00:41:49 +03:00
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return reader
|
2019-06-22 00:24:53 +03:00
|
|
|
}
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
func testStatusText(t *testing.T, fromLine int, toLine int, totalLines int, expected string) {
|
|
|
|
testMe := getReaderWithLineCount(totalLines)
|
2019-06-22 00:24:53 +03:00
|
|
|
linesRequested := toLine - fromLine + 1
|
2023-05-18 10:01:14 +03:00
|
|
|
lines, _ := testMe.GetLines(fromLine, linesRequested)
|
|
|
|
statusText := lines.statusText
|
2019-06-22 00:24:53 +03:00
|
|
|
assert.Equal(t, statusText, expected)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestStatusText(t *testing.T) {
|
2021-05-22 17:05:06 +03:00
|
|
|
testStatusText(t, 1, 10, 20, "20 lines 50%")
|
|
|
|
testStatusText(t, 1, 5, 5, "5 lines 100%")
|
|
|
|
testStatusText(t, 998, 999, 1000, "1_000 lines 99%")
|
2019-06-22 00:24:53 +03:00
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
testStatusText(t, 0, 0, 0, "<empty>")
|
2021-05-22 17:05:06 +03:00
|
|
|
testStatusText(t, 1, 1, 1, "1 line 100%")
|
2019-06-22 00:24:53 +03:00
|
|
|
|
|
|
|
// Test with filename
|
2023-02-19 05:14:43 +03:00
|
|
|
testMe, err := NewReaderFromFilename(getSamplesDir()+"/empty", *styles.Get("native"), formatters.TTY16m)
|
2019-06-22 00:24:53 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2021-05-08 11:32:04 +03:00
|
|
|
if err := testMe._wait(); err != nil {
|
2019-07-10 00:41:49 +03:00
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
2023-05-18 10:01:14 +03:00
|
|
|
line, overflow := testMe.GetLines(0, 0)
|
|
|
|
assert.Equal(t, line.statusText, "empty: <empty>")
|
|
|
|
assert.Equal(t, overflow, didFit) // Empty always fits
|
2019-06-22 00:24:53 +03:00
|
|
|
}
|
|
|
|
|
2020-12-29 19:08:54 +03:00
|
|
|
func testCompressedFile(t *testing.T, filename string) {
|
|
|
|
filenameWithPath := getSamplesDir() + "/" + filename
|
2023-02-19 05:14:43 +03:00
|
|
|
reader, e := NewReaderFromFilename(filenameWithPath, *styles.Get("native"), formatters.TTY16m)
|
2019-06-23 22:30:11 +03:00
|
|
|
if e != nil {
|
2019-06-24 22:55:33 +03:00
|
|
|
t.Errorf("Error opening file <%s>: %s", filenameWithPath, e.Error())
|
2019-06-23 22:30:11 +03:00
|
|
|
panic(e)
|
|
|
|
}
|
2021-05-08 11:32:04 +03:00
|
|
|
if err := reader._wait(); err != nil {
|
2019-07-10 00:41:49 +03:00
|
|
|
panic(err)
|
|
|
|
}
|
2019-06-23 22:30:11 +03:00
|
|
|
|
2023-05-18 10:01:14 +03:00
|
|
|
lines, _ := reader.GetLines(1, 5)
|
|
|
|
assert.Equal(t, lines.lines[0].Plain(), "This is a compressed file", "%s", filename)
|
2019-06-23 22:30:11 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestCompressedFiles(t *testing.T) {
|
2020-12-29 19:08:54 +03:00
|
|
|
testCompressedFile(t, "compressed.txt.gz")
|
|
|
|
testCompressedFile(t, "compressed.txt.bz2")
|
2021-04-15 14:24:24 +03:00
|
|
|
|
|
|
|
_, err := exec.LookPath("xz")
|
|
|
|
if err == nil {
|
|
|
|
testCompressedFile(t, "compressed.txt.xz")
|
|
|
|
} else {
|
|
|
|
t.Log("WARNING: xz not found in path, not testing automatic xz decompression")
|
|
|
|
}
|
2019-06-23 22:30:11 +03:00
|
|
|
}
|
2019-06-24 22:55:33 +03:00
|
|
|
|
2019-06-25 20:44:22 +03:00
|
|
|
func TestFilterNotInstalled(t *testing.T) {
|
|
|
|
// FIXME: Test what happens if we try to use a filter that is not installed
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFilterFailure(t *testing.T) {
|
|
|
|
// FIXME: Test what happens if the filter command fails because of bad command line options
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFilterPermissionDenied(t *testing.T) {
|
|
|
|
// FIXME: Test what happens if the filter command fails because it can't access the requested file
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestFilterFileNotFound(t *testing.T) {
|
2020-03-28 12:10:38 +03:00
|
|
|
// What happens if the filter cannot read its input file?
|
|
|
|
NonExistentPath := "/does-not-exist"
|
|
|
|
|
2020-12-29 19:19:56 +03:00
|
|
|
reader, err := newReaderFromCommand(NonExistentPath, "cat")
|
2020-03-28 12:10:38 +03:00
|
|
|
|
|
|
|
// Creating should be fine, it's waiting for it to finish that should fail.
|
|
|
|
// Feel free to re-evaluate in the future.
|
|
|
|
assert.Check(t, err == nil)
|
|
|
|
|
2021-05-08 11:32:04 +03:00
|
|
|
err = reader._wait()
|
2020-03-28 12:10:38 +03:00
|
|
|
assert.Check(t, err != nil)
|
|
|
|
|
|
|
|
assert.Check(t, strings.Contains(err.Error(), NonExistentPath), err.Error())
|
2019-06-25 20:44:22 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestFilterNotAFile(t *testing.T) {
|
|
|
|
// FIXME: Test what happens if the filter command fails because the target is not a file
|
|
|
|
}
|
2021-04-22 08:56:10 +03:00
|
|
|
|
|
|
|
// How long does it take to read a file?
|
|
|
|
//
|
|
|
|
// This can be slow due to highlighting.
|
|
|
|
//
|
2021-04-24 17:14:30 +03:00
|
|
|
// Run with: go test -run='^$' -bench=. . ./...
|
2021-04-22 08:56:10 +03:00
|
|
|
func BenchmarkReaderDone(b *testing.B) {
|
|
|
|
filename := getSamplesDir() + "/../m/pager.go" // This is our longest .go file
|
|
|
|
b.ResetTimer()
|
|
|
|
for n := 0; n < b.N; n++ {
|
|
|
|
// This is our longest .go file
|
2023-02-19 05:14:43 +03:00
|
|
|
readMe, err := NewReaderFromFilename(filename, *styles.Get("native"), formatters.TTY16m)
|
2021-04-22 08:56:10 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
2021-04-22 09:56:34 +03:00
|
|
|
|
|
|
|
// Wait for the reader to finish
|
2023-05-14 20:42:18 +03:00
|
|
|
for !readMe.done.Load() {
|
|
|
|
}
|
2021-04-22 09:56:34 +03:00
|
|
|
if readMe.err != nil {
|
|
|
|
panic(readMe.err)
|
2021-04-22 08:56:10 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-03 20:15:43 +03:00
|
|
|
|
|
|
|
// Try loading a large file
|
|
|
|
func BenchmarkReadLargeFile(b *testing.B) {
|
|
|
|
// Try loading a file this large
|
|
|
|
const largeSizeBytes = 35_000_000
|
|
|
|
|
|
|
|
// First, create it from something...
|
|
|
|
input_filename := getSamplesDir() + "/../m/pager.go"
|
2022-08-07 16:14:59 +03:00
|
|
|
contents, err := os.ReadFile(input_filename)
|
2021-05-03 20:15:43 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
testdir := b.TempDir()
|
|
|
|
largeFileName := testdir + "/large-file"
|
|
|
|
largeFile, err := os.Create(largeFileName)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
totalBytesWritten := 0
|
|
|
|
for totalBytesWritten < largeSizeBytes {
|
|
|
|
written, err := largeFile.Write(contents)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
totalBytesWritten += written
|
|
|
|
}
|
|
|
|
err = largeFile.Close()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
b.ResetTimer()
|
|
|
|
for n := 0; n < b.N; n++ {
|
2023-02-19 05:14:43 +03:00
|
|
|
readMe, err := NewReaderFromFilename(largeFileName, *styles.Get("native"), formatters.TTY16m)
|
2021-05-03 20:15:43 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Wait for the reader to finish
|
2023-05-14 20:42:18 +03:00
|
|
|
for !readMe.done.Load() {
|
|
|
|
}
|
2021-05-03 20:15:43 +03:00
|
|
|
if readMe.err != nil {
|
|
|
|
panic(readMe.err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-05-08 14:57:54 +03:00
|
|
|
|
|
|
|
// Count lines in pager.go
|
|
|
|
func BenchmarkCountLines(b *testing.B) {
|
|
|
|
// First, get some sample lines...
|
|
|
|
input_filename := getSamplesDir() + "/../m/pager.go"
|
2022-08-07 16:14:59 +03:00
|
|
|
contents, err := os.ReadFile(input_filename)
|
2021-05-08 14:57:54 +03:00
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
testdir := b.TempDir()
|
|
|
|
countFileName := testdir + "/count-file"
|
|
|
|
countFile, err := os.Create(countFileName)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// 1000x makes this take about 12ms on my machine right now. Before 1000x
|
|
|
|
// the numbers fluctuated much more.
|
|
|
|
for n := 0; n < b.N*1000; n++ {
|
|
|
|
_, err := countFile.Write(contents)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
err = countFile.Close()
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
b.ResetTimer()
|
|
|
|
_, err = countLines(countFileName)
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|