mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Bundle a view's cell buffer and write state into a viewBuffer
The fields that make up a view's content and the act of writing to it — the cell buffer (lines), the write cursor (wx/wy), the escape-sequence decoder (ei) and the held-newline flag (pendingNewline) — were loose fields on View. Bundle them into a viewBuffer struct that View holds by pointer. This is a behaviour-preserving prep refactor: every access just goes through v.buf now. It sets up rendering into a second, off-screen viewBuffer that can be swapped in atomically, so an async re-render never exposes a half-written buffer to readers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
114d18dc04
commit
2a6cb8d78e
|
|
@ -1497,7 +1497,7 @@ func (g *Gui) drawSubtitle(v *View, fgColor, bgColor Attribute) error {
|
|||
|
||||
// drawListFooter draws the footer of a list view, showing something like '1 of 10'
|
||||
func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error {
|
||||
if len(v.lines) == 0 {
|
||||
if len(v.buf.lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1747,13 +1747,13 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
|||
if newY < 0 {
|
||||
newY = 0
|
||||
newCy = -v.oy
|
||||
} else if newY >= len(v.lines) {
|
||||
newY = len(v.lines) - 1
|
||||
} else if newY >= len(v.buf.lines) {
|
||||
newY = len(v.buf.lines) - 1
|
||||
newCy = newY - v.oy
|
||||
}
|
||||
|
||||
visibleLineWidth := 0
|
||||
for _, c := range v.lines[newY].cells {
|
||||
for _, c := range v.buf.lines[newY].cells {
|
||||
visibleLineWidth += c.width
|
||||
}
|
||||
if visibleLineWidth < newX {
|
||||
|
|
|
|||
|
|
@ -25,17 +25,44 @@ const (
|
|||
RIGHT = 8 // view is overlapping at right edge
|
||||
)
|
||||
|
||||
// viewBuffer holds a view's content as cells, together with the cursor and
|
||||
// escape-sequence decoder state used to turn incoming bytes into those cells.
|
||||
// A view normally has a single buffer (the one it displays), but bundling this
|
||||
// state lets a re-render build a second, off-screen buffer and swap it in
|
||||
// atomically once the new content is ready, so no reader ever sees a
|
||||
// half-written buffer.
|
||||
type viewBuffer struct {
|
||||
// the view's content: one []cell per unwrapped line
|
||||
lines []lineType
|
||||
|
||||
// write cursor into lines
|
||||
wx, wy int
|
||||
|
||||
// decodes ESC sequences as bytes are written
|
||||
ei *escapeInterpreter
|
||||
|
||||
// If the last character written was a newline, we don't write it but instead
|
||||
// set pendingNewline to true. If more text is written, we write the newline
|
||||
// then. This avoids an extra blank line at the end of the view.
|
||||
pendingNewline bool
|
||||
}
|
||||
|
||||
// A View is a window. It maintains its own internal buffer and cursor
|
||||
// position.
|
||||
type View struct {
|
||||
name string
|
||||
x0, y0, x1, y1 int // left top right bottom
|
||||
ox, oy int // view offsets
|
||||
cx, cy int // cursor position
|
||||
rx, ry int // Read() offsets
|
||||
wx, wy int // Write() offsets
|
||||
lines []lineType // All the data
|
||||
x0, y0, x1, y1 int // left top right bottom
|
||||
ox, oy int // view offsets
|
||||
cx, cy int // cursor position
|
||||
rx, ry int // Read() offsets
|
||||
outMode OutputMode
|
||||
|
||||
// buf bundles the view's cell buffer and the cursor / escape-parser state
|
||||
// used to write into it (see the viewBuffer type). Bundling these makes it
|
||||
// possible to build a second, off-screen buffer during a re-render and swap
|
||||
// it in atomically once ready, so no reader ever sees a half-written buffer.
|
||||
buf *viewBuffer
|
||||
|
||||
// The y position of the first line of a range selection.
|
||||
// This is not relative to the view's origin: it is relative to the first line
|
||||
// of the view's content, so you can scroll the view and this value will remain
|
||||
|
|
@ -74,17 +101,9 @@ type View struct {
|
|||
// true and viewLines to nil
|
||||
viewLines []viewLine
|
||||
|
||||
// If the last character written was a newline, we don't write it but
|
||||
// instead set pendingNewline to true. If more text is written, we write the
|
||||
// newline then. This is to avoid having an extra blank at the end of the view.
|
||||
pendingNewline bool
|
||||
|
||||
// writeMutex protects locks the write process
|
||||
writeMutex sync.Mutex
|
||||
|
||||
// ei is used to decode ESC sequences on Write
|
||||
ei *escapeInterpreter
|
||||
|
||||
// Visible specifies whether the view is visible.
|
||||
Visible bool
|
||||
|
||||
|
|
@ -461,7 +480,7 @@ type SearchPosition struct {
|
|||
}
|
||||
|
||||
type viewLine struct {
|
||||
linesX, linesY int // coordinates relative to v.lines
|
||||
linesX, linesY int // coordinates relative to v.buf.lines
|
||||
line []cell
|
||||
|
||||
// Colors used to extend the bg past this wrapped segment's content.
|
||||
|
|
@ -470,7 +489,7 @@ type viewLine struct {
|
|||
trailingFillAttributes *trailingFillAttributes
|
||||
}
|
||||
|
||||
// lineType is one of v.lines: the cells of a source line, plus optional
|
||||
// lineType is one of v.buf.lines: the cells of a source line, plus optional
|
||||
// trailingFillAttributes recording the colors used to extend the bg
|
||||
// past the line's content when the writer emitted '\x1b[K'.
|
||||
type lineType struct {
|
||||
|
|
@ -536,7 +555,7 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
|
|||
Editor: DefaultEditor,
|
||||
tainted: true,
|
||||
outMode: mode,
|
||||
ei: newEscapeInterpreter(mode),
|
||||
buf: &viewBuffer{ei: newEscapeInterpreter(mode)},
|
||||
searcher: &searcher{},
|
||||
TextArea: &TextArea{},
|
||||
rangeSelectStartY: -1,
|
||||
|
|
@ -547,7 +566,7 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
|
|||
v.SelFgColor, v.SelBgColor = ColorDefault, ColorDefault
|
||||
v.InactiveViewSelBgColor = ColorDefault
|
||||
v.TitleColor, v.FrameColor = ColorDefault, ColorDefault
|
||||
v.ei.screenColMax = v.InnerWidth()
|
||||
v.buf.ei.screenColMax = v.InnerWidth()
|
||||
return v
|
||||
}
|
||||
|
||||
|
|
@ -558,7 +577,7 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
|
|||
// content can consult this snapshot instead of reading the view's live
|
||||
// dimensions (which the UI thread mutates during layout).
|
||||
func (v *View) SetContentWidth(width int) {
|
||||
v.ei.screenColMax = width
|
||||
v.buf.ei.screenColMax = width
|
||||
}
|
||||
|
||||
// Dimensions returns the dimensions of the View
|
||||
|
|
@ -748,16 +767,16 @@ func (v *View) SetWritePos(x, y int) {
|
|||
y = 0
|
||||
}
|
||||
|
||||
v.wx = x
|
||||
v.wy = y
|
||||
v.buf.wx = x
|
||||
v.buf.wy = y
|
||||
|
||||
// Changing the write position makes a pending newline obsolete
|
||||
v.pendingNewline = false
|
||||
v.buf.pendingNewline = false
|
||||
}
|
||||
|
||||
// WritePos returns the current write position of the view's internal buffer.
|
||||
func (v *View) WritePos() (x, y int) {
|
||||
return v.wx, v.wy
|
||||
return v.buf.wx, v.buf.wy
|
||||
}
|
||||
|
||||
// SetReadPos sets the read position of the view's internal buffer.
|
||||
|
|
@ -785,52 +804,52 @@ func (v *View) makeWriteable(x, y int) {
|
|||
// TODO: make this more efficient
|
||||
|
||||
// line `y` must be index-able (that's why `<=`)
|
||||
for len(v.lines) <= y {
|
||||
if cap(v.lines) > len(v.lines) {
|
||||
newLen := cap(v.lines)
|
||||
for len(v.buf.lines) <= y {
|
||||
if cap(v.buf.lines) > len(v.buf.lines) {
|
||||
newLen := cap(v.buf.lines)
|
||||
if newLen > y {
|
||||
newLen = y + 1
|
||||
}
|
||||
v.lines = v.lines[:newLen]
|
||||
v.buf.lines = v.buf.lines[:newLen]
|
||||
} else {
|
||||
v.lines = append(v.lines, lineType{})
|
||||
v.buf.lines = append(v.buf.lines, lineType{})
|
||||
}
|
||||
}
|
||||
// cell `x` need not be index-able (that's why `<`)
|
||||
// append should be used by `lines[y]` user if he wants to write beyond `x`
|
||||
for len(v.lines[y].cells) < x {
|
||||
if cap(v.lines[y].cells) > len(v.lines[y].cells) {
|
||||
newLen := cap(v.lines[y].cells)
|
||||
for len(v.buf.lines[y].cells) < x {
|
||||
if cap(v.buf.lines[y].cells) > len(v.buf.lines[y].cells) {
|
||||
newLen := cap(v.buf.lines[y].cells)
|
||||
if newLen > x {
|
||||
newLen = x
|
||||
}
|
||||
v.lines[y].cells = v.lines[y].cells[:newLen]
|
||||
v.buf.lines[y].cells = v.buf.lines[y].cells[:newLen]
|
||||
} else {
|
||||
v.lines[y].cells = append(v.lines[y].cells, cell{})
|
||||
v.buf.lines[y].cells = append(v.buf.lines[y].cells, cell{})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writeCells copies []cell to (v.wx, v.wy), and advances v.wx accordingly.
|
||||
// writeCells copies []cell to (v.buf.wx, v.buf.wy), and advances v.buf.wx accordingly.
|
||||
// !!! caller MUST ensure that specified location (x, y) is writeable by calling makeWriteable
|
||||
func (v *View) writeCells(cells []cell) {
|
||||
var newLen int
|
||||
// use maximum len available
|
||||
line := v.lines[v.wy].cells[:cap(v.lines[v.wy].cells)]
|
||||
maxCopy := len(line) - v.wx
|
||||
line := v.buf.lines[v.buf.wy].cells[:cap(v.buf.lines[v.buf.wy].cells)]
|
||||
maxCopy := len(line) - v.buf.wx
|
||||
if maxCopy < len(cells) {
|
||||
copy(line[v.wx:], cells[:maxCopy])
|
||||
copy(line[v.buf.wx:], cells[:maxCopy])
|
||||
line = append(line, cells[maxCopy:]...)
|
||||
newLen = len(line)
|
||||
} else { // maxCopy >= len(cells)
|
||||
copy(line[v.wx:], cells)
|
||||
newLen = v.wx + len(cells)
|
||||
if newLen < len(v.lines[v.wy].cells) {
|
||||
newLen = len(v.lines[v.wy].cells)
|
||||
copy(line[v.buf.wx:], cells)
|
||||
newLen = v.buf.wx + len(cells)
|
||||
if newLen < len(v.buf.lines[v.buf.wy].cells) {
|
||||
newLen = len(v.buf.lines[v.buf.wy].cells)
|
||||
}
|
||||
}
|
||||
v.lines[v.wy].cells = line[:newLen]
|
||||
v.wx += len(cells)
|
||||
v.buf.lines[v.buf.wy].cells = line[:newLen]
|
||||
v.buf.wx += len(cells)
|
||||
}
|
||||
|
||||
// Write appends a byte slice into the view's internal buffer. Because
|
||||
|
|
@ -848,35 +867,35 @@ func (v *View) Write(p []byte) (n int, err error) {
|
|||
|
||||
func (v *View) write(p []byte) {
|
||||
v.tainted = true
|
||||
// write only ever touches lines from v.wy onwards, so any cached wrapping
|
||||
// write only ever touches lines from v.buf.wy onwards, so any cached wrapping
|
||||
// below that stays valid.
|
||||
v.firstDirtyLine = min(v.firstDirtyLine, v.wy)
|
||||
v.firstDirtyLine = min(v.firstDirtyLine, v.buf.wy)
|
||||
v.clearHover()
|
||||
|
||||
// Fill with empty cells, if writing outside current view buffer
|
||||
v.makeWriteable(v.wx, v.wy)
|
||||
v.makeWriteable(v.buf.wx, v.buf.wy)
|
||||
|
||||
finishLine := func() {
|
||||
v.autoRenderHyperlinksInCurrentLine()
|
||||
}
|
||||
|
||||
advanceToNextLine := func() {
|
||||
v.wx = 0
|
||||
v.wy++
|
||||
if v.wy >= len(v.lines) {
|
||||
v.lines = append(v.lines, lineType{})
|
||||
v.buf.wx = 0
|
||||
v.buf.wy++
|
||||
if v.buf.wy >= len(v.buf.lines) {
|
||||
v.buf.lines = append(v.buf.lines, lineType{})
|
||||
}
|
||||
}
|
||||
|
||||
if v.pendingNewline {
|
||||
if v.buf.pendingNewline {
|
||||
advanceToNextLine()
|
||||
v.ei.notifyRowAdvance()
|
||||
v.pendingNewline = false
|
||||
v.buf.ei.notifyRowAdvance()
|
||||
v.buf.pendingNewline = false
|
||||
}
|
||||
|
||||
until := len(p)
|
||||
if !v.Editable && until > 0 && p[until-1] == '\n' {
|
||||
v.pendingNewline = true
|
||||
v.buf.pendingNewline = true
|
||||
until--
|
||||
}
|
||||
|
||||
|
|
@ -892,15 +911,15 @@ func (v *View) write(p []byte) {
|
|||
case characterEquals(chr, '\n') || isCRLF(chr):
|
||||
finishLine()
|
||||
advanceToNextLine()
|
||||
v.ei.notifyRowAdvance()
|
||||
v.buf.ei.notifyRowAdvance()
|
||||
case characterEquals(chr, '\r'):
|
||||
finishLine()
|
||||
v.wx = 0
|
||||
v.ei.notifyColumnReset()
|
||||
v.buf.wx = 0
|
||||
v.buf.ei.notifyColumnReset()
|
||||
default:
|
||||
truncateLine, cells := v.parseInput(chr, width, v.wx, v.wy)
|
||||
if cd, ok := v.ei.instruction.(cursorDown); ok {
|
||||
v.ei.instructionRead()
|
||||
truncateLine, cells := v.parseInput(chr, width, v.buf.wx, v.buf.wy)
|
||||
if cd, ok := v.buf.ei.instruction.(cursorDown); ok {
|
||||
v.buf.ei.instructionRead()
|
||||
for range cd.n {
|
||||
v.autoRenderHyperlinksInCurrentLine()
|
||||
advanceToNextLine()
|
||||
|
|
@ -911,7 +930,7 @@ func (v *View) write(p []byte) {
|
|||
}
|
||||
v.writeCells(cells)
|
||||
if truncateLine {
|
||||
v.lines[v.wy].cells = v.lines[v.wy].cells[:v.wx]
|
||||
v.buf.lines[v.buf.wy].cells = v.buf.lines[v.buf.wy].cells[:v.buf.wx]
|
||||
}
|
||||
// Soft-wrap tracking. truncateLine is true exactly when the
|
||||
// cells are from \x1b[K filling to end of line — ConPTY
|
||||
|
|
@ -922,12 +941,12 @@ func (v *View) write(p []byte) {
|
|||
for _, c := range cells {
|
||||
totalWidth += c.width
|
||||
}
|
||||
v.ei.notifyCellsWritten(totalWidth)
|
||||
v.buf.ei.notifyCellsWritten(totalWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if v.pendingNewline {
|
||||
if v.buf.pendingNewline {
|
||||
finishLine()
|
||||
} else {
|
||||
v.autoRenderHyperlinksInCurrentLine()
|
||||
|
|
@ -981,7 +1000,7 @@ func (v *View) autoRenderHyperlinksInCurrentLine() {
|
|||
return
|
||||
}
|
||||
|
||||
line := v.lines[v.wy].cells
|
||||
line := v.buf.lines[v.buf.wy].cells
|
||||
start := 0
|
||||
for {
|
||||
linkStart := findLinkStart(line[start:])
|
||||
|
|
@ -998,7 +1017,7 @@ func (v *View) autoRenderHyperlinksInCurrentLine() {
|
|||
link.WriteString(line[linkEnd].chr)
|
||||
}
|
||||
for i := linkStart; i < linkEnd; i++ {
|
||||
v.lines[v.wy].cells[i].hyperlink = link.String()
|
||||
v.buf.lines[v.buf.wy].cells[i].hyperlink = link.String()
|
||||
}
|
||||
start = linkEnd
|
||||
}
|
||||
|
|
@ -1011,9 +1030,9 @@ func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) {
|
|||
cells := []cell{}
|
||||
truncateLine := false
|
||||
|
||||
isEscape, err := v.ei.parseOne(ch)
|
||||
isEscape, err := v.buf.ei.parseOne(ch)
|
||||
if err != nil {
|
||||
for _, chr := range v.ei.characters() {
|
||||
for _, chr := range v.buf.ei.characters() {
|
||||
c := cell{
|
||||
fgColor: v.FgColor,
|
||||
bgColor: v.BgColor,
|
||||
|
|
@ -1022,28 +1041,28 @@ func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) {
|
|||
}
|
||||
cells = append(cells, c)
|
||||
}
|
||||
v.ei.reset()
|
||||
v.buf.ei.reset()
|
||||
} else {
|
||||
repeatCount := 1
|
||||
if _, ok := v.ei.instruction.(eraseInLineFromCursor); ok {
|
||||
if _, ok := v.buf.ei.instruction.(eraseInLineFromCursor); ok {
|
||||
// Discard any old content past the cursor and record the
|
||||
// fill colors so draw() paints the trailing area with them.
|
||||
// This extends the bg to the right edge in both the
|
||||
// content-fits and content-wraps cases — for the latter,
|
||||
// the metadata is what reaches every wrapped segment past
|
||||
// the last word.
|
||||
v.ei.instructionRead()
|
||||
v.buf.ei.instructionRead()
|
||||
truncateLine = true
|
||||
v.lines[v.wy].trailingFillAttributes = &trailingFillAttributes{
|
||||
fg: v.ei.curFgColor,
|
||||
bg: v.ei.curBgColor,
|
||||
v.buf.lines[v.buf.wy].trailingFillAttributes = &trailingFillAttributes{
|
||||
fg: v.buf.ei.curFgColor,
|
||||
bg: v.buf.ei.curBgColor,
|
||||
}
|
||||
return truncateLine, []cell{}
|
||||
} else if cf, ok := v.ei.instruction.(cursorForward); ok {
|
||||
} else if cf, ok := v.buf.ei.instruction.(cursorForward); ok {
|
||||
// emit `n` space cells under the parser-tracked SGR — used
|
||||
// to materialize ConPTY's compressed runs of spaces (which
|
||||
// it emits as ECH+CUF instead of literal whitespace).
|
||||
v.ei.instructionRead()
|
||||
v.buf.ei.instructionRead()
|
||||
repeatCount = cf.n
|
||||
ch = []byte{' '}
|
||||
width = 1
|
||||
|
|
@ -1061,9 +1080,9 @@ func (v *View) parseInput(ch []byte, width int, x int, _ int) (bool, []cell) {
|
|||
repeatCount = tabWidth - (x % tabWidth)
|
||||
}
|
||||
c := cell{
|
||||
fgColor: v.ei.curFgColor,
|
||||
bgColor: v.ei.curBgColor,
|
||||
hyperlink: v.ei.hyperlink.String(),
|
||||
fgColor: v.buf.ei.curFgColor,
|
||||
bgColor: v.buf.ei.curBgColor,
|
||||
hyperlink: v.buf.ei.hyperlink.String(),
|
||||
chr: string(ch),
|
||||
width: width,
|
||||
}
|
||||
|
|
@ -1091,9 +1110,9 @@ func (v *View) Read(p []byte) (n int, err error) {
|
|||
}
|
||||
v.readBuffer = nil
|
||||
}
|
||||
for v.ry < len(v.lines) {
|
||||
for v.rx < len(v.lines[v.ry].cells) {
|
||||
s := v.lines[v.ry].cells[v.rx].chr
|
||||
for v.ry < len(v.buf.lines) {
|
||||
for v.rx < len(v.buf.lines[v.ry].cells) {
|
||||
s := v.buf.lines[v.ry].cells[v.rx].chr
|
||||
count := len(s)
|
||||
copy(p[offset:], s)
|
||||
v.rx++
|
||||
|
|
@ -1115,7 +1134,7 @@ func (v *View) Read(p []byte) (n int, err error) {
|
|||
// only use this if the calling function has a lock on writeMutex
|
||||
func (v *View) clear() {
|
||||
v.rewind()
|
||||
v.lines = nil
|
||||
v.buf.lines = nil
|
||||
v.clearViewLines()
|
||||
}
|
||||
|
||||
|
|
@ -1157,7 +1176,7 @@ func (v *View) CopyContent(from *View) {
|
|||
// This is a shallow clone -- the per-row cell data is immutable once written
|
||||
// and stays shared, so the cost is proportional to the number of rows, not
|
||||
// their contents.
|
||||
v.lines = slices.Clone(from.lines)
|
||||
v.buf.lines = slices.Clone(from.buf.lines)
|
||||
v.viewLines = slices.Clone(from.viewLines)
|
||||
v.SetOriginX(from.ox)
|
||||
v.SetOriginY(from.oy)
|
||||
|
|
@ -1180,13 +1199,13 @@ func (v *View) Reset() {
|
|||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.rewind()
|
||||
v.lines = nil
|
||||
v.buf.lines = nil
|
||||
}
|
||||
|
||||
// This is for when we've done a restart for the sake of avoiding a flicker and
|
||||
// we've reached the end of the new content to display: we need to clear the remaining
|
||||
// content from the previous round. We do this by setting v.viewLines to nil so that
|
||||
// we just render the new content from v.lines directly
|
||||
// we just render the new content from v.buf.lines directly
|
||||
func (v *View) FlushStaleCells() {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
|
@ -1195,8 +1214,8 @@ func (v *View) FlushStaleCells() {
|
|||
}
|
||||
|
||||
func (v *View) rewind() {
|
||||
v.ei.reset()
|
||||
v.ei.resetScreenCursor()
|
||||
v.buf.ei.reset()
|
||||
v.buf.ei.resetScreenCursor()
|
||||
|
||||
v.SetReadPos(0, 0)
|
||||
v.SetWritePos(0, 0)
|
||||
|
|
@ -1268,14 +1287,14 @@ func (v *View) updateSearchPositions() {
|
|||
for _, result := range v.searcher.modelSearchResults {
|
||||
// This code only works when v.Wrap is false.
|
||||
|
||||
if result.Y >= len(v.lines) {
|
||||
if result.Y >= len(v.buf.lines) {
|
||||
break
|
||||
}
|
||||
|
||||
// If a view line exists for this line index:
|
||||
if v.lines[result.Y].cells != nil {
|
||||
if v.buf.lines[result.Y].cells != nil {
|
||||
// search this view line for the search string
|
||||
positions := searchPositionsForLine(v.lines[result.Y].cells, result.Y)
|
||||
positions := searchPositionsForLine(v.buf.lines[result.Y].cells, result.Y)
|
||||
if len(positions) > 0 {
|
||||
// If we found any occurrences, add them
|
||||
v.searcher.searchPositions = append(v.searcher.searchPositions, positions...)
|
||||
|
|
@ -1422,7 +1441,7 @@ func (v *View) refreshViewLinesIfNeeded() {
|
|||
}
|
||||
|
||||
lineIdx := 0
|
||||
lines := v.lines
|
||||
lines := v.buf.lines
|
||||
for i := range lines {
|
||||
line := &lines[i]
|
||||
|
||||
|
|
@ -1546,8 +1565,8 @@ func (v *View) BufferLines() []string {
|
|||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
lines := make([]string, len(v.lines))
|
||||
for i, l := range v.lines {
|
||||
lines := make([]string, len(v.buf.lines))
|
||||
for i, l := range v.buf.lines {
|
||||
lines[i] = l.cells.String()
|
||||
}
|
||||
return lines
|
||||
|
|
@ -1559,7 +1578,7 @@ func (v *View) Buffer() string {
|
|||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
return linesToString(v.lines)
|
||||
return linesToString(v.buf.lines)
|
||||
}
|
||||
|
||||
// ViewBufferLines returns the lines in the view's internal
|
||||
|
|
@ -1579,7 +1598,7 @@ func (v *View) ViewBufferLines() []string {
|
|||
|
||||
// LinesHeight is the count of view lines (i.e. lines excluding wrapping)
|
||||
func (v *View) LinesHeight() int {
|
||||
return len(v.lines)
|
||||
return len(v.buf.lines)
|
||||
}
|
||||
|
||||
// ViewLinesHeight is the count of view lines (i.e. lines including wrapping)
|
||||
|
|
@ -1610,11 +1629,11 @@ func (v *View) Line(y int) (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
if y < 0 || y >= len(v.lines) {
|
||||
if y < 0 || y >= len(v.buf.lines) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return v.lines[y].cells.String(), true
|
||||
return v.buf.lines[y].cells.String(), true
|
||||
}
|
||||
|
||||
// Word returns a string with the word of the view's internal buffer
|
||||
|
|
@ -1625,11 +1644,11 @@ func (v *View) Word(x, y int) (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
if x < 0 || y < 0 || y >= len(v.lines) || x >= len(v.lines[y].cells) {
|
||||
if x < 0 || y < 0 || y >= len(v.buf.lines) || x >= len(v.buf.lines[y].cells) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
str := v.lines[y].cells.String()
|
||||
str := v.buf.lines[y].cells.String()
|
||||
|
||||
nl := strings.LastIndexFunc(str[:x], indexFunc)
|
||||
if nl == -1 {
|
||||
|
|
@ -1655,12 +1674,12 @@ func indexFunc(r rune) bool {
|
|||
// SetHighlight toggles highlighting of separate lines, for custom lists
|
||||
// or multiple selection in views.
|
||||
func (v *View) SetHighlight(y int, on bool) {
|
||||
if y < 0 || y >= len(v.lines) {
|
||||
if y < 0 || y >= len(v.buf.lines) {
|
||||
return
|
||||
}
|
||||
|
||||
cells := make([]cell, 0, len(v.lines[y].cells))
|
||||
for _, c := range v.lines[y].cells {
|
||||
cells := make([]cell, 0, len(v.buf.lines[y].cells))
|
||||
for _, c := range v.buf.lines[y].cells {
|
||||
if on {
|
||||
c.bgColor = v.SelBgColor
|
||||
c.fgColor = v.SelFgColor
|
||||
|
|
@ -1672,7 +1691,7 @@ func (v *View) SetHighlight(y int, on bool) {
|
|||
}
|
||||
v.tainted = true
|
||||
v.firstDirtyLine = min(v.firstDirtyLine, y)
|
||||
v.lines[y].cells = cells
|
||||
v.buf.lines[y].cells = cells
|
||||
v.clearHover()
|
||||
}
|
||||
|
||||
|
|
@ -1784,7 +1803,7 @@ func (v *View) SelectedLine() string {
|
|||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
if len(v.lines) == 0 {
|
||||
if len(v.buf.lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
|
|
@ -1796,7 +1815,7 @@ func (v *View) SelectedLines() []string {
|
|||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
if len(v.lines) == 0 {
|
||||
if len(v.buf.lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -1811,7 +1830,7 @@ func (v *View) SelectedLines() []string {
|
|||
}
|
||||
|
||||
func (v *View) lineContentAtIdx(idx int) string {
|
||||
return v.lines[idx].cells.String()
|
||||
return v.buf.lines[idx].cells.String()
|
||||
}
|
||||
|
||||
func (v *View) SelectedPoint() (int, int) {
|
||||
|
|
@ -1884,8 +1903,8 @@ func (v *View) ClearTextArea() {
|
|||
|
||||
func (v *View) overwriteLines(y int, content string) {
|
||||
// break by newline, then for each line, write it, then add that erase command
|
||||
v.wx = 0
|
||||
v.wy = y
|
||||
v.buf.wx = 0
|
||||
v.buf.wy = y
|
||||
v.clearViewLines()
|
||||
|
||||
lines := strings.ReplaceAll(content, "\n", "\x1b[K\n")
|
||||
|
|
@ -1897,7 +1916,7 @@ func (v *View) overwriteLines(y int, content string) {
|
|||
v.writeString(lines)
|
||||
}
|
||||
|
||||
// only call this function if you don't care where v.wx and v.wy end up
|
||||
// only call this function if you don't care where v.buf.wx and v.buf.wy end up
|
||||
func (v *View) OverwriteLines(y int, content string) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
|
@ -1905,7 +1924,7 @@ func (v *View) OverwriteLines(y int, content string) {
|
|||
v.overwriteLines(y, content)
|
||||
}
|
||||
|
||||
// only call this function if you don't care where v.wx and v.wy end up
|
||||
// only call this function if you don't care where v.buf.wx and v.buf.wy end up
|
||||
func (v *View) OverwriteLinesAndClearEverythingElse(lineCount int, y int, content string) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
|
@ -1915,11 +1934,11 @@ func (v *View) OverwriteLinesAndClearEverythingElse(lineCount int, y int, conten
|
|||
v.overwriteLines(y, content)
|
||||
|
||||
for i := range y {
|
||||
v.lines[i] = lineType{}
|
||||
v.buf.lines[i] = lineType{}
|
||||
}
|
||||
|
||||
for i := v.wy + 1; i < len(v.lines); i += 1 {
|
||||
v.lines[i] = lineType{}
|
||||
for i := v.buf.wy + 1; i < len(v.buf.lines); i += 1 {
|
||||
v.buf.lines[i] = lineType{}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1927,7 +1946,7 @@ func (v *View) setContentLineCount(lineCount int) {
|
|||
if lineCount > 0 {
|
||||
v.makeWriteable(0, lineCount-1)
|
||||
}
|
||||
v.lines = v.lines[:lineCount]
|
||||
v.buf.lines = v.buf.lines[:lineCount]
|
||||
}
|
||||
|
||||
// If the current search result is no longer visible after a scroll up, select the last search
|
||||
|
|
@ -2061,7 +2080,7 @@ func (v *View) scrollMargin() int {
|
|||
// Returns true if the view contains a line containing the given text with the given
|
||||
// foreground color
|
||||
func (v *View) ContainsColoredText(fgColor string, text string) bool {
|
||||
for _, line := range v.lines {
|
||||
for _, line := range v.buf.lines {
|
||||
if containsColoredTextInLine(fgColor, text, line.cells) {
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,13 +101,13 @@ func TestWriteString(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
v := NewView("name", 0, 0, 10, 10, OutputNormal)
|
||||
for _, l := range test.existingLines {
|
||||
v.lines = append(v.lines, lineType{cells: stringToCells(l)})
|
||||
v.buf.lines = append(v.buf.lines, lineType{cells: stringToCells(l)})
|
||||
}
|
||||
for _, s := range test.stringsToWrite {
|
||||
v.writeString(s)
|
||||
}
|
||||
var resultingLines [][]string
|
||||
for _, l := range v.lines {
|
||||
for _, l := range v.buf.lines {
|
||||
resultingLines = append(resultingLines, cellsToStrings(l.cells))
|
||||
}
|
||||
assert.Equal(t, test.expectedLines, resultingLines)
|
||||
|
|
@ -144,19 +144,19 @@ func TestAutoRenderingHyperlinks(t *testing.T) {
|
|||
|
||||
v.writeString("htt")
|
||||
// No hyperlinks are generated for incomplete URLs
|
||||
assert.Equal(t, "", v.lines[0].cells[0].hyperlink)
|
||||
assert.Equal(t, "", v.buf.lines[0].cells[0].hyperlink)
|
||||
// Writing more characters to the same line makes the link complete (even
|
||||
// though we didn't see a newline yet)
|
||||
v.writeString("ps://example.com")
|
||||
assert.Equal(t, "https://example.com", v.lines[0].cells[0].hyperlink)
|
||||
assert.Equal(t, "https://example.com", v.buf.lines[0].cells[0].hyperlink)
|
||||
|
||||
v.Clear()
|
||||
// Valid but incomplete URL
|
||||
v.writeString("https://exa")
|
||||
assert.Equal(t, "https://exa", v.lines[0].cells[0].hyperlink)
|
||||
assert.Equal(t, "https://exa", v.buf.lines[0].cells[0].hyperlink)
|
||||
// Writing more characters to the same fixes the link
|
||||
v.writeString("mple.com")
|
||||
assert.Equal(t, "https://example.com", v.lines[0].cells[0].hyperlink)
|
||||
assert.Equal(t, "https://example.com", v.buf.lines[0].cells[0].hyperlink)
|
||||
}
|
||||
|
||||
func TestContainsColoredText(t *testing.T) {
|
||||
|
|
@ -233,7 +233,7 @@ func TestContainsColoredText(t *testing.T) {
|
|||
for j, cells := range test.lines {
|
||||
lines[j] = lineType{cells: cells}
|
||||
}
|
||||
v := &View{lines: lines}
|
||||
v := &View{buf: &viewBuffer{lines: lines}}
|
||||
assert.Equal(t, test.expected, v.ContainsColoredText(test.fgColorStr, test.text), "Test %d failed", i)
|
||||
}
|
||||
}
|
||||
|
|
@ -248,8 +248,8 @@ func TestWriteCursorPositionEscape(t *testing.T) {
|
|||
// "a", then "skip to row 3" (i.e. one blank row), then "b".
|
||||
v.writeString("a\r\n\x1b[3;1Hb\r\n")
|
||||
|
||||
got := make([][]string, 0, len(v.lines))
|
||||
for _, l := range v.lines {
|
||||
got := make([][]string, 0, len(v.buf.lines))
|
||||
for _, l := range v.buf.lines {
|
||||
got = append(got, cellsToStrings(l.cells))
|
||||
}
|
||||
|
||||
|
|
@ -269,8 +269,8 @@ func TestWriteCursorPositionEscapeAcrossWrites(t *testing.T) {
|
|||
// ConPTY is on row 3 here; CUP to row 5 should skip exactly one row.
|
||||
v.writeString("c\x1b[5;1Hd\n")
|
||||
|
||||
got := make([][]string, 0, len(v.lines))
|
||||
for _, l := range v.lines {
|
||||
got := make([][]string, 0, len(v.buf.lines))
|
||||
for _, l := range v.buf.lines {
|
||||
got = append(got, cellsToStrings(l.cells))
|
||||
}
|
||||
assert.Equal(t, [][]string{
|
||||
|
|
@ -292,8 +292,8 @@ func TestWriteCursorForwardEscape(t *testing.T) {
|
|||
// "a" + ECH 5 + CUF 5 + "b" — visually "a b".
|
||||
v.writeString("a\x1b[5X\x1b[5Cb\n")
|
||||
|
||||
got := make([][]string, 0, len(v.lines))
|
||||
for _, l := range v.lines {
|
||||
got := make([][]string, 0, len(v.buf.lines))
|
||||
for _, l := range v.buf.lines {
|
||||
got = append(got, cellsToStrings(l.cells))
|
||||
}
|
||||
|
||||
|
|
@ -312,8 +312,8 @@ func TestWriteCursorPositionEscapeWithSoftWraps(t *testing.T) {
|
|||
v.writeString("abcdefghij\n")
|
||||
v.writeString("\x1b[4;1Hxyz\n")
|
||||
|
||||
got := make([][]string, 0, len(v.lines))
|
||||
for _, l := range v.lines {
|
||||
got := make([][]string, 0, len(v.buf.lines))
|
||||
for _, l := range v.buf.lines {
|
||||
got = append(got, cellsToStrings(l.cells))
|
||||
}
|
||||
assert.Equal(t, [][]string{
|
||||
|
|
|
|||
Loading…
Reference in a new issue