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) <noreply@anthropic.com>
This commit is contained in:
Stefan HallerandClaude Opus 5.5 committed 2026-10-14 08:41:50 +02:00
1 parent ae6639a003
commit 80dff3c2b9
3 files changed
+63 -2

No files matched your search

+29
View File
@@ -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 {
+31
View File
@@ -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
@@ -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}
}