mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-16 01:43:21 +00:00
feat(viewport): SetLines (#728)
This commit is contained in:
+79
-35
@@ -104,9 +104,12 @@ type Model struct {
|
|||||||
// and [HihglightPrevious] to navigate.
|
// and [HihglightPrevious] to navigate.
|
||||||
SelectedHighlightStyle lipgloss.Style
|
SelectedHighlightStyle lipgloss.Style
|
||||||
|
|
||||||
highlights []highlightInfo
|
// StyleLineFunc allows to return a [lipgloss.Style] for each line.
|
||||||
hiIdx int
|
// The argument is the line index.
|
||||||
memoizedMatchedLines []string
|
StyleLineFunc func(int) lipgloss.Style
|
||||||
|
|
||||||
|
highlights []highlightInfo
|
||||||
|
hiIdx int
|
||||||
}
|
}
|
||||||
|
|
||||||
// GutterFunc can be implemented and set into [Model.LeftGutterFunc].
|
// GutterFunc can be implemented and set into [Model.LeftGutterFunc].
|
||||||
@@ -216,9 +219,16 @@ func (m Model) HorizontalScrollPercent() float64 {
|
|||||||
// Line endings will be normalized to '\n'.
|
// Line endings will be normalized to '\n'.
|
||||||
func (m *Model) SetContent(s string) {
|
func (m *Model) SetContent(s string) {
|
||||||
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
|
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
|
||||||
m.lines = strings.Split(s, "\n")
|
m.SetContentLines(strings.Split(s, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetContentLines allows to set the lines to be shown instead of the content.
|
||||||
|
// If a given line has a \n in it, it'll be considered a [Model.SoftWrap].
|
||||||
|
// See also [Model.SetContent].
|
||||||
|
func (m *Model) SetContentLines(lines []string) {
|
||||||
// if there's no content, set content to actual nil instead of one empty
|
// if there's no content, set content to actual nil instead of one empty
|
||||||
// line.
|
// line.
|
||||||
|
m.lines = lines
|
||||||
if len(m.lines) == 1 && ansi.StringWidth(m.lines[0]) == 0 {
|
if len(m.lines) == 1 && ansi.StringWidth(m.lines[0]) == 0 {
|
||||||
m.lines = nil
|
m.lines = nil
|
||||||
}
|
}
|
||||||
@@ -236,25 +246,38 @@ func (m Model) GetContent() string {
|
|||||||
return strings.Join(m.lines, "\n")
|
return strings.Join(m.lines, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateLine taking soft wrapiing into account, returns the total viewable
|
// calculateLine taking soft wraping into account, returns the total viewable
|
||||||
// lines and the real-line index for the given yoffset.
|
// lines and the real-line index for the given yoffset.
|
||||||
func (m Model) calculateLine(yoffset int) (total, idx int) {
|
func (m Model) calculateLine(yoffset int) (total, idx int) {
|
||||||
if !m.SoftWrap {
|
if !m.SoftWrap {
|
||||||
return len(m.lines), yoffset
|
for i, line := range m.lines {
|
||||||
|
adjust := max(1, lipgloss.Height(line))
|
||||||
|
if yoffset >= total && yoffset < total+adjust {
|
||||||
|
idx = i
|
||||||
|
}
|
||||||
|
total += adjust
|
||||||
|
}
|
||||||
|
if yoffset >= total {
|
||||||
|
idx = len(m.lines)
|
||||||
|
}
|
||||||
|
return total, idx
|
||||||
}
|
}
|
||||||
maxWidth := m.maxWidth()
|
|
||||||
|
|
||||||
|
maxWidth := m.maxWidth()
|
||||||
var gutterSize int
|
var gutterSize int
|
||||||
if m.LeftGutterFunc != nil {
|
if m.LeftGutterFunc != nil {
|
||||||
gutterSize = lipgloss.Width(m.LeftGutterFunc(GutterContext{}))
|
gutterSize = lipgloss.Width(m.LeftGutterFunc(GutterContext{}))
|
||||||
}
|
}
|
||||||
for i, line := range m.lines {
|
for i, line := range m.lines {
|
||||||
adjust := max(1, ansi.StringWidth(line)/(maxWidth-gutterSize))
|
adjust := max(1, lipgloss.Width(line)/(maxWidth-gutterSize))
|
||||||
if yoffset >= total && yoffset < total+adjust {
|
if yoffset >= total && yoffset < total+adjust {
|
||||||
idx = i
|
idx = i
|
||||||
}
|
}
|
||||||
total += adjust
|
total += adjust
|
||||||
}
|
}
|
||||||
|
if yoffset >= total {
|
||||||
|
idx = len(m.lines)
|
||||||
|
}
|
||||||
return total, idx
|
return total, idx
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,6 +333,7 @@ func (m Model) visibleLines() (lines []string) {
|
|||||||
bottom := clamp(pos+maxHeight, top, len(m.lines))
|
bottom := clamp(pos+maxHeight, top, len(m.lines))
|
||||||
lines = make([]string, bottom-top)
|
lines = make([]string, bottom-top)
|
||||||
copy(lines, m.lines[top:bottom])
|
copy(lines, m.lines[top:bottom])
|
||||||
|
lines = m.styleLines(lines, top)
|
||||||
lines = m.highlightLines(lines, top)
|
lines = m.highlightLines(lines, top)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,35 +343,47 @@ func (m Model) visibleLines() (lines []string) {
|
|||||||
|
|
||||||
// if longest line fit within width, no need to do anything else.
|
// if longest line fit within width, no need to do anything else.
|
||||||
if (m.xOffset == 0 && m.longestLineWidth <= maxWidth) || maxWidth == 0 {
|
if (m.xOffset == 0 && m.longestLineWidth <= maxWidth) || maxWidth == 0 {
|
||||||
return m.prependColumn(lines)
|
return m.setupGutter(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.SoftWrap {
|
if m.SoftWrap {
|
||||||
return m.softWrap(lines, maxWidth)
|
return m.softWrap(lines, maxWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range lines {
|
for i, line := range lines {
|
||||||
lines[i] = ansi.Cut(lines[i], m.xOffset, m.xOffset+maxWidth)
|
sublines := strings.Split(line, "\n") // will only have more than 1 if caller used [Model.SetContentLines].
|
||||||
|
for j := range sublines {
|
||||||
|
sublines[j] = ansi.Cut(sublines[j], m.xOffset, m.xOffset+maxWidth)
|
||||||
|
}
|
||||||
|
lines[i] = strings.Join(sublines, "\n")
|
||||||
}
|
}
|
||||||
return m.prependColumn(lines)
|
return m.setupGutter(lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// styleLines styles the lines using [Model.StyleLineFunc].
|
||||||
|
func (m Model) styleLines(lines []string, offset int) []string {
|
||||||
|
if m.StyleLineFunc == nil {
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
for i := range lines {
|
||||||
|
lines[i] = m.StyleLineFunc(i + offset).Render(lines[i])
|
||||||
|
}
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// highlightLines highlights the lines with [Model.HighlightStyle] and
|
||||||
|
// [Model.SelectedHighlightStyle].
|
||||||
func (m Model) highlightLines(lines []string, offset int) []string {
|
func (m Model) highlightLines(lines []string, offset int) []string {
|
||||||
if len(m.highlights) == 0 {
|
if len(m.highlights) == 0 {
|
||||||
return lines
|
return lines
|
||||||
}
|
}
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
if memoized := m.memoizedMatchedLines[i+offset]; memoized != "" {
|
ranges := makeHighlightRanges(
|
||||||
lines[i] = memoized
|
m.highlights,
|
||||||
} else {
|
i+offset,
|
||||||
ranges := makeHighlightRanges(
|
m.HighlightStyle,
|
||||||
m.highlights,
|
)
|
||||||
i+offset,
|
lines[i] = lipgloss.StyleRanges(lines[i], ranges...)
|
||||||
m.HighlightStyle,
|
|
||||||
)
|
|
||||||
lines[i] = lipgloss.StyleRanges(lines[i], ranges...)
|
|
||||||
m.memoizedMatchedLines[i+offset] = lines[i]
|
|
||||||
}
|
|
||||||
if m.hiIdx < 0 {
|
if m.hiIdx < 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -365,6 +401,7 @@ func (m Model) highlightLines(lines []string, offset int) []string {
|
|||||||
|
|
||||||
func (m Model) softWrap(lines []string, maxWidth int) []string {
|
func (m Model) softWrap(lines []string, maxWidth int) []string {
|
||||||
var wrappedLines []string
|
var wrappedLines []string
|
||||||
|
total := m.TotalLineCount()
|
||||||
for i, line := range lines {
|
for i, line := range lines {
|
||||||
idx := 0
|
idx := 0
|
||||||
for ansi.StringWidth(line) >= idx {
|
for ansi.StringWidth(line) >= idx {
|
||||||
@@ -372,7 +409,7 @@ func (m Model) softWrap(lines []string, maxWidth int) []string {
|
|||||||
if m.LeftGutterFunc != nil {
|
if m.LeftGutterFunc != nil {
|
||||||
truncatedLine = m.LeftGutterFunc(GutterContext{
|
truncatedLine = m.LeftGutterFunc(GutterContext{
|
||||||
Index: i + m.YOffset,
|
Index: i + m.YOffset,
|
||||||
TotalLines: m.TotalLineCount(),
|
TotalLines: total,
|
||||||
Soft: idx > 0,
|
Soft: idx > 0,
|
||||||
}) + truncatedLine
|
}) + truncatedLine
|
||||||
}
|
}
|
||||||
@@ -383,16 +420,25 @@ func (m Model) softWrap(lines []string, maxWidth int) []string {
|
|||||||
return wrappedLines
|
return wrappedLines
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) prependColumn(lines []string) []string {
|
// setupGutter sets up the left gutter using [Moddel.LeftGutterFunc].
|
||||||
|
func (m Model) setupGutter(lines []string) []string {
|
||||||
|
if m.LeftGutterFunc == nil {
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := max(0, m.lineToIndex(m.YOffset))
|
||||||
|
total := m.TotalLineCount()
|
||||||
result := make([]string, len(lines))
|
result := make([]string, len(lines))
|
||||||
for i, line := range lines {
|
for i := range lines {
|
||||||
if m.LeftGutterFunc != nil {
|
var line []string
|
||||||
line = m.LeftGutterFunc(GutterContext{
|
for j, realLine := range strings.Split(lines[i], "\n") {
|
||||||
Index: i + m.YOffset,
|
line = append(line, m.LeftGutterFunc(GutterContext{
|
||||||
TotalLines: m.TotalLineCount(),
|
Index: i + offset,
|
||||||
}) + line
|
TotalLines: total,
|
||||||
|
Soft: j > 0,
|
||||||
|
})+realLine)
|
||||||
}
|
}
|
||||||
result[i] = line
|
result[i] = strings.Join(line, "\n")
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -564,7 +610,6 @@ func (m *Model) SetHighlights(matches [][]int) {
|
|||||||
if len(matches) == 0 || len(m.lines) == 0 {
|
if len(matches) == 0 || len(m.lines) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.memoizedMatchedLines = make([]string, len(m.lines))
|
|
||||||
m.highlights = parseMatches(m.GetContent(), matches)
|
m.highlights = parseMatches(m.GetContent(), matches)
|
||||||
m.hiIdx = m.findNearedtMatch()
|
m.hiIdx = m.findNearedtMatch()
|
||||||
m.showHighlight()
|
m.showHighlight()
|
||||||
@@ -572,7 +617,6 @@ func (m *Model) SetHighlights(matches [][]int) {
|
|||||||
|
|
||||||
// ClearHighlights clears previously set highlights.
|
// ClearHighlights clears previously set highlights.
|
||||||
func (m *Model) ClearHighlights() {
|
func (m *Model) ClearHighlights() {
|
||||||
m.memoizedMatchedLines = nil
|
|
||||||
m.highlights = nil
|
m.highlights = nil
|
||||||
m.hiIdx = -1
|
m.hiIdx = -1
|
||||||
}
|
}
|
||||||
@@ -704,7 +748,7 @@ func clamp(v, low, high int) int {
|
|||||||
func maxLineWidth(lines []string) int {
|
func maxLineWidth(lines []string) int {
|
||||||
result := 0
|
result := 0
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
result = max(result, ansi.StringWidth(line))
|
result = max(result, lipgloss.Width(line))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -562,3 +562,33 @@ func testHighlights(tb testing.TB, content string, re *regexp.Regexp, expect []h
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCalculateLine(t *testing.T) {
|
||||||
|
t.Run("simple", func(t *testing.T) {
|
||||||
|
vp := New(WithWidth(40), WithHeight(20))
|
||||||
|
vp.SetContent("foo\nbar")
|
||||||
|
total, idx := vp.calculateLine(0)
|
||||||
|
if total != 2 || idx != 0 {
|
||||||
|
t.Errorf("total: %d, idx: %d", total, idx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("line breaks", func(t *testing.T) {
|
||||||
|
vp := New(WithWidth(40), WithHeight(20))
|
||||||
|
vp.SetContentLines([]string{"new\nbar", "foo", "another line", "multiple\nlines"})
|
||||||
|
total, idx := vp.calculateLine(6)
|
||||||
|
if total != 6 || idx != 4 {
|
||||||
|
t.Errorf("total: %d, idx: %d", total, idx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("soft breaks", func(t *testing.T) {
|
||||||
|
vp := New(WithWidth(40), WithHeight(20))
|
||||||
|
vp.SoftWrap = true
|
||||||
|
vp.SetContent("super long line super long line super long line super long line super long line super long line super long line super long line super long line super long line super long line super long line super long line super\nlong line super long line super long line super long line")
|
||||||
|
total, idx := vp.calculateLine(10)
|
||||||
|
if total != 6 || idx != 2 {
|
||||||
|
t.Errorf("total: %d, idx: %d", total, idx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user