diff --git a/cpu.prof b/cpu.prof new file mode 100644 index 0000000..ee3524b Binary files /dev/null and b/cpu.prof differ diff --git a/internal/viewport/linebuffer/linebuffer.go b/internal/viewport/linebuffer/linebuffer.go index 497a989..3cb671d 100644 --- a/internal/viewport/linebuffer/linebuffer.go +++ b/internal/viewport/linebuffer/linebuffer.go @@ -9,16 +9,16 @@ import ( ) // LineBuffer provides functionality to get sequential strings of a specified terminal width, accounting -// for the ansi escape codes styling the content. +// for the ansi escape codes styling the Content. type LineBuffer struct { Content string // underlying string with ansi codes. utf-8 bytes leftRuneIdx int // left plaintext rune idx to start next PopLeft result from - lineNoAnsi string // line without ansi codes. utf-8 bytes + lineNoAnsi string // Content without ansi codes. utf-8 bytes lineNoAnsiRunes []rune // runes of lineNoAnsi. len(lineNoAnsiRunes) == len(lineNoAnsiWidths) runeIdxToByteOffset []int // idx of lineNoAnsiRunes to byte offset. len(runeIdxToByteOffset) == len(lineNoAnsiRunes) lineNoAnsiWidths []int // terminal cell widths of lineNoAnsi. len(lineNoAnsiWidths) == len(lineNoAnsiRunes) lineNoAnsiCumWidths []int // cumulative lineNoAnsiWidths - ansiCodeIndexes [][]int // slice of startByte, endByte indexes of ansi codes in the line + ansiCodeIndexes [][]int // slice of startByte, endByte indexes of ansi codes in the Content } func New(line string) LineBuffer { @@ -27,31 +27,70 @@ func New(line string) LineBuffer { leftRuneIdx: 0, } - lb.ansiCodeIndexes = constants.AnsiRegex.FindAllStringIndex(line, -1) - lb.lineNoAnsi = stripAnsi(line) + lb.ansiCodeIndexes = findAnsiRanges(line) - lb.lineNoAnsiRunes = []rune(lb.lineNoAnsi) - n := len(lb.lineNoAnsiRunes) - lb.runeIdxToByteOffset = make([]int, n+1) - lb.lineNoAnsiWidths = make([]int, n) - lb.lineNoAnsiCumWidths = make([]int, n) + if len(lb.ansiCodeIndexes) > 0 { + totalLen := len(line) + for _, r := range lb.ansiCodeIndexes { + totalLen -= r[1] - r[0] + } + + buf := make([]byte, 0, totalLen) + lastPos := 0 + for _, r := range lb.ansiCodeIndexes { + buf = append(buf, line[lastPos:r[0]]...) + lastPos = r[1] + } + buf = append(buf, line[lastPos:]...) + lb.lineNoAnsi = string(buf) + } else { + lb.lineNoAnsi = line + } + + n := utf8.RuneCountInString(lb.lineNoAnsi) + + // single allocation for all integer slices + combined := make([]int, n+1+n+n) + lb.runeIdxToByteOffset = combined[:n+1] + lb.lineNoAnsiWidths = combined[n+1 : n+1+n] + lb.lineNoAnsiCumWidths = combined[n+1+n:] + + lb.lineNoAnsiRunes = make([]rune, n) currentOffset := 0 cumWidth := 0 - for i, r := range lb.lineNoAnsiRunes { + i := 0 + for _, r := range lb.lineNoAnsi { lb.runeIdxToByteOffset[i] = currentOffset currentOffset += utf8.RuneLen(r) - + lb.lineNoAnsiRunes[i] = r width := runewidth.RuneWidth(r) lb.lineNoAnsiWidths[i] = width cumWidth += width lb.lineNoAnsiCumWidths[i] = cumWidth + i++ } lb.runeIdxToByteOffset[n] = currentOffset return lb } +func ToLineBuffers(lines []string) []LineBuffer { + res := make([]LineBuffer, len(lines)) + for i, line := range lines { + res[i] = New(line) + } + return res +} + +func ToStrings(lbs []LineBuffer) []string { + res := make([]string, len(lbs)) + for i, lb := range lbs { + res[i] = lb.Content + } + return res +} + func (l LineBuffer) TotalLines(width int) int { if width == 0 { return 0 @@ -68,7 +107,7 @@ func (l *LineBuffer) SeekToLine(n, width int) { } func (l *LineBuffer) SeekToWidth(width int) { - // width can go past end, in which case PopLeft() returns "". Required when e.g. panning past line's end. + // width can go past end, in which case PopLeft() returns "". Required when e.g. panning past Content's end. if width <= 0 { l.leftRuneIdx = 0 return @@ -137,7 +176,7 @@ func (l *LineBuffer) PopLeft(width int, continuation, toHighlight string, highli res := result.String() - // apply left/right line continuation indicators + // apply left/right Content continuation indicators if len(continuation) > 0 && (startRuneIdx > 0 || l.leftRuneIdx < len(l.lineNoAnsiRunes)) { continuationRunes := []rune(continuation) @@ -274,8 +313,8 @@ func reapplyAnsi(original, truncated string, truncByteOffset int, ansiCodeIndexe return string(result) } -// highlightLine highlights a string in a line that potentially has ansi codes in it without disrupting them -// start and end are the byte offsets for which highlighting is considered in the line, not counting ansi codes +// highlightLine highlights a string in a Content that potentially has ansi codes in it without disrupting them +// start and end are the byte offsets for which highlighting is considered in the Content, not counting ansi codes func highlightLine(line, highlight string, highlightStyle lipgloss.Style, start, end int) string { if line == "" || highlight == "" { return line @@ -339,7 +378,28 @@ func highlightLine(line, highlight string, highlightStyle lipgloss.Style, start, } func stripAnsi(input string) string { - return constants.AnsiRegex.ReplaceAllString(input, "") + ranges := findAnsiRanges(input) + if len(ranges) == 0 { + return input + } + + totalAnsiLen := 0 + for _, r := range ranges { + totalAnsiLen += r[1] - r[0] + } + + finalLen := len(input) - totalAnsiLen + var builder strings.Builder + builder.Grow(finalLen) + + lastPos := 0 + for _, r := range ranges { + builder.WriteString(input[lastPos:r[0]]) + lastPos = r[1] + } + + builder.WriteString(input[lastPos:]) + return builder.String() } func simplifyAnsiCodes(ansis []string) []string { @@ -377,18 +437,6 @@ func simplifyAnsiCodes(ansis []string) []string { return ansis } -func initByteOffsets(runes []rune) []int { - offsets := make([]int, len(runes)+1) - currentOffset := 0 - for i, r := range runes { - offsets[i] = currentOffset - runeLen := utf8.RuneLen(r) - currentOffset += runeLen - } - offsets[len(runes)] = currentOffset - return offsets -} - // overflowsLeft checks if a substring overflows a string on the left if the string were to start at startByteIdx inclusive. // assumes s has no ansi codes. // It performs a case-sensitive comparison and returns two values: @@ -577,3 +625,41 @@ func replaceRuneWithRunes(rs []rune, idxToReplace int, replaceWith []rune) []run copy(result[idxToReplace+len(replaceWith):], rs[idxToReplace+1:]) return result } + +func findAnsiRanges(s string) [][]int { + // pre-count to allocate exact size + count := strings.Count(s, "\x1b[") + if count == 0 { + return nil + } + + allRanges := make([]int, count*2) + ranges := make([][]int, count) + + for i := 0; i < count; i++ { + ranges[i] = allRanges[i*2 : i*2+2] + } + + rangeIdx := 0 + for i := 0; i < len(s); { + if i+1 < len(s) && s[i] == '\x1b' && s[i+1] == '[' { + start := i + i += 2 // skip \x1b[ + + // find the 'm' that ends this sequence + for i < len(s) && s[i] != 'm' { + i++ + } + + if i < len(s) && s[i] == 'm' { + allRanges[rangeIdx*2] = start + allRanges[rangeIdx*2+1] = i + 1 + rangeIdx++ + i++ + continue + } + } + i++ + } + return ranges[:rangeIdx] +} diff --git a/internal/viewport/linebuffer/linebuffer_bench_test.go b/internal/viewport/linebuffer/linebuffer_bench_test.go new file mode 100644 index 0000000..a765e8f --- /dev/null +++ b/internal/viewport/linebuffer/linebuffer_bench_test.go @@ -0,0 +1,105 @@ +package linebuffer + +import ( + "fmt" + "strings" + "testing" +) + +// Example of interpreting output of `go test -v -bench=. -run=^$ -benchmem ./internal/linebuffer` +// BenchmarkNewLongLine-8 7842 152640 ns/op 904063 B/op 8 allocs/op +// - 7842: benchmark ran 7,842 iterations to get a stable measurement +// - 152640 ns/op: each call to New() takes about 153 microseconds +// - 904063 B/op: each operation allocates about 904KB of memory +// - 8 allocs/op: each call to New() makes 8 distinct memory allocations + +func BenchmarkNewLongLine(b *testing.B) { + base := strings.Repeat("hi there random words woohoo ", 1000) + + // reset timer to exclude setup time + b.ResetTimer() + + // enable memory profiling + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + lb := New(base) + // prevent compiler optimizations from eliminating the call + _ = lb + } +} + +func BenchmarkMemoryComparisonAscii(b *testing.B) { + // tests different string lengths to see how memory usage scales + sizes := []int{10, 100, 1000, 10000} + + for _, size := range sizes { + baseString := strings.Repeat("h", size) + + b.Run(fmt.Sprintf("String_%d", size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s := baseString + _ = s + } + }) + + b.Run(fmt.Sprintf("LineBuffer_%d", size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + lb := New(baseString) + _ = lb + } + }) + } +} + +func BenchmarkMemoryComparisonAsciiWithAnsi(b *testing.B) { + // tests different string lengths to see how memory usage scales + sizes := []int{10, 100, 1000, 10000} + + for _, size := range sizes { + baseString := strings.Repeat("\x1b[31mh\x1b[0m", size) + + b.Run(fmt.Sprintf("String_%d", size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s := baseString + _ = s + } + }) + + b.Run(fmt.Sprintf("LineBuffer_%d", size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + lb := New(baseString) + _ = lb + } + }) + } +} + +func BenchmarkMemoryComparisonAsciiWithUnicode(b *testing.B) { + // tests different string lengths to see how memory usage scales + sizes := []int{10, 100, 1000, 10000} + + for _, size := range sizes { + baseString := strings.Repeat("δΈ–", size) + + b.Run(fmt.Sprintf("String_%d", size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + s := baseString + _ = s + } + }) + + b.Run(fmt.Sprintf("LineBuffer_%d", size), func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + lb := New(baseString) + _ = lb + } + }) + } +} diff --git a/internal/viewport/linebuffer/linebuffer_test.go b/internal/viewport/linebuffer/linebuffer_test.go index d736890..92b16f5 100644 --- a/internal/viewport/linebuffer/linebuffer_test.go +++ b/internal/viewport/linebuffer/linebuffer_test.go @@ -2,8 +2,8 @@ package linebuffer import ( "github.com/charmbracelet/lipgloss/v2" - "github.com/robinovitch61/kl/internal/constants" "github.com/robinovitch61/kl/internal/util" + "regexp" "strings" "testing" ) @@ -224,7 +224,7 @@ func TestLineBuffer_SeekToLine(t *testing.T) { expectedPopLeft: "", }, { - name: "seek to negative line", + name: "seek to negative Content", s: "12345", width: 2, continuation: "", @@ -232,7 +232,7 @@ func TestLineBuffer_SeekToLine(t *testing.T) { expectedPopLeft: "12", }, { - name: "seek to zero'th line", + name: "seek to zero'th Content", s: "12345", width: 2, continuation: "", @@ -240,7 +240,7 @@ func TestLineBuffer_SeekToLine(t *testing.T) { expectedPopLeft: "12", }, { - name: "seek to first line", + name: "seek to first Content", s: "12345", width: 2, continuation: "", @@ -248,7 +248,7 @@ func TestLineBuffer_SeekToLine(t *testing.T) { expectedPopLeft: "34", }, { - name: "seek to second line", + name: "seek to second Content", s: "12345", width: 2, continuation: "", @@ -264,7 +264,7 @@ func TestLineBuffer_SeekToLine(t *testing.T) { expectedPopLeft: "", }, { - name: "unicode zero'th line", + name: "unicode zero'th Content", s: "δΈ–η•ŒπŸŒŸδΈ–η•ŒπŸŒŸ", width: 2, continuation: "", @@ -272,7 +272,7 @@ func TestLineBuffer_SeekToLine(t *testing.T) { expectedPopLeft: "δΈ–", }, { - name: "unicode first line", + name: "unicode first Content", s: "δΈ–η•ŒπŸŒŸδΈ–η•ŒπŸŒŸ", width: 2, continuation: "", @@ -647,13 +647,13 @@ func TestLineBuffer_PopLeft(t *testing.T) { }, { name: "ansi simple, no continuation", - s: "\x1b[38;2;255;0;0ma really really long line\x1b[m", + s: "\x1b[38;2;255;0;0ma really really long Content\x1b[m", width: 15, continuation: "", numPopLefts: 2, expected: []string{ "\x1b[38;2;255;0;0ma really really\x1b[m", - "\x1b[38;2;255;0;0m long line\x1b[m", + "\x1b[38;2;255;0;0m long Content\x1b[m", }, }, { @@ -670,13 +670,13 @@ func TestLineBuffer_PopLeft(t *testing.T) { }, { name: "inline ansi, no continuation", - s: "\x1b[38;2;255;0;0ma\x1b[m really really long line", + s: "\x1b[38;2;255;0;0ma\x1b[m really really long Content", width: 15, continuation: "", numPopLefts: 2, expected: []string{ "\x1b[38;2;255;0;0ma\x1b[m really really", - " long line", + " long Content", }, }, { @@ -917,7 +917,7 @@ func TestLineBuffer_WrappedLines(t *testing.T) { want: []string{""}, }, { - name: "single line within width", + name: "single Content within width", s: "Hello", width: 10, maxLinesEachEnd: 2, @@ -932,27 +932,27 @@ func TestLineBuffer_WrappedLines(t *testing.T) { }, { name: "zero maxLinesEachEnd", - s: "This is a very long line that needs wrapping", + s: "This is a very long Content that needs wrapping", width: 10, maxLinesEachEnd: 0, - want: []string{"This is a ", "very long ", "line that ", "needs wrap", "ping"}, + want: []string{"This is a ", "very long ", "Content that ", "needs wrap", "ping"}, }, { name: "negative maxLinesEachEnd", - s: "This is a very long line that needs wrapping", + s: "This is a very long Content that needs wrapping", width: 10, maxLinesEachEnd: -1, - want: []string{"This is a ", "very long ", "line that ", "needs wrap", "ping"}, + want: []string{"This is a ", "very long ", "Content that ", "needs wrap", "ping"}, }, { name: "limited by maxLinesEachEnd", - s: "This is a very long line that needs wrapping", + s: "This is a very long Content that needs wrapping", width: 10, maxLinesEachEnd: 2, want: []string{ "This is a ", "very long ", - //"line that ", + //"Content that ", "needs wrap", "ping"}, }, @@ -1048,7 +1048,7 @@ func TestLineBuffer_WrappedLines(t *testing.T) { for i := range got { if got[i] != tt.want[i] { - t.Errorf("wrap() line %d got %q, expected %q", i, got[i], tt.want[i]) + t.Errorf("wrap() Content %d got %q, expected %q", i, got[i], tt.want[i]) } } }) @@ -1311,9 +1311,11 @@ func TestLineBuffer_reapplyAnsi(t *testing.T) { }, } + ansiRegex := regexp.MustCompile("\x1b\\[[0-9;]*m") + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ansiCodeIndexes := constants.AnsiRegex.FindAllStringIndex(tt.original, -1) + ansiCodeIndexes := ansiRegex.FindAllStringIndex(tt.original, -1) actual := reapplyAnsi(tt.original, tt.truncated, tt.truncByteOffset, ansiCodeIndexes) util.CmpStr(t, tt.expected, actual) }) @@ -1355,18 +1357,18 @@ func TestLineBuffer_highlightLine(t *testing.T) { expected: "h\x1b[38;2;255;0;0mell\x1b[mo", }, { - name: "highlight already styled line", - line: "\x1b[38;2;255;0;0mfirst line\x1b[m", + name: "highlight already styled Content", + line: "\x1b[38;2;255;0;0mfirst Content\x1b[m", highlight: "first", highlightStyle: lipgloss.NewStyle().Foreground(blue), - expected: "\x1b[38;2;255;0;0m\x1b[m\x1b[38;2;0;0;255mfirst\x1b[m\x1b[38;2;255;0;0m line\x1b[m", + expected: "\x1b[38;2;255;0;0m\x1b[m\x1b[38;2;0;0;255mfirst\x1b[m\x1b[38;2;255;0;0m Content\x1b[m", }, { - name: "highlight already partially styled line", - line: "hi a \x1b[38;2;255;0;0mstyled line\x1b[m cool \x1b[38;2;255;0;0mand styled\x1b[m more", + name: "highlight already partially styled Content", + line: "hi a \x1b[38;2;255;0;0mstyled Content\x1b[m cool \x1b[38;2;255;0;0mand styled\x1b[m more", highlight: "style", highlightStyle: lipgloss.NewStyle().Foreground(blue), - expected: "hi a \x1b[38;2;255;0;0m\x1b[m\x1b[38;2;0;0;255mstyle\x1b[m\x1b[38;2;255;0;0md line\x1b[m cool \x1b[38;2;255;0;0mand \x1b[m\x1b[38;2;0;0;255mstyle\x1b[m\x1b[38;2;255;0;0md\x1b[m more", + expected: "hi a \x1b[38;2;255;0;0m\x1b[m\x1b[38;2;0;0;255mstyle\x1b[m\x1b[38;2;255;0;0md Content\x1b[m cool \x1b[38;2;255;0;0mand \x1b[m\x1b[38;2;0;0;255mstyle\x1b[m\x1b[38;2;255;0;0md\x1b[m more", }, { name: "dont highlight ansi escape codes themselves", @@ -1376,14 +1378,14 @@ func TestLineBuffer_highlightLine(t *testing.T) { expected: "\x1b[38;2;255;0;0mhi\x1b[m", }, { - name: "single letter in partially styled line", - line: "line \x1b[38;2;255;0;0mred\x1b[m e again", + name: "single letter in partially styled Content", + line: "Content \x1b[38;2;255;0;0mred\x1b[m e again", highlight: "e", highlightStyle: lipgloss.NewStyle().Foreground(blue), expected: "lin\x1b[38;2;0;0;255me\x1b[m \x1b[38;2;255;0;0mr\x1b[m\x1b[38;2;0;0;255me\x1b[m\x1b[38;2;255;0;0md\x1b[m \x1b[38;2;0;0;255me\x1b[m again", }, { - name: "super long line", + name: "super long Content", line: strings.Repeat("python generator code world world world code text test code words random words generator hello python generator", 10000), highlight: "e", highlightStyle: lipgloss.NewStyle().Foreground(red), @@ -1391,30 +1393,30 @@ func TestLineBuffer_highlightLine(t *testing.T) { }, { name: "start and end", - line: "my line", - highlight: "line", + line: "my Content", + highlight: "Content", highlightStyle: lipgloss.NewStyle().Foreground(red), start: 0, end: 2, - expected: "my line", + expected: "my Content", }, { name: "start and end ansi, in range", - line: "\x1b[38;2;0;0;255mmy line\x1b[m", + line: "\x1b[38;2;0;0;255mmy Content\x1b[m", highlight: "my", highlightStyle: lipgloss.NewStyle().Foreground(red), start: 0, end: 2, - expected: "\x1b[38;2;0;0;255m\x1b[m\x1b[38;2;255;0;0mmy\x1b[m\x1b[38;2;0;0;255m line\x1b[m", + expected: "\x1b[38;2;0;0;255m\x1b[m\x1b[38;2;255;0;0mmy\x1b[m\x1b[38;2;0;0;255m Content\x1b[m", }, { name: "start and end ansi, out of range", - line: "\x1b[38;2;0;0;255mmy line\x1b[m", + line: "\x1b[38;2;0;0;255mmy Content\x1b[m", highlight: "my", highlightStyle: lipgloss.NewStyle().Foreground(red), start: 2, end: 4, - expected: "\x1b[38;2;0;0;255mmy line\x1b[m", + expected: "\x1b[38;2;0;0;255mmy Content\x1b[m", }, } { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/viewport/viewport.go b/internal/viewport/viewport.go index c581ff3..0b5791e 100644 --- a/internal/viewport/viewport.go +++ b/internal/viewport/viewport.go @@ -214,9 +214,9 @@ func (m Model[T]) View() string { truncatedVisibleContentLines := make([]string, len(visibleContentLines.lines)) for i := range visibleContentLines.lines { if m.wrapText { - truncated = visibleContentLines.lines[i] + truncated = visibleContentLines.lines[i].Content } else { - lineBuffer := linebuffer.New(visibleContentLines.lines[i]) + lineBuffer := visibleContentLines.lines[i] lineBuffer.SeekToWidth(m.xOffset) truncated = lineBuffer.PopLeft(m.width, m.continuationIndicator, m.stringToHighlight, m.highlightStyle(visibleContentLines.itemIndexes[i])) } @@ -226,7 +226,7 @@ func (m Model[T]) View() string { truncated = m.styleSelection(truncated) } - if !m.wrapText && m.xOffset > 0 && lipgloss.Width(truncated) == 0 && lipgloss.Width(visibleContentLines.lines[i]) > 0 { + if !m.wrapText && m.xOffset > 0 && lipgloss.Width(truncated) == 0 && lipgloss.Width(visibleContentLines.lines[i].Content) > 0 { // if panned right past where line ends, show continuation indicator lineBuffer := linebuffer.New(m.getLineContinuationIndicator()) truncated = lineBuffer.PopLeft(m.width, "", "", lipgloss.NewStyle()) @@ -465,7 +465,8 @@ func (m Model[T]) maxLineWidth() int { maxLineWidth := 0 headerLines := m.getVisibleHeaderLines() visibleContentLines := m.getVisibleContentLines() - allVisibleLines := append(headerLines, visibleContentLines.lines...) + // TODO LEO: put Width as attr on linebuffer and use that directly + allVisibleLines := append(headerLines, linebuffer.ToStrings(visibleContentLines.lines)...) if visibleContentLines.showFooter { allVisibleLines = append(allVisibleLines, m.getTruncatedFooterLine(visibleContentLines)) } @@ -653,7 +654,7 @@ func (m Model[T]) getVisibleHeaderLines() []string { type visibleContentLinesResult struct { // lines is the untruncated visible lines, each corresponding to one terminal row - lines []string + lines []linebuffer.LineBuffer // itemIndexes is the index of the item in allItems that corresponds to each line. len(itemIndexes) == len(lines) itemIndexes []int // showFooter is true if the footer should be shown due to the num visible lines exceeding the vertical space @@ -670,17 +671,17 @@ func (m Model[T]) getVisibleContentLines() visibleContentLinesResult { return visibleContentLinesResult{lines: nil, itemIndexes: nil, showFooter: false} } - var contentLines []string + var contentLines []linebuffer.LineBuffer var itemIndexes []int numLinesAfterHeader := max(0, m.height-len(m.getVisibleHeaderLines())) - addLine := func(l string, itemIndex int) bool { + addLine := func(l linebuffer.LineBuffer, itemIndex int) bool { contentLines = append(contentLines, l) itemIndexes = append(itemIndexes, itemIndex) return len(contentLines) == numLinesAfterHeader } - addLines := func(ls []string, itemIndex int) bool { + addLines := func(ls []linebuffer.LineBuffer, itemIndex int) bool { for i := range ls { if addLine(ls[i], itemIndex) { return true @@ -701,7 +702,7 @@ func (m Model[T]) getVisibleContentLines() visibleContentLinesResult { lb := currItem.Render() // TODO LEO: strings.TrimRightFunc(currItem.String(), unicode.IsSpace), m.width itemLines := lb.WrappedLines(m.width, m.height, m.stringToHighlight, m.highlightStyle(currItemIdx)) offsetLines := safeSliceFromIdx(itemLines, m.topItemLineOffset) - done = addLines(offsetLines, currItemIdx) + done = addLines(linebuffer.ToLineBuffers(offsetLines), currItemIdx) for !done { currItemIdx += 1 @@ -711,18 +712,18 @@ func (m Model[T]) getVisibleContentLines() visibleContentLinesResult { currItem = m.allItems[currItemIdx] lb = currItem.Render() // TODO LEO: strings.TrimRightFunc(currItem.String(), unicode.IsSpace), m.width itemLines = lb.WrappedLines(m.width, m.height, m.stringToHighlight, m.highlightStyle(currItemIdx)) - done = addLines(itemLines, currItemIdx) + done = addLines(linebuffer.ToLineBuffers(itemLines), currItemIdx) } } } else { - done = addLine(currItem.Render().Content, m.width) + done = addLine(currItem.Render(), m.width) for !done { currItemIdx += 1 if currItemIdx >= len(m.allItems) { done = true } else { currItem = m.allItems[currItemIdx] - done = addLine(currItem.Render().Content, currItemIdx) + done = addLine(currItem.Render(), currItemIdx) } } } diff --git a/main.go b/main.go index 326f735..dbc3956 100644 --- a/main.go +++ b/main.go @@ -3,18 +3,23 @@ package main import ( "github.com/robinovitch61/kl/cmd" "log" - "net/http" - _ "net/http/pprof" // This automatically registers the pprof endpoints + "os" + "runtime/pprof" ) func main() { - // Start pprof server on a separate goroutine - go func() { - log.Println("Starting pprof server on :6060") - log.Println(http.ListenAndServe("localhost:6060", nil)) - }() + if cpuProfile := os.Getenv("KL_CPU_PROFILE"); cpuProfile != "" { + f, err := os.Create("cpu.prof") + if err != nil { + log.Fatal("could not create CPU profile: ", err) + } + defer f.Close() + if err := pprof.StartCPUProfile(f); err != nil { + log.Fatal("could not start CPU profile: ", err) + } + defer pprof.StopCPUProfile() + } - // Run your Bubble Tea app as normal err := cmd.Execute() if err != nil { panic(err)