From 80dff3c2b968b539decdffe462dee6ebad497af6 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Tue, 13 Oct 2026 17:25:41 +0200 Subject: [PATCH] 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} }