mirror of
https://github.com/zoriya/bubbles.git
synced 2026-08-05 12:46:14 +00:00
* horizontal scroll * rebase branch * add tests * add tests with 2 cells symbols * trimLeft, move to charmbracelete/x/ansi lib * up ansi package * Update viewport/viewport.go Co-authored-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix: do not navigate out to the right * fix: cache line width on setcontent * fix tests * fix viewport tests * add test for preventing right overscroll * chore(viewport): increase horizontal step to 6 * chore(viewport): make horizontal scroll API better match vertical scroll API * fix: nolint * fix: use ansi.Cut * perf: do not cut anything if not needed * feat: expose HorizontalScrollPercent * fix: do not scroll if width is 0 Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix: visible lines take frame into account * feat(viewport): column sign * feat: gutter, soft wrap * wip: search * wip: search * wip: search * fix: perf Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix: rename * wip * wip * refactor: viewport highlight ranges * fix: ligloss update * doc: godoc * feat: fill height optional * fix: handle no content * fix: empty lines * wip * wip * Revert "wip" This reverts commit 933f181e88405c21d65a08b816a88030806ca03e. * Reapply "wip" This reverts commit 0e3e31b70f53d93250a1474547cc5157d50e9237. * fix: wide * fix: wide, find * still not quite there * fix: grapheme width * fix: cleanups * fix: refactors, improves highlight visibility * docs: godoc * chore: lipgloss update Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * chore: x/ansi update Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix: typos, godocs Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix: rename Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix: typo * fix: scroll when soft-wrapping * fix: soft wrap adjustments * fix: update Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix: deps Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> Co-authored-by: Roman Suvorov <suvorov83@gmail.com> Co-authored-by: Roman Suvorov <tty2.rs@gmail.com> Co-authored-by: Christian Rocha <christian@rocha.is>
142 lines
3.1 KiB
Go
142 lines
3.1 KiB
Go
package viewport
|
|
|
|
import (
|
|
"github.com/charmbracelet/lipgloss/v2"
|
|
"github.com/charmbracelet/x/ansi"
|
|
"github.com/rivo/uniseg"
|
|
)
|
|
|
|
// parseMatches converts the given matches into highlight ranges.
|
|
//
|
|
// Assumptions:
|
|
// - matches are measured in bytes, e.g. what [regex.FindAllStringIndex] would return
|
|
// - matches were made against the given content
|
|
// - matches are in order
|
|
// - matches do not overlap
|
|
// - content is line terminated with \n only
|
|
//
|
|
// We'll then convert the ranges into [highlightInfo]s, which hold the starting
|
|
// line and the grapheme positions.
|
|
func parseMatches(
|
|
content string,
|
|
matches [][]int,
|
|
) []highlightInfo {
|
|
if len(matches) == 0 {
|
|
return nil
|
|
}
|
|
|
|
line := 0
|
|
graphemePos := 0
|
|
previousLinesOffset := 0
|
|
bytePos := 0
|
|
|
|
highlights := make([]highlightInfo, 0, len(matches))
|
|
gr := uniseg.NewGraphemes(ansi.Strip(content))
|
|
|
|
for _, match := range matches {
|
|
byteStart, byteEnd := match[0], match[1]
|
|
|
|
// hilight for this match:
|
|
hi := highlightInfo{
|
|
lines: map[int][2]int{},
|
|
}
|
|
|
|
// find the beginning of this byte range, setup current line and
|
|
// grapheme position.
|
|
for byteStart > bytePos {
|
|
if !gr.Next() {
|
|
break
|
|
}
|
|
if content[bytePos] == '\n' {
|
|
previousLinesOffset = graphemePos + 1
|
|
line++
|
|
}
|
|
graphemePos += max(1, gr.Width())
|
|
bytePos += len(gr.Str())
|
|
}
|
|
|
|
hi.lineStart = line
|
|
hi.lineEnd = line
|
|
|
|
graphemeStart := graphemePos
|
|
|
|
// loop until we find the end
|
|
for byteEnd > bytePos {
|
|
if !gr.Next() {
|
|
break
|
|
}
|
|
|
|
// if it ends with a new line, add the range, increase line, and continue
|
|
if content[bytePos] == '\n' {
|
|
colstart := max(0, graphemeStart-previousLinesOffset)
|
|
colend := max(graphemePos-previousLinesOffset+1, colstart) // +1 its \n itself
|
|
|
|
if colend > colstart {
|
|
hi.lines[line] = [2]int{colstart, colend}
|
|
hi.lineEnd = line
|
|
}
|
|
|
|
previousLinesOffset = graphemePos + 1
|
|
line++
|
|
}
|
|
|
|
graphemePos += max(1, gr.Width())
|
|
bytePos += len(gr.Str())
|
|
}
|
|
|
|
// we found it!, add highlight and continue
|
|
if bytePos == byteEnd {
|
|
colstart := max(0, graphemeStart-previousLinesOffset)
|
|
colend := max(graphemePos-previousLinesOffset, colstart)
|
|
|
|
if colend > colstart {
|
|
hi.lines[line] = [2]int{colstart, colend}
|
|
hi.lineEnd = line
|
|
}
|
|
}
|
|
|
|
highlights = append(highlights, hi)
|
|
}
|
|
|
|
return highlights
|
|
}
|
|
|
|
type highlightInfo struct {
|
|
// in which line this highlight starts and ends
|
|
lineStart, lineEnd int
|
|
|
|
// the grapheme highlight ranges for each of these lines
|
|
lines map[int][2]int
|
|
}
|
|
|
|
// coords returns the line x column of this highlight.
|
|
func (hi highlightInfo) coords() (int, int, int) {
|
|
for i := hi.lineStart; i <= hi.lineEnd; i++ {
|
|
hl, ok := hi.lines[i]
|
|
if !ok {
|
|
continue
|
|
}
|
|
return i, hl[0], hl[1]
|
|
}
|
|
return hi.lineStart, 0, 0
|
|
}
|
|
|
|
func makeHighlightRanges(
|
|
highlights []highlightInfo,
|
|
line int,
|
|
style lipgloss.Style,
|
|
) []lipgloss.Range {
|
|
result := []lipgloss.Range{}
|
|
for _, hi := range highlights {
|
|
lihi, ok := hi.lines[line]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if lihi == [2]int{} {
|
|
continue
|
|
}
|
|
result = append(result, lipgloss.NewRange(lihi[0], lihi[1], style))
|
|
}
|
|
return result
|
|
}
|