Skip to content

Commit

Permalink
progress
Browse files Browse the repository at this point in the history
  • Loading branch information
robinovitch61 committed Dec 26, 2024
1 parent 2c6cc47 commit b9eea5e
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 73 deletions.
52 changes: 24 additions & 28 deletions internal/linebuffer/linebuffer.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,27 @@ import (
// LineBuffer provides functionality to get sequential strings of a specified terminal width, accounting
// for the ansi escape codes styling the line.
type LineBuffer struct {
line string // line with ansi codes. utf-8 bytes
width int // width in terminal cells (not bytes or runes)
continuation string // indicator for line continuation, e.g. "..."
toHighlight string // string to highlight using highlightStyle
highlightStyle lipgloss.Style // style for toHighlight
leftRuneIdx int // left plaintext rune idx to start next PopLeft result from
lineRunes []rune // runes of line
runeIdxToByteOffset []int // idx of lineRunes to byte offset. len(runeIdxToByteOffset) == len(lineRunes)
lineNoAnsi string // line without ansi codes. utf-8 bytes
lineNoAnsiRunes []rune // runes of lineNoAnsi. len(lineNoAnsiRunes) == len(lineNoAnsiWidths)
lineNoAnsiWidths []int // terminal cell widths of lineNoAnsi. len(lineNoAnsiWidths) == len(lineNoAnsiRunes)
lineNoAnsiCumWidths []int // cumulative lineNoAnsiWidths
continuationRunes []rune // runes of continuation
continuationWidths []int // terminal cell widths of continuation
ansiCodeIndexes [][]int // slice of startByte, endByte indexes of ansi codes in the line
line string // line with ansi codes. utf-8 bytes
width int // width in terminal cells (not bytes or runes)
continuation string // indicator for line continuation, e.g. "..."
leftRuneIdx int // left plaintext rune idx to start next PopLeft result from
lineRunes []rune // runes of line
runeIdxToByteOffset []int // idx of lineRunes to byte offset. len(runeIdxToByteOffset) == len(lineRunes)
lineNoAnsi string // line without ansi codes. utf-8 bytes
lineNoAnsiRunes []rune // runes of lineNoAnsi. len(lineNoAnsiRunes) == len(lineNoAnsiWidths)
lineNoAnsiWidths []int // terminal cell widths of lineNoAnsi. len(lineNoAnsiWidths) == len(lineNoAnsiRunes)
lineNoAnsiCumWidths []int // cumulative lineNoAnsiWidths
continuationRunes []rune // runes of continuation
continuationWidths []int // terminal cell widths of continuation
ansiCodeIndexes [][]int // slice of startByte, endByte indexes of ansi codes in the line
}

func New(line string, width int, continuation string, toHighlight string, highlightStyle lipgloss.Style) LineBuffer {
func New(line string, width int, continuation string) LineBuffer {
lb := LineBuffer{
line: line,
width: width,
continuation: continuation,
toHighlight: stripAnsi(toHighlight),
highlightStyle: highlightStyle,
leftRuneIdx: 0,
line: line,
width: width,
continuation: continuation,
leftRuneIdx: 0,
}

if len(constants.AnsiRegex.FindAllStringIndex(continuation, -1)) > 0 {
Expand Down Expand Up @@ -126,7 +122,7 @@ func (l *LineBuffer) SeekToWidth(w int) {
}

// PopLeft returns a string of the buffer's width from its current left offset, scrolling the left offset to the right
func (l *LineBuffer) PopLeft() string {
func (l *LineBuffer) PopLeft(toHighlight string, highlightStyle lipgloss.Style) string {
if l.leftRuneIdx >= len(l.lineNoAnsiRunes) || l.width == 0 {
return ""
}
Expand Down Expand Up @@ -164,17 +160,17 @@ func (l *LineBuffer) PopLeft() string {
res = reapplyANSI(l.line, res, startByteOffset, l.ansiCodeIndexes)
}

res = highlightLine(res, l.toHighlight, l.highlightStyle, 0, len(res))
res = highlightLine(res, toHighlight, highlightStyle, 0, len(res))

if left, endIdx := overflowsLeft(l.lineNoAnsi, startByteOffset, l.toHighlight); left {
if left, endIdx := overflowsLeft(l.lineNoAnsi, startByteOffset, toHighlight); left {
highlightLeft := l.lineNoAnsi[startByteOffset:endIdx]
res = highlightLine(res, highlightLeft, l.highlightStyle, 0, len(highlightLeft))
res = highlightLine(res, highlightLeft, highlightStyle, 0, len(highlightLeft))
}
endByteOffset := l.runeIdxToByteOffset[l.leftRuneIdx]
if right, startIdx := overflowsRight(l.lineNoAnsi, endByteOffset, l.toHighlight); right {
if right, startIdx := overflowsRight(l.lineNoAnsi, endByteOffset, toHighlight); right {
highlightRight := l.lineNoAnsi[startIdx:endByteOffset]
lenPlainTextRes := len(stripAnsi(res))
res = highlightLine(res, highlightRight, l.highlightStyle, lenPlainTextRes-len(highlightRight), lenPlainTextRes)
res = highlightLine(res, highlightRight, highlightStyle, lenPlainTextRes-len(highlightRight), lenPlainTextRes)
}

return res
Expand Down
6 changes: 3 additions & 3 deletions internal/linebuffer/linebuffer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func TestTotalLines(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
lb := New(tt.s, tt.width, tt.continuation, "", lipgloss.NewStyle())
lb := New(tt.s, tt.width, tt.continuation)
if lb.TotalLines() != tt.expected {
t.Fatalf("expected %d, got %d", tt.expected, lb.TotalLines())
}
Expand Down Expand Up @@ -568,9 +568,9 @@ func TestPopLeft(t *testing.T) {
if len(tt.expected) != tt.numPopLefts {
t.Fatalf("num expected != num popLefts")
}
lb := New(tt.s, tt.width, tt.continuation, tt.toHighlight, highlightStyle)
lb := New(tt.s, tt.width, tt.continuation)
for i := 0; i < tt.numPopLefts; i++ {
actual := lb.PopLeft()
actual := lb.PopLeft(tt.toHighlight, highlightStyle)
util.CmpStr(t, tt.expected[i], actual)
}
})
Expand Down
83 changes: 41 additions & 42 deletions internal/viewport/viewport.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ func (m Model[T]) View() string {

visibleHeaderLines := m.getVisibleHeaderLines()
for i := range visibleHeaderLines {
viewString += m.truncateNoXOffset(visibleHeaderLines[i]) + "\n"
lineBuffer := linebuffer.New(visibleHeaderLines[i], m.width, "")
viewString += lineBuffer.PopLeft("", lipgloss.NewStyle()) + "\n"
}

// get the lines to show based on the vertical scroll position (topItemIdx and topItemLineOffset)
Expand All @@ -213,14 +214,18 @@ func (m Model[T]) View() string {
isSelection := m.selectionEnabled && visibleContentLines.itemIndexes[i] == m.selectedItemIdx
if isSelection {
// style entire selected line
// TODO LEO: can at least move this lower
visibleContentLines.lines[i] = m.styleSelection(visibleContentLines.lines[i])
}

truncated := m.truncate(visibleContentLines.lines[i])
lineBuffer := linebuffer.New(visibleContentLines.lines[i], m.width, m.continuationIndicator)
lineBuffer.SeekToWidth(m.xOffset)
truncated := lineBuffer.PopLeft(m.stringToHighlight, m.highlightStyle(visibleContentLines.itemIndexes[i]))

if m.xOffset > 0 && lipgloss.Width(truncated) == 0 && lipgloss.Width(visibleContentLines.lines[i]) > 0 {
// if panned right past where line ends, show continuation indicator
truncated = m.truncateNoXOffset(m.getLineContinuationIndicator())
lineBuffer := linebuffer.New(m.getLineContinuationIndicator(), m.width, "")
truncated = lineBuffer.PopLeft("", lipgloss.NewStyle())
if isSelection {
truncated = m.styleSelection(truncated)
}
Expand Down Expand Up @@ -443,7 +448,13 @@ func (m *Model[T]) ScrollSoItemIdxInView(itemIdx int) {
}
}

func (m Model[T]) wrap(line string, width int, maxLinesEachEnd int) []string {
func (m Model[T]) wrap(
line string,
width int,
maxLinesEachEnd int,
toHighlight string,
toHighlightStyle lipgloss.Style,
) []string {
if width <= 0 {
return []string{}
}
Expand All @@ -463,33 +474,21 @@ func (m Model[T]) wrap(line string, width int, maxLinesEachEnd int) []string {
}

var res []string
// TODO LEO: highlight style changes if selected
lineBuffer := linebuffer.New(line, m.width, "", m.stringToHighlight, m.HighlightStyle)
//lineWidth := lineBuffer.fullLineWidth()
lineBuffer := linebuffer.New(line, width, "")
totalLines := lineBuffer.TotalLines()

if maxLinesEachEnd > 0 && totalLines > maxLinesEachEnd*2 {
//for xOffset := 0; xOffset < width*maxLinesEachEnd; xOffset += width {
// res = append(res, lineBuffer.Truncate(xOffset, width))
//}
for nLines := 0; nLines < maxLinesEachEnd; nLines++ {
res = append(res, lineBuffer.PopLeft())
res = append(res, lineBuffer.PopLeft(toHighlight, toHighlightStyle))
}

//startOffset := lineWidth - (maxLinesEachEnd * width)
//for xOffset := startOffset; xOffset < lineWidth; xOffset += width {
// res = append(res, lineBuffer.Truncate(xOffset, width))
//}
lineBuffer.SeekToLine(totalLines - maxLinesEachEnd)
for nLines := 0; nLines < maxLinesEachEnd; nLines++ {
res = append(res, lineBuffer.PopLeft())
res = append(res, lineBuffer.PopLeft(toHighlight, toHighlightStyle))
}
} else {
//for xOffset := 0; xOffset < lineWidth; xOffset += width {
// res = append(res, lineBuffer.Truncate(xOffset, width))
//}
for nLines := 0; nLines < totalLines; nLines++ {
res = append(res, lineBuffer.PopLeft())
res = append(res, lineBuffer.PopLeft(toHighlight, toHighlightStyle))
}
}

Expand All @@ -516,7 +515,7 @@ func (m Model[T]) numLinesForItem(itemIdx int) int {
if len(m.allItems) == 0 || itemIdx < 0 || itemIdx >= len(m.allItems) {
return 0
}
return len(m.wrap(m.allItems[itemIdx].Render(), m.width, m.height))
return len(m.wrap(m.allItems[itemIdx].Render(), m.width, m.height, "", lipgloss.NewStyle()))
}

func (m *Model[T]) safelySetXOffset(n int) {
Expand Down Expand Up @@ -675,7 +674,7 @@ func (m Model[T]) getVisibleHeaderLines() []string {
// wrapped
var wrappedHeaderLines []string
for _, s := range m.header {
wrappedHeaderLines = append(wrappedHeaderLines, m.wrap(s, m.width, m.height)...)
wrappedHeaderLines = append(wrappedHeaderLines, m.wrap(s, m.width, m.height, "", lipgloss.NewStyle())...)
}
return safeSliceUpToIdx(wrappedHeaderLines, m.height)
}
Expand Down Expand Up @@ -725,7 +724,7 @@ func (m Model[T]) getVisibleContentLines() visibleContentLinesResult {
}

if m.wrapText {
itemLines := m.wrap(currItem.Render(), m.width, m.height)
itemLines := m.wrap(currItem.Render(), m.width, m.height, m.stringToHighlight, m.highlightStyle(currItemIdx))
offsetLines := safeSliceFromIdx(itemLines, m.topItemLineOffset)
done = addLines(offsetLines, currItemIdx)

Expand All @@ -735,7 +734,7 @@ func (m Model[T]) getVisibleContentLines() visibleContentLinesResult {
done = true
} else {
currItem = m.allItems[currItemIdx]
itemLines = m.wrap(currItem.Render(), m.width, m.height)
itemLines = m.wrap(currItem.Render(), m.width, m.height, m.stringToHighlight, m.highlightStyle(currItemIdx))
done = addLines(itemLines, currItemIdx)
}
}
Expand Down Expand Up @@ -777,6 +776,13 @@ func (m Model[T]) getVisibleContentLines() visibleContentLinesResult {
return visibleContentLinesResult{lines: contentLines, itemIndexes: itemIndexes, showFooter: showFooter}
}

func (m Model[T]) highlightStyle(itemIdx int) lipgloss.Style {
if m.selectionEnabled && itemIdx == m.selectedItemIdx {
return m.HighlightStyleIfSelected
}
return m.HighlightStyle
}

func (m Model[T]) getTruncatedFooterLine(visibleContentLines visibleContentLinesResult) string {
numerator := m.selectedItemIdx + 1 // 0th line is 1st
denominator := len(m.allItems)
Expand All @@ -800,8 +806,8 @@ func (m Model[T]) getTruncatedFooterLine(visibleContentLines visibleContentLines
footerString := fmt.Sprintf("%d%% (%d/%d)", percentScrolled, numerator, denominator)
// use m.continuationIndicator regardless of wrapText

footerBuffer := linebuffer.New(footerString, m.width, m.continuationIndicator, "", lipgloss.NewStyle())
return m.FooterStyle.Render(footerBuffer.PopLeft())
footerBuffer := linebuffer.New(footerString, m.width, m.continuationIndicator)
return m.FooterStyle.Render(footerBuffer.PopLeft("", lipgloss.NewStyle()))
}

func (m Model[T]) getLineContinuationIndicator() string {
Expand Down Expand Up @@ -889,22 +895,14 @@ func (m Model[T]) maxItemIdxAndMaxTopLineOffset() (int, int) {
}

// truncate truncates a line to fit within the viewport's width, accounting for the current xOffset (left/right) position
func (m Model[T]) truncate(line string) string {
//lineBuffer := linebuffer.New(line, m.continuationIndicator)
//return lineBuffer.Truncate(m.xOffset, m.width)
// TODO LEO: highlight style fix if selected
lineBuffer := linebuffer.New(line, m.width, m.continuationIndicator, m.stringToHighlight, m.HighlightStyle)
lineBuffer.SeekToWidth(m.xOffset)
return lineBuffer.PopLeft()
}

func (m Model[T]) truncateNoXOffset(line string) string {
//lineBuffer := linebuffer.New(line, m.continuationIndicator)
//return lineBuffer.Truncate(0, m.width)
// TODO LEO: highlight style fix if selected
lineBuffer := linebuffer.New(line, m.width, m.continuationIndicator, m.stringToHighlight, m.HighlightStyle)
return lineBuffer.PopLeft()
}
//func (m Model[T]) truncate(line string) string {
// //lineBuffer := linebuffer.New(line, m.continuationIndicator)
// //return lineBuffer.Truncate(m.xOffset, m.width)
// // TODO LEO: highlight style fix if selected
// lineBuffer := linebuffer.New(line, m.width, m.continuationIndicator)
// lineBuffer.SeekToWidth(m.xOffset)
// return lineBuffer.PopLeft(m.stringToHighlight, m.HighlightStyle)
//}

func (m Model[T]) getNumVisibleItems() int {
if !m.wrapText {
Expand All @@ -920,6 +918,7 @@ func (m Model[T]) getNumVisibleItems() int {
}
}

// TODO LEO: can avoid this now, or simplify?
func (m Model[T]) styleSelection(s string) string {
split := surroundingAnsiRegex.Split(s, -1)
matches := surroundingAnsiRegex.FindAllString(s, -1)
Expand Down

0 comments on commit b9eea5e

Please sign in to comment.