Leave out the text of the rows of a rendering that states its lines

Under a diff renderer that states which diff line each row shows, such
as delta, the queries about the lines of the main view read the rows by
those records alone. Still, they took the text of every row as well,
and for a diff of 300,000 lines this was most of the time each of them
took. Moving the selection by change block runs two of them for every
key press.

Take the records of the rows first, and their text only if no row
states a diff line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller
2026-10-14 08:41:50 +02:00
co-authored by Claude Opus 5.5
parent 3f64902a11
commit dc4fee296c
8 changed files with 73 additions and 10 deletions
+14
View File
@@ -2158,6 +2158,20 @@ func (v *View) DiffLineContents() []DiffLineContent {
return diffLineContentsFrom(v.buf, 0)
}
// DiffLineMetadata is DiffLineContents without the text of the lines, for a reader
// that only needs the records. Taking the text is most of the cost of
// DiffLineContents.
func (v *View) DiffLineMetadata() [][]string {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
metadata := make([][]string, len(v.buf.lines))
for i := range v.buf.lines {
metadata[i] = v.buf.lines[i].diffLineMetadata()
}
return metadata
}
// OffscreenDiffLineContents is DiffLineContents for the content of a re-render in
// progress (see BeginOffscreenRender). A reader deciding where the new content
// should be shown has to work from this: it has to answer before the swap, since
+15
View File
@@ -354,6 +354,21 @@ func TestDiffLineContentsOfARewrittenLine(t *testing.T) {
}, v.DiffLineContents())
}
func TestDiffLineMetadata(t *testing.T) {
v := NewView("name", 0, 0, 80, 10, OutputNormal)
v.writeString(strings.Join([]string{
osc1717("1;d;2;2;foo.txt") + "old2 " + osc1717("1;a;2;;foo.txt") + "new2",
"@@ a hunk header, which carries no record @@",
osc1717("1;c;3;;foo.txt") + "line3",
}, "\n"))
assert.Equal(t, [][]string{
{"1;d;2;2;foo.txt", "1;a;2;;foo.txt"},
nil,
{"1;c;3;;foo.txt"},
}, v.DiffLineMetadata())
}
func TestDiffLineContentsSwallowsHandshake(t *testing.T) {
v := NewView("name", 0, 0, 80, 10, OutputNormal)
@@ -60,7 +60,7 @@ func (self *DiffLineHelper) diffLineIdentitiesAt(
return nil, false
}
contents := view.DiffLineContents()
contents := diffLineContents(view)
if bufferLineIdx >= len(contents) {
return nil, false
}
@@ -9,6 +9,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
// diffFilePrefix marks the start of a file's section in a (possibly multi-file)
@@ -125,6 +126,20 @@ func renderingStatesDiffLines(contents []gocui.DiffLineContent) bool {
})
}
// diffLineContents returns the contents of view's rows, as the queries about the lines
// of a diff read them. A rendering that states which diff line each row shows is read
// by those records alone (see renderingStatesDiffLines), so the text of its rows is
// left out then. Taking the text is most of the cost of reading a long diff.
func diffLineContents(view *gocui.View) []gocui.DiffLineContent {
contents := lo.Map(view.DiffLineMetadata(), func(metadata []string, _ int) gocui.DiffLineContent {
return gocui.DiffLineContent{Metadata: metadata}
})
if renderingStatesDiffLines(contents) {
return contents
}
return view.DiffLineContents()
}
// parseDiffLineRecords parses the records a row carries, left to right, leaving out
// the ones we don't understand. A row carries more than one when the rendering puts
// two diff lines on it, as a side-by-side row does with a deletion and the addition
@@ -544,3 +544,22 @@ func TestParseDiffLineIdentities(t *testing.T) {
}
})
}
func TestDiffLineContentsLeaveOutTheTextOfARenderingWithRecords(t *testing.T) {
record := func(payload string) string { return "\x1b]1717;" + payload + "\x1b\\" }
rendering := gocui.NewView("rendering", 0, 0, 80, 10, gocui.OutputNormal)
rendering.WriteString(record("1;a;1;;foo.txt") + "added\n" + "a divider\n")
assert.Equal(t, []gocui.DiffLineContent{
{Metadata: []string{"1;a;1;;foo.txt"}},
{},
}, diffLineContents(rendering))
// A diff without records is read by its text, so it keeps it.
diff := gocui.NewView("diff", 0, 0, 80, 10, gocui.OutputNormal)
diff.WriteString("diff --git a/foo.txt b/foo.txt\n+added\n")
assert.Equal(t, []gocui.DiffLineContent{
{Text: "diff --git a/foo.txt b/foo.txt"},
{Text: "+added"},
}, diffLineContents(diff))
}
@@ -28,7 +28,7 @@ import (
// beside the addition replacing it — and all of them are reported: what the user
// pointed at is the row, so everything on it is selected.
func (self *DiffLineHelper) DiffLinesInBufferRange(view *gocui.View, first int, last int) []types.DiffLineInfo {
identities := self.resolveDiffLineIdentities(view.DiffLineContents())
identities := self.resolveDiffLineIdentities(diffLineContents(view))
infos := []types.DiffLineInfo{}
for bufferLine := first; bufferLine <= min(last, len(identities)-1); bufferLine++ {
@@ -136,7 +136,7 @@ func (self *DiffLineHelper) ChangeLinesInBufferRange(view *gocui.View, first int
// header, or a row whose identity couldn't be recovered. Those are the rows a
// selection is anchored on and navigation moves between.
func (self *DiffLineHelper) changeLines(view *gocui.View) []bool {
resolved := self.resolveDiffLines(view.DiffLineContents())
resolved := self.resolveDiffLines(diffLineContents(view))
isChange := make([]bool, len(resolved))
for i, r := range resolved {
isChange[i] = r.ok && r.info.IsChange()
@@ -244,7 +244,7 @@ func (self *DiffLineHelper) IsSingleHunkForWholeFile(view *gocui.View, changeVie
if !ok {
return false
}
resolved := self.resolveDiffLines(view.DiffLineContents())
resolved := self.resolveDiffLines(diffLineContents(view))
if anchor >= len(resolved) || !resolved[anchor].ok {
return false
}
@@ -375,7 +375,7 @@ func (self *DiffLineHelper) StartOfFileInDiff(view *gocui.View, path string) (in
// was materialized into, so that both halves of a change belong to the same file however
// the diff renderer states them.
func (self *DiffLineHelper) filePaths(view *gocui.View) []string {
resolved := self.resolveDiffLines(view.DiffLineContents())
resolved := self.resolveDiffLines(diffLineContents(view))
toRepoTerms := self.repoTermsMapper(view)
paths := make([]string, len(resolved))
for i, row := range resolved {
@@ -151,7 +151,7 @@ func (self *DiffLineHelper) selectionLine(
if len(candidates) == 0 {
return anchorViewLine
}
_, bufferLine := self.nearestSurvivingCandidate(view.DiffLineContents(), candidates)
_, bufferLine := self.nearestSurvivingCandidate(diffLineContents(view), candidates)
if bufferLine == -1 {
return anchorViewLine
}
@@ -208,7 +208,7 @@ func selectionFarEndViewLine(view *gocui.View) (int, bool, bool) {
// findDiffLine returns the view line showing the given diff line in what view is
// displaying now, for placing a remembered line once the re-render is on screen.
func (self *DiffLineHelper) findDiffLine(view *gocui.View, identity types.DiffLineInfo) (int, bool) {
bufferLine, ok := self.patchLineRows(view.DiffLineContents())[patchLineOf(identity)]
bufferLine, ok := self.patchLineRows(diffLineContents(view))[patchLineOf(identity)]
if !ok {
return 0, false
}
@@ -273,7 +273,7 @@ func (self *DiffLineHelper) restoreDiffLinePositionOnRerender(
// place it left behind is the same one that identity used to have.
func (self *DiffLineHelper) ChangeLineOrdinal(view *gocui.View, bufferLine int) int {
ordinal := 0
for i, row := range self.resolveDiffLines(view.DiffLineContents()) {
for i, row := range self.resolveDiffLines(diffLineContents(view)) {
if i >= bufferLine {
break
}
@@ -421,7 +421,7 @@ func (self *DiffLineHelper) nearbyDiffLines(view *gocui.View, anchorViewLine int
if !ok {
return nil
}
resolved := self.resolveDiffLines(view.DiffLineContents())
resolved := self.resolveDiffLines(diffLineContents(view))
if anchor >= len(resolved) {
return nil
}
@@ -222,7 +222,7 @@ func (self *DiffLineHelper) RefreshInclusionGutter() {
return
}
resolved := self.resolveDiffLines(view.DiffLineContents())
resolved := self.resolveDiffLines(diffLineContents(view))
marks := make([]bool, len(resolved))
for i, row := range resolved {
if row.ok && row.info.IsChange() {