mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-24 02:24:25 -05:00
Cache each line's wrapping so scrolling doesn't re-wrap the whole buffer
refreshViewLinesIfNeeded re-wrapped every line of the buffer whenever the view was tainted. That's cheap for short content, but scrolling a long diff calls it constantly: adjustDownwardScrollAmount queries ViewLinesHeight on every scroll event, and each newly-read line taints the view, so every notch re-wrapped the entire buffer. Wrapping measures each cell's width (uniseg) and allocates per line, so once you'd scrolled far enough down the diff, scrolling turned sluggish - the cost grew with how much had been read. (A CPU profile of scrolling deep in a long diff put 77% of the time in lineWrap, reached almost entirely via ViewLinesHeight rather than draw.) Cache each line's wrapped result on the lineType, keyed by the width it was wrapped at, and only re-wrap lines that have actually changed since the last refresh. A firstDirtyLine index, updated in the same three places that set `tainted` (write, clearViewLines' callers, SetHighlight), marks the lowest line that might have changed; lines below it with a matching cached width reuse their cached wrapping. The cache lives on the line, so it's freed with the line when the view's content is replaced (e.g. selecting a different commit) - it doesn't accumulate across a session. The wrapping cost per scroll now scales with the number of lines just read, not with the total size of the buffer, so scrolling stays smooth no matter how far down you are. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
73d7b443ec
commit
585c7f126d
+79
-41
@@ -50,6 +50,14 @@ type View struct {
|
|||||||
// tained is true if the viewLines must be updated
|
// tained is true if the viewLines must be updated
|
||||||
tainted bool
|
tainted bool
|
||||||
|
|
||||||
|
// firstDirtyLine is the index of the lowest line in `lines` that has been
|
||||||
|
// written to or highlighted since viewLines was last refreshed, and whose
|
||||||
|
// cached wrapping (lineType.wrappedCells) may therefore be stale. Lines
|
||||||
|
// below it are unchanged and can reuse their cached wrapping instead of
|
||||||
|
// being re-wrapped, which keeps refreshViewLinesIfNeeded cheap while
|
||||||
|
// scrolling appends new lines to a long buffer.
|
||||||
|
firstDirtyLine int
|
||||||
|
|
||||||
// the last position that the mouse was hovering over; nil if the mouse is outside of
|
// the last position that the mouse was hovering over; nil if the mouse is outside of
|
||||||
// this view, or not hovering over a cell
|
// this view, or not hovering over a cell
|
||||||
lastHoverPosition *pos
|
lastHoverPosition *pos
|
||||||
@@ -457,6 +465,16 @@ type viewLine struct {
|
|||||||
type lineType struct {
|
type lineType struct {
|
||||||
cells cells
|
cells cells
|
||||||
trailingFillAttributes *trailingFillAttributes
|
trailingFillAttributes *trailingFillAttributes
|
||||||
|
|
||||||
|
// wrappedCells caches the result of wrapping `cells` to `wrappedColumns`
|
||||||
|
// columns, so that unchanged lines don't have to be re-wrapped on every
|
||||||
|
// refreshViewLinesIfNeeded (which runs on every scroll event, via
|
||||||
|
// ViewLinesHeight). Wrapping measures every cell's width and allocates, so
|
||||||
|
// for a long buffer that dominates the cost of scrolling. The cache is used
|
||||||
|
// only for lines below View.firstDirtyLine whose wrappedColumns still
|
||||||
|
// matches the current width; nil means nothing is cached yet.
|
||||||
|
wrappedCells [][]cell
|
||||||
|
wrappedColumns int
|
||||||
}
|
}
|
||||||
|
|
||||||
// trailingFillAttributes describes the fg/bg colors that draw() should
|
// trailingFillAttributes describes the fg/bg colors that draw() should
|
||||||
@@ -815,6 +833,9 @@ func (v *View) Write(p []byte) (n int, err error) {
|
|||||||
|
|
||||||
func (v *View) write(p []byte) {
|
func (v *View) write(p []byte) {
|
||||||
v.tainted = true
|
v.tainted = true
|
||||||
|
// write only ever touches lines from v.wy onwards, so any cached wrapping
|
||||||
|
// below that stays valid.
|
||||||
|
v.firstDirtyLine = min(v.firstDirtyLine, v.wy)
|
||||||
v.clearHover()
|
v.clearHover()
|
||||||
|
|
||||||
// Fill with empty cells, if writing outside current view buffer
|
// Fill with empty cells, if writing outside current view buffer
|
||||||
@@ -1358,48 +1379,64 @@ func (v *View) draw() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) refreshViewLinesIfNeeded() {
|
func (v *View) refreshViewLinesIfNeeded() {
|
||||||
if v.tainted {
|
if !v.tainted {
|
||||||
maxX := v.InnerWidth()
|
return
|
||||||
lineIdx := 0
|
|
||||||
lines := v.lines
|
|
||||||
for i, line := range lines {
|
|
||||||
wrap := 0
|
|
||||||
if v.Wrap {
|
|
||||||
wrap = maxX
|
|
||||||
}
|
|
||||||
|
|
||||||
ls := lineWrap(line.cells, wrap)
|
|
||||||
for j := range ls {
|
|
||||||
// Per-segment trailing fill. When the source line opted in
|
|
||||||
// via '\x1b[K', the LAST wrapped segment uses those colors
|
|
||||||
// directly; earlier segments use the colors of their own
|
|
||||||
// last cell, so the trailing area matches the bg active
|
|
||||||
// where that segment ended rather than bleeding the
|
|
||||||
// '\x1b[K' bg back across color changes in the line.
|
|
||||||
var attrs *trailingFillAttributes
|
|
||||||
if line.trailingFillAttributes != nil {
|
|
||||||
if j == len(ls)-1 {
|
|
||||||
attrs = line.trailingFillAttributes
|
|
||||||
} else if len(ls[j]) > 0 {
|
|
||||||
last := ls[j][len(ls[j])-1]
|
|
||||||
attrs = &trailingFillAttributes{fg: last.fgColor, bg: last.bgColor}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vline := viewLine{
|
|
||||||
linesX: j, linesY: i, line: ls[j],
|
|
||||||
trailingFillAttributes: attrs,
|
|
||||||
}
|
|
||||||
|
|
||||||
if lineIdx > len(v.viewLines)-1 {
|
|
||||||
v.viewLines = append(v.viewLines, vline)
|
|
||||||
} else {
|
|
||||||
v.viewLines[lineIdx] = vline
|
|
||||||
}
|
|
||||||
lineIdx++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
v.tainted = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
maxX := v.InnerWidth()
|
||||||
|
wrap := 0
|
||||||
|
if v.Wrap {
|
||||||
|
wrap = maxX
|
||||||
|
}
|
||||||
|
|
||||||
|
lineIdx := 0
|
||||||
|
lines := v.lines
|
||||||
|
for i := range lines {
|
||||||
|
line := &lines[i]
|
||||||
|
|
||||||
|
// Reuse the previously wrapped result for lines that haven't changed
|
||||||
|
// since the last refresh (i.e. below firstDirtyLine) and were wrapped at
|
||||||
|
// the current width. Wrapping is expensive and this loop runs on every
|
||||||
|
// scroll event, so only the lines that were actually just read (or
|
||||||
|
// re-highlighted) should be wrapped afresh.
|
||||||
|
if line.wrappedCells == nil || line.wrappedColumns != wrap || i >= v.firstDirtyLine {
|
||||||
|
line.wrappedCells = lineWrap(line.cells, wrap)
|
||||||
|
line.wrappedColumns = wrap
|
||||||
|
}
|
||||||
|
ls := line.wrappedCells
|
||||||
|
|
||||||
|
for j := range ls {
|
||||||
|
// Per-segment trailing fill. When the source line opted in
|
||||||
|
// via '\x1b[K', the LAST wrapped segment uses those colors
|
||||||
|
// directly; earlier segments use the colors of their own
|
||||||
|
// last cell, so the trailing area matches the bg active
|
||||||
|
// where that segment ended rather than bleeding the
|
||||||
|
// '\x1b[K' bg back across color changes in the line.
|
||||||
|
var attrs *trailingFillAttributes
|
||||||
|
if line.trailingFillAttributes != nil {
|
||||||
|
if j == len(ls)-1 {
|
||||||
|
attrs = line.trailingFillAttributes
|
||||||
|
} else if len(ls[j]) > 0 {
|
||||||
|
last := ls[j][len(ls[j])-1]
|
||||||
|
attrs = &trailingFillAttributes{fg: last.fgColor, bg: last.bgColor}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
vline := viewLine{
|
||||||
|
linesX: j, linesY: i, line: ls[j],
|
||||||
|
trailingFillAttributes: attrs,
|
||||||
|
}
|
||||||
|
|
||||||
|
if lineIdx > len(v.viewLines)-1 {
|
||||||
|
v.viewLines = append(v.viewLines, vline)
|
||||||
|
} else {
|
||||||
|
v.viewLines[lineIdx] = vline
|
||||||
|
}
|
||||||
|
lineIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.firstDirtyLine = len(lines)
|
||||||
|
v.tainted = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// if autoscroll is enabled but we only have a single row of cells shown to the
|
// if autoscroll is enabled but we only have a single row of cells shown to the
|
||||||
@@ -1599,6 +1636,7 @@ func (v *View) SetHighlight(y int, on bool) {
|
|||||||
cells = append(cells, c)
|
cells = append(cells, c)
|
||||||
}
|
}
|
||||||
v.tainted = true
|
v.tainted = true
|
||||||
|
v.firstDirtyLine = min(v.firstDirtyLine, y)
|
||||||
v.lines[y].cells = cells
|
v.lines[y].cells = cells
|
||||||
v.clearHover()
|
v.clearHover()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user