Skip to content

Commit

Permalink
[fix] perf improvements and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
robinovitch61 committed Dec 24, 2024
1 parent 2db53d3 commit 5cc427c
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 64 deletions.
34 changes: 20 additions & 14 deletions internal/filterable_viewport/filterable_viewport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"regexp"
"strings"
"testing"
"time"
)

var (
Expand Down Expand Up @@ -341,17 +342,22 @@ func TestFilterableViewport_FilterRegex(t *testing.T) {
}
}

// TODO LEO: improve this
//func TestFilterableViewport_LongLineManyMatches(t *testing.T) {
// fv := newFilterableViewport()
// fv, _ = fv.Update(wrapKeyMsg)
// if !fv.viewport.GetWrapText() {
// t.Error("wrap text should be enabled")
// }
// fv.SetAllRows([]TestItem{
// {content: strings.Repeat("r", 10000)},
// })
// fv = applyTestFilter(fv, focusRegexFilterKeyMsg, "r")
// lines := getTestLines(fv)
// println(len(lines))
//}
func TestFilterableViewport_LongLineManyMatches(t *testing.T) {
runTest := func(t *testing.T) {
fv := newFilterableViewport()
fv, _ = fv.Update(wrapKeyMsg)
if !fv.viewport.GetWrapText() {
t.Error("wrap text should be enabled")
}
fv.SetAllRows([]TestItem{
{content: strings.Repeat("rick ross really rad rebel arrr", 10000)},
})
fv = applyTestFilter(fv, focusRegexFilterKeyMsg, "r")
lines := getTestLines(fv)
if len(lines) != 20 {
t.Errorf("expected 20 lines, got %d", len(lines))
}
}

util.RunWithTimeout(t, runTest, 3500*time.Millisecond)
}
56 changes: 38 additions & 18 deletions internal/linebuffer/linebuffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func (l LineBuffer) Truncate(xOffset, width int) string {
}

if len(l.ansiCodeIndexes) > 0 {
//println(fmt.Sprintf("reapplyAnsi(%q, %q, %d, %v) = %q", l.line, visible, l.byteOffsets[start], l.ansiCodeIndexes, reapplied))
return reapplyANSI(l.line, visible, l.byteOffsets[start], l.ansiCodeIndexes)
}
return visible
Expand Down Expand Up @@ -142,24 +143,8 @@ func reapplyANSI(original, truncated string, truncByteOffset int, ansiCodeIndexe
}
}

// if there's just a bunch of reset sequences, compress it to one
allReset := len(ansisToAdd) > 0
for _, ansi := range ansisToAdd {
if ansi != "\x1b[m" {
allReset = false
break
}
}
if allReset {
ansisToAdd = []string{"\x1b[m"}
}

// if the last sequence in a set of more than one is a reset, no point adding any of them
redundant := len(ansisToAdd) > 1 && ansisToAdd[len(ansisToAdd)-1] == "\x1b[m"
if !redundant {
for _, ansi := range ansisToAdd {
result = append(result, ansi...)
}
for _, ansi := range simplifyAnsiCodes(ansisToAdd) {
result = append(result, ansi...)
}

// add the bytes of the current rune
Expand All @@ -175,6 +160,41 @@ func reapplyANSI(original, truncated string, truncByteOffset int, ansiCodeIndexe
return string(result)
}

func simplifyAnsiCodes(ansis []string) []string {
//println()
//for _, a := range ansis {
// println(fmt.Sprintf("%q", a))
//}
if len(ansis) == 0 {
return []string{}
}

// if there's just a bunch of reset sequences, compress it to one
allReset := true
for _, ansi := range ansis {
if ansi != "\x1b[m" {
allReset = false
break
}
}
if allReset {
return []string{"\x1b[m"}
}

// return all ansis to the right of the rightmost reset seq
for i := len(ansis) - 1; i >= 0; i-- {
if ansis[i] == "\x1b[m" {
result := ansis[i+1:]
// keep reset at the start if present
if ansis[0] == "\x1b[m" {
return append([]string{"\x1b[m"}, result...)
}
return result
}
}
return ansis
}

func initByteOffsets(runes []rune) []int {
offsets := make([]int, len(runes)+1)
currentOffset := 0
Expand Down
21 changes: 21 additions & 0 deletions internal/linebuffer/linebuffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,13 @@ func TestReapplyAnsi(t *testing.T) {
truncByteOffset: 1,
expected: "2345",
},
{
name: "multi ansi, no offset",
original: "\x1b[38;2;255;0;0m1\x1b[m\x1b[38;2;0;0;255m2\x1b[m\x1b[38;2;255;0;0m3\x1b[m45",
truncated: "123",
truncByteOffset: 0,
expected: "\x1b[38;2;255;0;0m1\x1b[m\x1b[38;2;0;0;255m2\x1b[m\x1b[38;2;255;0;0m3\x1b[m",
},
{
name: "surrounding ansi, no offset",
original: "\x1b[38;2;255;0;0m12345\x1b[m",
Expand Down Expand Up @@ -417,6 +424,20 @@ func TestReapplyAnsi(t *testing.T) {
truncByteOffset: 3, // 你 is 3 bytes
expected: "\x1b[31m\x1b[32m好\x1b[33m世\x1b[m界",
},
{
name: "lots of leading ansi",
original: "\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;255;0;0mr\x1b[m",
truncated: "r",
truncByteOffset: 10,
expected: "\x1b[38;2;255;0;0mr\x1b[m",
},
{
name: "complex ansi, no offset",
original: "\x1b[38;2;0;0;255msome \x1b[m\x1b[38;2;255;0;0mred\x1b[m\x1b[38;2;0;0;255m t\x1b[m",
truncated: "some red t",
truncByteOffset: 0,
expected: "\x1b[38;2;0;0;255msome \x1b[m\x1b[38;2;255;0;0mred\x1b[m\x1b[38;2;0;0;255m t\x1b[m",
},
}

for _, tt := range tests {
Expand Down
18 changes: 18 additions & 0 deletions internal/util/time.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package util

import (
"fmt"
"testing"
"time"
)

Expand Down Expand Up @@ -81,3 +82,20 @@ func DurationTilNext(start time.Time, now time.Time, between time.Duration) time
nextUpdate := start.Add(between * (intervals + 1))
return nextUpdate.Sub(now)
}

func RunWithTimeout(t *testing.T, runTest func(t *testing.T), timeout time.Duration) {
done := make(chan bool)
start := time.Now()
go func() {
runTest(t)
done <- true
}()

select {
case <-done:
break
case <-time.After(timeout):
elapsed := time.Since(start)
t.Fatalf("Test took too long: %v", elapsed)
}
}
29 changes: 8 additions & 21 deletions internal/viewport/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,32 +55,22 @@ func percent(a, b int) int {
return int(float32(a) / float32(b) * 100)
}

// highlightLine highlights a line that potentially has ansi codes in it without disrupting them
// highlightLine highlights a string in a line that potentially has ansi codes in it without disrupting them
func highlightLine(line, highlight string, highlightStyle lipgloss.Style) string {
if line == "" || highlight == "" {
return line
}

// Helper function to check if we're inside an ANSI escape sequence
isInAnsiCode := func(s string, pos int) bool {
// Look back for ESC character
for i := pos; i >= 0; i-- {
if s[i] == '\x1b' {
return true
} else if s[i] == 'm' {
return false
}
}
return false
}

renderedHighlight := highlightStyle.Render(highlight)
result := &strings.Builder{}
i := 0
activeStyle := ""
inAnsiCode := false

for i < len(line) {
if strings.HasPrefix(line[i:], "\x1b[") {
// Found start of ANSI sequence
inAnsiCode = true
escEnd := strings.Index(line[i:], "m")
if escEnd != -1 {
escEnd += i + 1
Expand All @@ -92,35 +82,32 @@ func highlightLine(line, highlight string, highlightStyle lipgloss.Style) string
}
result.WriteString(currentSequence)
i = escEnd
inAnsiCode = false
continue
}
}

// Check if current position starts a highlight match
if len(highlight) > 0 && strings.HasPrefix(line[i:], highlight) && !isInAnsiCode(line, i) {
if len(highlight) > 0 && strings.HasPrefix(line[i:], highlight) && !inAnsiCode {
// Reset current style if any
if activeStyle != "" {
result.WriteString("\x1b[m")
}

// Apply highlight
result.WriteString(highlightStyle.Render(highlight))

result.WriteString(renderedHighlight)
// Restore previous style if there was one
if activeStyle != "" {
result.WriteString(activeStyle)
}

i += len(highlight)
continue
}

// Regular character
result.WriteByte(line[i])
i++
}

// removing empty sequences may hurt performance, but helps legibility
// TODO LEO: figure out how to remove this
return constants.EmptySequenceRegex.ReplaceAllString(result.String(), "")
}

Expand Down
36 changes: 29 additions & 7 deletions internal/viewport/util_test.go

Large diffs are not rendered by default.

13 changes: 9 additions & 4 deletions internal/viewport/viewport.go
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,7 @@ func (m Model[T]) getVisibleContentLines() visibleContentLinesResult {
if m.selectionEnabled && idx == m.selectedItemIdx {
highlightStyle = m.HighlightStyleIfSelected
}
//println(fmt.Sprintf("highlightLine(%q, %s, %s) = %q", item.Render(), m.stringToHighlight, highlightStyle, highlighted))
return highlightLine(item.Render(), m.stringToHighlight, highlightStyle)
}

Expand Down Expand Up @@ -873,15 +874,19 @@ func (m Model[T]) getNumVisibleItems() int {
func (m Model[T]) styleSelection(s string) string {
split := surroundingAnsiRegex.Split(s, -1)
matches := surroundingAnsiRegex.FindAllString(s, -1)
var builder strings.Builder

// Pre-allocate the builder's capacity based on the input string length
// This is optional but can improve performance for longer strings
builder.Grow(len(s))

finalResult := ""
for i, section := range split {
if section != "" {
finalResult += m.SelectedItemStyle.Render(section)
builder.WriteString(m.SelectedItemStyle.Render(section))
}
if i < len(split)-1 && i < len(matches) {
finalResult += matches[i]
builder.WriteString(matches[i])
}
}
return finalResult
return builder.String()
}
Loading

0 comments on commit 5cc427c

Please sign in to comment.