mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-10-28 16:10:22 -05:00
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>
297 lines
9.6 KiB
Go
297 lines
9.6 KiB
Go
package patch
|
|
|
|
import (
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type Patch struct {
|
|
// header of the patch (split on newlines) e.g.
|
|
// diff --git a/filename b/filename
|
|
// index dcd3485..1ba5540 100644
|
|
// --- a/filename
|
|
// +++ b/filename
|
|
header []string
|
|
// hunks of the patch
|
|
hunks []*Hunk
|
|
}
|
|
|
|
// Returns a new patch with the specified transformation applied (e.g.
|
|
// only selecting a subset of changes).
|
|
// Leaves the original patch unchanged.
|
|
func (self *Patch) Transform(opts TransformOpts) *Patch {
|
|
return transform(self, opts)
|
|
}
|
|
|
|
// Returns the patch as a plain string
|
|
func (self *Patch) FormatPlain() string {
|
|
return formatPlain(self)
|
|
}
|
|
|
|
// Returns a range of lines from the patch as a plain string (range is inclusive)
|
|
func (self *Patch) FormatRangePlain(startIdx int, endIdx int) string {
|
|
return formatRangePlain(self, startIdx, endIdx)
|
|
}
|
|
|
|
// Returns the patch as a string with ANSI color codes for displaying in a view
|
|
func (self *Patch) FormatView(opts FormatViewOpts) string {
|
|
return formatView(self, opts)
|
|
}
|
|
|
|
// Returns the lines of the patch
|
|
func (self *Patch) Lines() []*PatchLine {
|
|
lines := []*PatchLine{}
|
|
for _, line := range self.header {
|
|
lines = append(lines, &PatchLine{Content: line, Kind: PATCH_HEADER})
|
|
}
|
|
|
|
for _, hunk := range self.hunks {
|
|
lines = append(lines, hunk.allLines()...)
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
// Returns the old-file starting line number of the hunk containing the given
|
|
// patch line index. Returns 0 if the line is not inside any hunk.
|
|
func (self *Patch) HunkOldStartForLine(idx int) int {
|
|
hunkIdx := self.HunkContainingLine(idx)
|
|
if hunkIdx == -1 {
|
|
return 0
|
|
}
|
|
return self.hunks[hunkIdx].oldStart
|
|
}
|
|
|
|
// Returns the patch line index of the first line in the given hunk
|
|
func (self *Patch) HunkStartIdx(hunkIndex int) int {
|
|
hunkIndex = lo.Clamp(hunkIndex, 0, len(self.hunks)-1)
|
|
|
|
result := len(self.header)
|
|
for i := range hunkIndex {
|
|
result += self.hunks[i].lineCount()
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Returns the patch line index of the last line in the given hunk
|
|
func (self *Patch) HunkEndIdx(hunkIndex int) int {
|
|
hunkIndex = lo.Clamp(hunkIndex, 0, len(self.hunks)-1)
|
|
|
|
return self.HunkStartIdx(hunkIndex) + self.hunks[hunkIndex].lineCount() - 1
|
|
}
|
|
|
|
// IsWellFormed reports whether every hunk's body matches the lengths declared in
|
|
// its header. A faithful unified diff always satisfies this; a rendering that
|
|
// restructured the diff body does not — a diff renderer that puts line numbers in
|
|
// a gutter, say, shifts the +/- marker off the start of each line, so every body
|
|
// line reads as context and the computed lengths no longer match the header. That
|
|
// makes this the test for whether a rendered diff can be parsed as a unified diff
|
|
// at all, rather than trusting a mis-parse. Only meaningful for patches produced
|
|
// by Parse, which is where the declared lengths come from.
|
|
func (self *Patch) IsWellFormed() bool {
|
|
return self.isWellFormed(false)
|
|
}
|
|
|
|
// IsWellFormedSoFar is IsWellFormed for a patch parsed from a diff we have only the
|
|
// beginning of. Its last hunk holds the first lines of a body that hasn't all arrived,
|
|
// so every hunk but the last has to match its header exactly, as before, while the last
|
|
// one only has to fit within what its header declares.
|
|
//
|
|
// The check exists to tell a faithful rendering from a restructured one, and it still
|
|
// does that. A rendering that moves the +/- marker off the start of the line makes us
|
|
// read a change as context, and a context line counts towards both lengths, so such a
|
|
// hunk comes out longer than its header declares rather than shorter.
|
|
func (self *Patch) IsWellFormedSoFar() bool {
|
|
return self.isWellFormed(true)
|
|
}
|
|
|
|
func (self *Patch) isWellFormed(lastHunkMayBeIncomplete bool) bool {
|
|
for i, hunk := range self.hunks {
|
|
if lastHunkMayBeIncomplete && i == len(self.hunks)-1 {
|
|
return hunk.oldLength() <= hunk.declaredOldLength &&
|
|
hunk.newLength() <= hunk.declaredNewLength
|
|
}
|
|
if hunk.oldLength() != hunk.declaredOldLength || hunk.newLength() != hunk.declaredNewLength {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (self *Patch) ContainsChanges() bool {
|
|
return lo.SomeBy(self.hunks, func(hunk *Hunk) bool {
|
|
return hunk.containsChanges()
|
|
})
|
|
}
|
|
|
|
// Takes a line index in the patch and returns the line number in the new file.
|
|
// If the line is a header line, returns 1.
|
|
// If the line is a hunk header line, returns the first file line number in that hunk.
|
|
// If the line is out of range below, returns the last file line number in the last hunk.
|
|
func (self *Patch) LineNumberOfLine(idx int) int {
|
|
if idx < len(self.header) || len(self.hunks) == 0 {
|
|
return 1
|
|
}
|
|
|
|
hunkIdx := self.HunkContainingLine(idx)
|
|
// cursor out of range, return last file line number
|
|
if hunkIdx == -1 {
|
|
lastHunk := self.hunks[len(self.hunks)-1]
|
|
return lastHunk.newStart + lastHunk.newLength() - 1
|
|
}
|
|
|
|
hunk := self.hunks[hunkIdx]
|
|
hunkStartIdx := self.HunkStartIdx(hunkIdx)
|
|
idxInHunk := idx - hunkStartIdx
|
|
|
|
if idxInHunk == 0 {
|
|
return hunk.newStart
|
|
}
|
|
|
|
lines := hunk.bodyLines[:idxInHunk-1]
|
|
offset := nLinesWithKind(lines, []PatchLineKind{ADDITION, CONTEXT})
|
|
return hunk.newStart + offset
|
|
}
|
|
|
|
// Takes a line index in the patch and returns the line number in the old file.
|
|
// This is the old-file counterpart of LineNumberOfLine; for a deletion it gives
|
|
// the line's position in the old file (additions get the position they sit at).
|
|
// If the line is a header line, returns 1.
|
|
// If the line is a hunk header line, returns the first old-file line number in that hunk.
|
|
// If the line is out of range below, returns the last old-file line number in the last hunk.
|
|
func (self *Patch) OldLineNumberOfLine(idx int) int {
|
|
if idx < len(self.header) || len(self.hunks) == 0 {
|
|
return 1
|
|
}
|
|
|
|
hunkIdx := self.HunkContainingLine(idx)
|
|
// cursor out of range, return last file line number
|
|
if hunkIdx == -1 {
|
|
lastHunk := self.hunks[len(self.hunks)-1]
|
|
return lastHunk.oldStart + lastHunk.oldLength() - 1
|
|
}
|
|
|
|
hunk := self.hunks[hunkIdx]
|
|
hunkStartIdx := self.HunkStartIdx(hunkIdx)
|
|
idxInHunk := idx - hunkStartIdx
|
|
|
|
if idxInHunk == 0 {
|
|
return hunk.oldStart
|
|
}
|
|
|
|
lines := hunk.bodyLines[:idxInHunk-1]
|
|
offset := nLinesWithKind(lines, []PatchLineKind{DELETION, CONTEXT})
|
|
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 {
|
|
hunkStartIdx := self.HunkStartIdx(hunkIdx)
|
|
if idx >= hunkStartIdx && idx < hunkStartIdx+hunk.lineCount() {
|
|
return hunkIdx
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// Returns the patch line index of the next change (i.e. addition or deletion)
|
|
// that matches the same "included" state, given the includedLines. If you don't
|
|
// care about included states, pass nil for includedLines and false for included.
|
|
func (self *Patch) GetNextChangeIdxOfSameIncludedState(idx int, includedLines []int, included bool) (int, bool) {
|
|
idx = lo.Clamp(idx, 0, self.LineCount()-1)
|
|
|
|
lines := self.Lines()
|
|
|
|
isMatch := func(i int, line *PatchLine) bool {
|
|
sameIncludedState := lo.Contains(includedLines, i) == included
|
|
return line.IsChange() && sameIncludedState
|
|
}
|
|
|
|
for i, line := range lines[idx:] {
|
|
if isMatch(i+idx, line) {
|
|
return i + idx, true
|
|
}
|
|
}
|
|
|
|
// there are no changes from the cursor onwards so we'll instead
|
|
// return the index of the last change
|
|
for i := len(lines) - 1; i >= 0; i-- {
|
|
line := lines[i]
|
|
if isMatch(i, line) {
|
|
return i, true
|
|
}
|
|
}
|
|
|
|
return 0, false
|
|
}
|
|
|
|
// Returns the patch line index of the next change (i.e. addition or deletion).
|
|
func (self *Patch) GetNextChangeIdx(idx int) int {
|
|
result, _ := self.GetNextChangeIdxOfSameIncludedState(idx, nil, false)
|
|
return result
|
|
}
|
|
|
|
// Returns the length of the patch in lines
|
|
func (self *Patch) LineCount() int {
|
|
count := len(self.header)
|
|
for _, hunk := range self.hunks {
|
|
count += hunk.lineCount()
|
|
}
|
|
return count
|
|
}
|
|
|
|
// Returns the number of hunks of the patch
|
|
func (self *Patch) HunkCount() int {
|
|
return len(self.hunks)
|
|
}
|
|
|
|
// Adjust the given line number (one-based) according to the current patch. The
|
|
// patch is supposed to be a diff of an old file state against the working
|
|
// directory; the line number is a line number in that old file, and the
|
|
// function returns the corresponding line number in the working directory file.
|
|
func (self *Patch) AdjustLineNumber(lineNumber int) int {
|
|
adjustedLineNumber := lineNumber
|
|
for _, hunk := range self.hunks {
|
|
if hunk.oldStart >= lineNumber {
|
|
break
|
|
}
|
|
|
|
if hunk.oldStart+hunk.oldLength() > lineNumber {
|
|
return hunk.newStart
|
|
}
|
|
|
|
adjustedLineNumber += hunk.newLength() - hunk.oldLength()
|
|
}
|
|
|
|
return adjustedLineNumber
|
|
}
|