jesseduffield.lazygit/pkg/gui/controllers/helpers/diff_line_helper.go
2026-09-06 17:32:48 +02:00

176 lines
7.3 KiB
Go

package helpers
import (
"path/filepath"
"github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type DiffLineHelper struct {
c *HelperCommon
// What the probe said about the diff renderer that rendererSignature names, or nil
// before it has been asked about any (see diffRendererEmitsMetadata).
rendererEmitsMetadata *bool
rendererSignature string
}
func NewDiffLineHelper(c *HelperCommon) *DiffLineHelper {
return &DiffLineHelper{c: c}
}
// GetDiffLineInfo recovers the identity — file, kind, and old/new line number —
// of the diff row at the given (wrapped) view line of the given view. It is the
// seam every consumer of a diff row goes through, so that how we recover that
// identity can change without them noticing.
//
// There are two ways. A diff renderer that speaks the OSC 1717 protocol states
// the identity of each line it renders, which is the only way to recover it from
// a rendering that doesn't look like a diff any more — columns, or +/- markers
// replaced by colour. Otherwise we parse the view's contents as a unified diff,
// which works for the renderings that keep a diff's structure (no renderer, `git
// diff --color`, a renderer that only colorizes) and fails for the rest.
//
// ok is false when the row's identity can't be recovered, in which case the
// caller must not act on the line at all.
func (self *DiffLineHelper) GetDiffLineInfo(view *gocui.View, viewLineIdx int) (types.DiffLineInfo, bool) {
identities, ok := self.diffLineIdentitiesAt(view, viewLineIdx)
if !ok {
return types.DiffLineInfo{}, false
}
return identities[0], true
}
// diffLineIdentitiesAt recovers every diff line the row at the given (wrapped) view
// line shows, left to right. It is GetDiffLineInfo's form for a reader that can't
// settle for the line the row leads with: an end of a selection covers its whole
// row, so where a rendering puts a modification's two halves side by side it covers
// both of them. ok is false when the row's identity can't be recovered at all.
func (self *DiffLineHelper) diffLineIdentitiesAt(
view *gocui.View, viewLineIdx int,
) ([]types.DiffLineInfo, bool) {
// The cursor and clicks land on a view line, which counts wrapped segments;
// the contents are indexed by unwrapped buffer line.
bufferLineIdx, ok := view.BufferLineForViewLine(viewLineIdx)
if !ok {
return nil, false
}
contents := view.DiffLineContents()
if bufferLineIdx >= len(contents) {
return nil, false
}
if identities := self.diffLineIdentitiesFromRecords(contents[bufferLineIdx].Metadata); len(identities) > 0 {
return self.inRepoTerms(view, identities), true
}
parsed, ok := parseDiffLineFromBuffer(diffLineTexts(contents), bufferLineIdx)
if !ok {
return nil, false
}
return self.inRepoTerms(view, []types.DiffLineInfo{self.diffLineInfo(parsed)}), true
}
// diffLineInfoFromRecords recovers a row's identity from the records the diff
// renderer stated for it. These take precedence over the buffer parse. ok is false
// when the row carries no record we understand, leaving the caller to parse.
//
// A row can carry more than one record, when the rendering puts two diff lines on it
// (a side-by-side row shows a deletion and the addition replacing it); the leftmost
// is the one a reader would call the row's own, so it is the row's identity.
func (self *DiffLineHelper) diffLineInfoFromRecords(metadata []string) (types.DiffLineInfo, bool) {
identities := self.diffLineIdentitiesFromRecords(metadata)
if len(identities) == 0 {
return types.DiffLineInfo{}, false
}
return identities[0], true
}
// diffLineIdentitiesFromRecords recovers the identity of every diff line the row's
// records state, left to right. Which of them a reader is after depends on the
// reader: the one the row leads with is the row's own identity (see
// diffLineInfoFromRecords), while a reader looking for a particular line has to
// consider them all, since which of a modification's two halves leads a row is up to
// the rendering.
func (self *DiffLineHelper) diffLineIdentitiesFromRecords(metadata []string) []types.DiffLineInfo {
identities := make([]types.DiffLineInfo, 0, len(metadata))
for _, record := range metadata {
if parsed, ok := parseDiffLineMetadata(record); ok {
identities = append(identities, self.diffLineInfo(parsed))
}
}
return identities
}
// resolvedDiffLine is one rendered row's recovered identity, plus whether it could
// be recovered at all — the element of the table resolveDiffLines produces.
type resolvedDiffLine struct {
info types.DiffLineInfo
ok bool
}
// resolveDiffLines recovers the identity of every row of a rendered diff in one
// pass, indexed 1:1 with contents. It is the batch form of GetDiffLineInfo, for the
// whole-buffer scans (which change lines are where, which file each row belongs
// to). Resolving row by row would re-run the buffer parser's whole-section parse
// once per row — O(n²) on a large single-file diff — so the buffer parser runs once
// for the whole buffer and the per-row metadata takes precedence on top.
func (self *DiffLineHelper) resolveDiffLines(contents []gocui.DiffLineContent) []resolvedDiffLine {
bufferParsed := parseAllDiffLinesFromBuffer(diffLineTexts(contents))
resolved := make([]resolvedDiffLine, len(contents))
for i, content := range contents {
if info, ok := self.diffLineInfoFromRecords(content.Metadata); ok {
resolved[i] = resolvedDiffLine{info, true}
} else if bufferParsed[i].ok {
resolved[i] = resolvedDiffLine{self.diffLineInfo(bufferParsed[i].parsed), true}
}
}
return resolved
}
// resolveDiffLineIdentities recovers every diff line each row of a rendered diff
// shows, in one pass, indexed 1:1 with contents. It is resolveDiffLines' form for the
// readers that can't settle for the line a row leads with: looking for a remembered
// line in a new rendering has to consider both halves of a modification, since a
// 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 {
bufferParsed := parseAllDiffLinesFromBuffer(diffLineTexts(contents))
identities := make([][]types.DiffLineInfo, len(contents))
for i, content := range contents {
if fromRecords := self.diffLineIdentitiesFromRecords(content.Metadata); len(fromRecords) > 0 {
identities[i] = fromRecords
} else if bufferParsed[i].ok {
identities[i] = []types.DiffLineInfo{self.diffLineInfo(bufferParsed[i].parsed)}
}
}
return identities
}
// diffLineInfo turns a parser's result into the absolute-path identity consumers
// work with. The path arrives repo-relative from the diff header, but a renderer
// states it however it likes, absolute paths included.
func (self *DiffLineHelper) diffLineInfo(parsed parsedDiffLine) types.DiffLineInfo {
return diffLineInfoIn(self.c.Git().RepoPaths.WorktreePath(), parsed)
}
// 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 types.DiffLineInfo{
Path: path,
Type: parsed.Type,
NewLine: parsed.NewLine,
OldLine: parsed.OldLine,
}
}