From 80dff3c2b968b539decdffe462dee6ebad497af6 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Oct 2026 17:25:41 +0200 Subject: [PATCH 1/5] Number the lines of a diff in one pass when parsing it With no diff renderer configured, opening the jump-to-file menu for the diff of a commit that adds or deletes a file of 100,000 lines takes more than 20 seconds, and so does picking a file from it. In such a diff, moving the selection by change block takes as long for each key press. All this time the UI is frozen. To find out which line of which file each row of the main view shows, lazygit parses the view's content as a diff. For every line of a file's diff it asks the parsed patch for the line's number in the new and in the old file, and each of these calls counts the lines of the hunk before it. So parsing takes time quadratic in the length of a hunk, and the diff of a new or deleted file is a single hunk. For a diff of 200,000 lines, one parse took about 10 seconds, and opening the menu parses it twice. A diff renderer that states which line each row shows, such as delta, doesn't go through this parse. Number all lines of the patch in one pass instead. Parsing that diff now takes about 40 milliseconds. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/commands/patch/patch.go | 29 +++++++++++++++++ pkg/commands/patch/patch_test.go | 31 +++++++++++++++++++ .../controllers/helpers/diff_line_parser.go | 5 +-- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/pkg/commands/patch/patch.go b/pkg/commands/patch/patch.go index 767f3cd89..74742f282 100644 --- a/pkg/commands/patch/patch.go +++ b/pkg/commands/patch/patch.go @@ -183,6 +183,35 @@ func (self *Patch) OldLineNumberOfLine(idx int) int { return hunk.oldStart + offset } +// LineNumbersOfLines returns what LineNumberOfLine and OldLineNumberOfLine return for +// each line of the patch. It numbers all of them in one pass, whereas calling those +// two for every line takes time quadratic in the length of a hunk. +func (self *Patch) LineNumbersOfLines() (newLineNumbers []int, oldLineNumbers []int) { + newLineNumbers = make([]int, 0, self.LineCount()) + oldLineNumbers = make([]int, 0, self.LineCount()) + for range self.header { + newLineNumbers = append(newLineNumbers, 1) + oldLineNumbers = append(oldLineNumbers, 1) + } + for _, hunk := range self.hunks { + newLineNumber, oldLineNumber := hunk.newStart, hunk.oldStart + // The hunk's header line has the numbers of the hunk's first line. + newLineNumbers = append(newLineNumbers, newLineNumber) + oldLineNumbers = append(oldLineNumbers, oldLineNumber) + for _, line := range hunk.bodyLines { + newLineNumbers = append(newLineNumbers, newLineNumber) + oldLineNumbers = append(oldLineNumbers, oldLineNumber) + if line.Kind == ADDITION || line.Kind == CONTEXT { + newLineNumber++ + } + if line.Kind == DELETION || line.Kind == CONTEXT { + oldLineNumber++ + } + } + } + return newLineNumbers, oldLineNumbers +} + // Returns hunk index containing the line at the given patch line index func (self *Patch) HunkContainingLine(idx int) int { for hunkIdx, hunk := range self.hunks { diff --git a/pkg/commands/patch/patch_test.go b/pkg/commands/patch/patch_test.go index e22876b19..dffceece2 100644 --- a/pkg/commands/patch/patch_test.go +++ b/pkg/commands/patch/patch_test.go @@ -838,6 +838,37 @@ func TestOldLineNumberOfLine(t *testing.T) { } } +func TestLineNumbersOfLines(t *testing.T) { + scenarios := []struct { + testName string + patchStr string + }{ + {"simpleDiff", simpleDiff}, + {"twoHunks", twoHunks}, + {"twoHunksWithMoreAdditionsThanRemovals", twoHunksWithMoreAdditionsThanRemovals}, + {"twoChangesInOneHunk", twoChangesInOneHunk}, + {"consecutiveDeletions", consecutiveDeletions}, + {"renameWithModificationDiff", renameWithModificationDiff}, + {"addNewlineToEndOfFile", addNewlineToEndOfFile}, + {"newFile", newFile}, + {"deletedFile", deletedFile}, + } + + for _, s := range scenarios { + t.Run(s.testName, func(t *testing.T) { + patch := Parse(s.patchStr) + newLineNumbers, oldLineNumbers := patch.LineNumbersOfLines() + + assert.Len(t, newLineNumbers, patch.LineCount()) + assert.Len(t, oldLineNumbers, patch.LineCount()) + for idx := range patch.LineCount() { + assert.Equal(t, patch.LineNumberOfLine(idx), newLineNumbers[idx], "new line number of line %d", idx) + assert.Equal(t, patch.OldLineNumberOfLine(idx), oldLineNumbers[idx], "old line number of line %d", idx) + } + }) + } +} + func TestGetNextStageableLineIndex(t *testing.T) { type scenario struct { testName string diff --git a/pkg/gui/controllers/helpers/diff_line_parser.go b/pkg/gui/controllers/helpers/diff_line_parser.go index 39d99c9ec..1a019bf1c 100644 --- a/pkg/gui/controllers/helpers/diff_line_parser.go +++ b/pkg/gui/controllers/helpers/diff_line_parser.go @@ -272,6 +272,7 @@ func parseFileSection(fileLines []string, endsTheBuffer bool) []bufferLineParse return result } patchLines := p.Lines() + newLineNumbers, oldLineNumbers := p.LineNumbersOfLines() for i := range fileLines { if i >= len(patchLines) { break @@ -279,10 +280,10 @@ func parseFileSection(fileLines []string, endsTheBuffer bool) []bufferLineParse parsed := parsedDiffLine{ Path: relPath, Type: diffLineTypeForKind(patchLines[i].Kind), - NewLine: p.LineNumberOfLine(i), + NewLine: newLineNumbers[i], } if parsed.Type == types.DiffLineDeleted { - parsed.OldLine = p.OldLineNumberOfLine(i) + parsed.OldLine = oldLineNumbers[i] } result[i] = bufferLineParse{parsed, true} } From 185f147db2965d9017b783eab866301eefe2f42b Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Oct 2026 17:27:34 +0200 Subject: [PATCH 2/5] Number the lines of a patch in one pass when indexing its change lines Several actions on the selected lines of a diff look up those lines in the diff of their file by their line numbers: staging, unstaging and discarding them, editing the hunk they are in, and adding them to a custom patch. Showing which lines of a diff are in the custom patch does the same lookup the other way round. For this, the change lines of the file's diff are indexed by their line numbers. The index asked the patch for the number of each change line separately, and each of these calls counts the lines of the hunk before the line. So for a new file of 100,000 lines, building the index took 2 seconds. Number all lines of the patch in one pass instead. Building the index for that file now takes 4 milliseconds. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/commands/patch/patch_builder.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/commands/patch/patch_builder.go b/pkg/commands/patch/patch_builder.go index 7b619e369..71c6972b3 100644 --- a/pkg/commands/patch/patch_builder.go +++ b/pkg/commands/patch/patch_builder.go @@ -416,12 +416,13 @@ type LineIdentity struct { // old file keeps them apart. func ChangeLineIndexByIdentity(parsed *Patch) map[LineIdentity]int { byIdentity := map[LineIdentity]int{} + newLineNumbers, oldLineNumbers := parsed.LineNumbersOfLines() for idx, line := range parsed.Lines() { switch { case line.IsAddition(): - byIdentity[LineIdentity{parsed.LineNumberOfLine(idx), false}] = idx + byIdentity[LineIdentity{newLineNumbers[idx], false}] = idx case line.IsDeletion(): - byIdentity[LineIdentity{parsed.OldLineNumberOfLine(idx), true}] = idx + byIdentity[LineIdentity{oldLineNumbers[idx], true}] = idx } } return byIdentity From cbfa22cfbe4b56766d79b7f2083a5c31b1956b0c Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Oct 2026 17:33:13 +0200 Subject: [PATCH 3/5] Make the path of a diff's lines absolute once per file Every query about the lines of the main view, such as moving the selection to the next change block, finds out which line of which file each row shows. For every row, this joined the path that the diff states with the path of the worktree. In a diff of 300,000 lines, this took more than half of the time of the query. The lines of a diff come file by file, so remember the last path that was made absolute and reuse it while the path stays the same. Do the same in the other places that go through every line of a diff. A query over that diff now takes about 27 milliseconds instead of 63. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../controllers/helpers/diff_line_helper.go | 45 ++++++++++++++----- .../helpers/diff_line_helper_test.go | 34 ++++++++++++++ .../helpers/diff_line_plain_text.go | 3 +- .../controllers/helpers/diff_line_queries.go | 3 +- 4 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 pkg/gui/controllers/helpers/diff_line_helper_test.go diff --git a/pkg/gui/controllers/helpers/diff_line_helper.go b/pkg/gui/controllers/helpers/diff_line_helper.go index 0da3f858d..a8a8f5e53 100644 --- a/pkg/gui/controllers/helpers/diff_line_helper.go +++ b/pkg/gui/controllers/helpers/diff_line_helper.go @@ -5,6 +5,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) type DiffLineHelper struct { @@ -134,10 +135,13 @@ func (self *DiffLineHelper) resolveDiffLines(contents []gocui.DiffLineContent) [ // side-by-side row leads with the deletion whose addition was what got remembered // under a unified one. func (self *DiffLineHelper) resolveDiffLineIdentities(contents []gocui.DiffLineContent) [][]types.DiffLineInfo { + toDiffLineInfo := self.diffLineInfoConverter() identities := make([][]types.DiffLineInfo, len(contents)) for i, parsed := range parseDiffLineIdentities(contents) { if len(parsed) > 0 { - identities[i] = self.diffLineInfos(parsed) + identities[i] = lo.Map(parsed, func(line parsedDiffLine, _ int) types.DiffLineInfo { + return toDiffLineInfo(line) + }) } } return identities @@ -159,19 +163,40 @@ func (self *DiffLineHelper) diffLineInfos(parsed []parsedDiffLine) []types.DiffL return infos } +// diffLineInfoConverter returns diffLineInfo as a function, for the callers that turn +// every line of a diff into an identity (see diffLineInfoConverterIn). +func (self *DiffLineHelper) diffLineInfoConverter() func(parsedDiffLine) types.DiffLineInfo { + return diffLineInfoConverterIn(self.c.Git().RepoPaths.WorktreePath()) +} + // diffLineInfoIn is diffLineInfo against a given worktree, for the callers that can't // ask which repo we are in where they run: a repo switch replaces it, so only the UI // thread may read it. func diffLineInfoIn(worktreePath string, parsed parsedDiffLine) types.DiffLineInfo { - path := parsed.Path - if !filepath.IsAbs(path) { - path = filepath.Join(worktreePath, path) - } + return diffLineInfoConverterIn(worktreePath)(parsed) +} - return types.DiffLineInfo{ - Path: path, - Type: parsed.Type, - NewLine: parsed.NewLine, - OldLine: parsed.OldLine, +// diffLineInfoConverterIn returns diffLineInfoIn for the given worktree as a function, +// for the callers that turn every line of a diff into an identity. The lines of a diff +// come file by file, so the path of a line is nearly always the path of the line +// before it. The function makes a path absolute only when it differs from the one +// before. +func diffLineInfoConverterIn(worktreePath string) func(parsedDiffLine) types.DiffLineInfo { + path, absolutePath := "", "" + return func(parsed parsedDiffLine) types.DiffLineInfo { + if absolutePath == "" || parsed.Path != path { + path = parsed.Path + absolutePath = parsed.Path + if !filepath.IsAbs(absolutePath) { + absolutePath = filepath.Join(worktreePath, absolutePath) + } + } + + return types.DiffLineInfo{ + Path: absolutePath, + Type: parsed.Type, + NewLine: parsed.NewLine, + OldLine: parsed.OldLine, + } } } diff --git a/pkg/gui/controllers/helpers/diff_line_helper_test.go b/pkg/gui/controllers/helpers/diff_line_helper_test.go new file mode 100644 index 000000000..057173845 --- /dev/null +++ b/pkg/gui/controllers/helpers/diff_line_helper_test.go @@ -0,0 +1,34 @@ +package helpers + +import ( + "path/filepath" + "testing" + + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/stretchr/testify/assert" +) + +func TestDiffLineInfoConverterIn(t *testing.T) { + worktreePath := t.TempDir() + absolutePath := filepath.Join(t.TempDir(), "d.txt") + toDiffLineInfo := diffLineInfoConverterIn(worktreePath) + + lines := []parsedDiffLine{ + {Path: "a.txt", Type: types.DiffLineAdded, NewLine: 1}, + {Path: "a.txt", Type: types.DiffLineDeleted, NewLine: 2, OldLine: 1}, + {Path: "dir/b.txt", Type: types.DiffLineContext, NewLine: 3}, + {Path: absolutePath, Type: types.DiffLineAdded, NewLine: 4}, + {Path: "a.txt", Type: types.DiffLineAdded, NewLine: 5}, + } + expected := []types.DiffLineInfo{ + {Path: filepath.Join(worktreePath, "a.txt"), Type: types.DiffLineAdded, NewLine: 1}, + {Path: filepath.Join(worktreePath, "a.txt"), Type: types.DiffLineDeleted, NewLine: 2, OldLine: 1}, + {Path: filepath.Join(worktreePath, "dir", "b.txt"), Type: types.DiffLineContext, NewLine: 3}, + {Path: absolutePath, Type: types.DiffLineAdded, NewLine: 4}, + {Path: filepath.Join(worktreePath, "a.txt"), Type: types.DiffLineAdded, NewLine: 5}, + } + + for i, line := range lines { + assert.Equal(t, expected[i], toDiffLineInfo(line), "line %d", i) + } +} diff --git a/pkg/gui/controllers/helpers/diff_line_plain_text.go b/pkg/gui/controllers/helpers/diff_line_plain_text.go index 6b53d94af..f62144ec1 100644 --- a/pkg/gui/controllers/helpers/diff_line_plain_text.go +++ b/pkg/gui/controllers/helpers/diff_line_plain_text.go @@ -72,12 +72,13 @@ func (self *DiffLineHelper) PlainDiffOfSelection( } diffLines := strings.Split(strings.TrimSuffix(plainDiff(relPaths), "\n"), "\n") + toDiffLineInfo := diffLineInfoConverterIn(worktreePath) runs := map[string][2]int{} for i, parsed := range parseAllDiffLinesFromBuffer(diffLines) { if !parsed.ok { continue } - info := diffLineInfoIn(worktreePath, parsed.parsed) + info := toDiffLineInfo(parsed.parsed) selected := selection[info.Path] if selected == nil || !selected.covers(info) { continue diff --git a/pkg/gui/controllers/helpers/diff_line_queries.go b/pkg/gui/controllers/helpers/diff_line_queries.go index ce6fba60a..c8e59bbb4 100644 --- a/pkg/gui/controllers/helpers/diff_line_queries.go +++ b/pkg/gui/controllers/helpers/diff_line_queries.go @@ -47,13 +47,14 @@ func (self *DiffLineHelper) DiffLinesInBufferRange(view *gocui.View, first int, func (self *DiffLineHelper) ChangeLineOrdinals( diff string, infos []types.DiffLineInfo, ) map[string][]int { + toDiffLineInfo := self.diffLineInfoConverter() ordinals := map[patchLine]int{} counts := map[string]int{} for _, parsed := range parseAllDiffLinesFromBuffer(strings.Split(diff, "\n")) { if !parsed.ok { continue } - info := self.diffLineInfo(parsed.parsed) + info := toDiffLineInfo(parsed.parsed) if !info.IsChange() { continue } From 3f64902a1152ce1b583067416b3ead982b72eb81 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Oct 2026 18:51:05 +0200 Subject: [PATCH 4/5] Collect the records of a line when the line has been written Under a diff renderer that states which diff line each row shows, such as delta, moving the selection by change block in a diff of 300,000 lines lags noticeably. One reason is that the queries about the lines of the main view collect the records of every row from the row's cells, going through every cell of every row. Under delta, a row has about 130 cells. Collect the records of a line once, when the write cursor moves on to the next line, and take them from there. A line that is written to again is read from its cells until it has been finished again. Co-Authored-By: Claude Opus 5.5 (1M context) --- pkg/gocui/view.go | 47 +++++++++++++++++++++++++++++++++++------- pkg/gocui/view_test.go | 19 +++++++++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 784f04aab..7cb3f5fc9 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -754,6 +754,15 @@ type lineType struct { // which they hold as the overwrite it caused. nil while the cells spell the // line as it was written, as they do for most lines. asWritten []byte + + // metadata holds the OSC 1717 records that the line's cells carry, each once, in + // the order they first appear. They are collected when the write cursor moves on + // to the next line, so that a reader can take them from here instead of going + // through every cell of every line. metadataCollected is false until then, and + // again once anything is written to the line; a reader then takes them from the + // cells. + metadata []string + metadataCollected bool } // textAsWritten returns the line's text as its writer wrote it, escape sequences @@ -766,6 +775,34 @@ func (l *lineType) textAsWritten() string { return l.cells.String() } +// diffLineMetadata returns the OSC 1717 records that the line's cells carry (see +// lineType.metadata). +func (l *lineType) diffLineMetadata() []string { + if l.metadataCollected { + return l.metadata + } + return metadataOfCells(l.cells) +} + +// collectMetadata keeps the records that the line's cells carry, for readers to take +// without going through the cells (see lineType.metadata). +func (l *lineType) collectMetadata() { + l.metadata = metadataOfCells(l.cells) + l.metadataCollected = true +} + +// metadataOfCells returns the OSC 1717 records that the given cells carry, each once, +// in the order they first appear. +func metadataOfCells(cells []cell) []string { + var metadata []string + for _, c := range cells { + if c.metadata != "" && !slices.Contains(metadata, c.metadata) { + metadata = append(metadata, c.metadata) + } + } + return metadata +} + // trailingFillAttributes describes the fg/bg colors that draw() should // use for cells past the end of a wrapped segment's content. On a source // line this records what the writer asked for via '\x1b[K' (and so opts @@ -1153,6 +1190,7 @@ func (b *viewBuffer) writeCells(cells []cell) { } } b.lines[b.wy].cells = line[:newLen] + b.lines[b.wy].metadataCollected = false b.wx += len(cells) } @@ -1244,6 +1282,7 @@ func (b *viewBuffer) write(v *View, p []byte) { } advanceToNextLine := func() { + b.lines[b.wy].collectMetadata() b.seekWrite(0, b.wy+1) if b.wy >= len(b.lines) { b.lines = append(b.lines, lineType{}) @@ -2170,13 +2209,7 @@ func diffLineContentsFrom(buf *viewBuffer, from int) []DiffLineContent { contents := make([]DiffLineContent, len(lines)) for i := range lines { line := &lines[i] - var metadata []string - for _, c := range line.cells { - if c.metadata != "" && !slices.Contains(metadata, c.metadata) { - metadata = append(metadata, c.metadata) - } - } - contents[i] = DiffLineContent{Text: line.textAsWritten(), Metadata: metadata} + contents[i] = DiffLineContent{Text: line.textAsWritten(), Metadata: line.diffLineMetadata()} } return contents } diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index 7daf10ced..a8e1cea56 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -335,6 +335,25 @@ func TestDiffLineContentsWithRecordsCoveringNoCell(t *testing.T) { }, v.DiffLineContents()) } +func TestDiffLineContentsOfARewrittenLine(t *testing.T) { + v := NewView("name", 0, 0, 80, 10, OutputNormal) + v.writeString(strings.Join([]string{ + osc1717("1;c;1;;foo.txt") + "line1", + osc1717("1;c;2;;foo.txt") + "line2", + "last line", + }, "\n")) + + // The records of a line that is written again are the ones it is written with, + // even when the line's earlier records were taken from it already. + v.OverwriteLines(0, osc1717("1;a;7;;bar.txt")+"new1") + + assert.Equal(t, []DiffLineContent{ + {Text: "new1", Metadata: []string{"1;a;7;;bar.txt"}}, + {Text: "line2", Metadata: []string{"1;c;2;;foo.txt"}}, + {Text: "last line"}, + }, v.DiffLineContents()) +} + func TestDiffLineContentsSwallowsHandshake(t *testing.T) { v := NewView("name", 0, 0, 80, 10, OutputNormal) From dc4fee296c63927dfe3b3dd6ac7a4922400f56d3 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Oct 2026 18:53:37 +0200 Subject: [PATCH 5/5] 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) --- pkg/gocui/view.go | 14 ++++++++++++++ pkg/gocui/view_test.go | 15 +++++++++++++++ .../controllers/helpers/diff_line_helper.go | 2 +- .../controllers/helpers/diff_line_parser.go | 15 +++++++++++++++ .../helpers/diff_line_parser_test.go | 19 +++++++++++++++++++ .../controllers/helpers/diff_line_queries.go | 8 ++++---- .../controllers/helpers/diff_line_restore.go | 8 ++++---- .../helpers/diff_line_selection.go | 2 +- 8 files changed, 73 insertions(+), 10 deletions(-) diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 7cb3f5fc9..f1c708796 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -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 diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index a8e1cea56..e4f2441b2 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -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) diff --git a/pkg/gui/controllers/helpers/diff_line_helper.go b/pkg/gui/controllers/helpers/diff_line_helper.go index a8a8f5e53..22d67b148 100644 --- a/pkg/gui/controllers/helpers/diff_line_helper.go +++ b/pkg/gui/controllers/helpers/diff_line_helper.go @@ -60,7 +60,7 @@ func (self *DiffLineHelper) diffLineIdentitiesAt( return nil, false } - contents := view.DiffLineContents() + contents := diffLineContents(view) if bufferLineIdx >= len(contents) { return nil, false } diff --git a/pkg/gui/controllers/helpers/diff_line_parser.go b/pkg/gui/controllers/helpers/diff_line_parser.go index 1a019bf1c..667946a7c 100644 --- a/pkg/gui/controllers/helpers/diff_line_parser.go +++ b/pkg/gui/controllers/helpers/diff_line_parser.go @@ -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 diff --git a/pkg/gui/controllers/helpers/diff_line_parser_test.go b/pkg/gui/controllers/helpers/diff_line_parser_test.go index f08618b74..55553e0f3 100644 --- a/pkg/gui/controllers/helpers/diff_line_parser_test.go +++ b/pkg/gui/controllers/helpers/diff_line_parser_test.go @@ -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)) +} diff --git a/pkg/gui/controllers/helpers/diff_line_queries.go b/pkg/gui/controllers/helpers/diff_line_queries.go index c8e59bbb4..e56591a25 100644 --- a/pkg/gui/controllers/helpers/diff_line_queries.go +++ b/pkg/gui/controllers/helpers/diff_line_queries.go @@ -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 { diff --git a/pkg/gui/controllers/helpers/diff_line_restore.go b/pkg/gui/controllers/helpers/diff_line_restore.go index 5cd924cc9..bee446b7e 100644 --- a/pkg/gui/controllers/helpers/diff_line_restore.go +++ b/pkg/gui/controllers/helpers/diff_line_restore.go @@ -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 } diff --git a/pkg/gui/controllers/helpers/diff_line_selection.go b/pkg/gui/controllers/helpers/diff_line_selection.go index ff9a9d2c9..85e554af3 100644 --- a/pkg/gui/controllers/helpers/diff_line_selection.go +++ b/pkg/gui/controllers/helpers/diff_line_selection.go @@ -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() {