Resolve a diff line's identity from a content snapshot, not the live view

The diff-line primitive (recover a rendered row's patch-space identity) is about
to gain a second, inverse consumer: the escape restore scans a focused main
view's rows as it re-renders, looking for the row that matches a target patch
identity. That scan runs over the *loading* off-screen buffer, not the displayed
view, so the resolver can't be tied to the displayed view's per-view-line readers.

Pull the three backends (OSC metadata, buffer parse, lazygit-edit hyperlink) onto
a single buffer-agnostic resolver that takes a snapshot of a diff's per-line
content — text, metadata and hyperlink per unwrapped buffer line — and the line
to resolve. The forward consumers (click/enter/edit/PR) feed it a snapshot of the
displayed buffer (gocui.DiffLineContents) after mapping the wrapped view line to
its buffer line; the upcoming scan will feed it the off-screen buffer's loaded
rows. Behavior is unchanged.
This commit is contained in:
Stefan Haller 2026-06-10 07:38:41 +02:00
parent 239426d46b
commit cfe356b08d
2 changed files with 82 additions and 17 deletions

View file

@ -1832,6 +1832,48 @@ func (v *View) DiffLineMetadataInLine(y int) (string, bool) {
return "", false
}
// DiffLineContent is the raw per-line material the diff-line backends parse to
// recover a rendered row's patch-space identity (see diff-line-metadata-notes.md):
// the decolorized text (for host-side parsing, mechanism #1), the OSC-456
// metadata payload a pager emitted (#2), and the line's hyperlink (delta's
// lazygit-edit fallback). It is indexed by unwrapped buffer line, so one entry
// covers all the (wrapped) view lines that line maps to.
type DiffLineContent struct {
Text string
Metadata string
Hyperlink string
}
// DiffLineContents returns the per-line diff material (see DiffLineContent) for
// every line of the displayed buffer. It is the snapshot the diff-line backends
// scan: taken under the write lock in one call, so the text, metadata and
// hyperlink of a given line stay consistent with each other even if a concurrent
// re-render is rebuilding the buffer.
func (v *View) DiffLineContents() []DiffLineContent {
v.writeMutex.Lock()
defer v.writeMutex.Unlock()
return diffLineContents(v.buf)
}
func diffLineContents(buf *viewBuffer) []DiffLineContent {
contents := make([]DiffLineContent, len(buf.lines))
for i, line := range buf.lines {
text := strings.ReplaceAll(line.cells.String(), "\x00", "")
var metadata, hyperlink string
for _, c := range line.cells {
if metadata == "" {
metadata = c.metadata
}
if hyperlink == "" {
hyperlink = c.hyperlink
}
}
contents[i] = DiffLineContent{Text: text, Metadata: metadata, Hyperlink: hyperlink}
}
return contents
}
// BufferLineForViewLine maps a view line index (which counts wrapped lines) to
// the index of the corresponding line in the unwrapped internal buffer (as
// returned by BufferLines). Several view lines can map to the same buffer line

View file

@ -160,22 +160,50 @@ func (self *StagingHelper) GetDiffLineInfo(windowName string, viewLineIdx int) (
return types.DiffLineInfo{}, false
}
if info, ok := self.diffLineInfoFromMetadata(v, viewLineIdx); ok {
// A click/cursor lands on a (wrapped) view line; resolve it to the unwrapped
// buffer line all three backends key off, then read that buffer line's content.
bufferLineIdx, ok := v.BufferLineForViewLine(viewLineIdx)
if !ok {
return types.DiffLineInfo{}, false
}
return self.diffLineInfoFromContents(v.DiffLineContents(), bufferLineIdx)
}
// diffLineInfoFromContents recovers the patch-space identity of the buffer line
// at idx within a snapshot of a diff's per-line content (see gocui.DiffLineContent).
// It is the single resolver behind both directions of the diff-line primitive —
// the forward consumers (click/enter/edit/PR, via GetDiffLineInfo on the displayed
// view) and the inverse identity scan that finds a target line in a focused main
// view as it re-renders (escape restore, via the loading off-screen content). It
// tries three backends in order of fidelity:
//
// - mechanism #2: per-line OSC metadata emitted by a patched pager (delta),
// which carries the side directly and so serves the renderings #1 can't parse
// (delta's default mode, --line-numbers, diff-so-fancy);
// - mechanism #1: parsing the decolorized buffer, which serves structure-
// preserving renderings (no pager, git diff --color, delta --color-only);
// - delta's lazygit-edit:// hyperlinks; the hyperlink can't convey the side, so
// its result is reported as a non-deletion content line.
func (self *StagingHelper) diffLineInfoFromContents(contents []gocui.DiffLineContent, idx int) (types.DiffLineInfo, bool) {
if idx < 0 || idx >= len(contents) {
return types.DiffLineInfo{}, false
}
if info, ok := self.diffLineInfoFromMetadata(contents[idx].Metadata); ok {
return info, true
}
if info, ok := self.diffLineInfoFromBuffer(v, viewLineIdx); ok {
if info, ok := self.diffLineInfoFromBuffer(contents, idx); ok {
return info, true
}
return self.diffLineInfoFromHyperlink(v, viewLineIdx)
return self.diffLineInfoFromHyperlink(contents[idx].Hyperlink)
}
// diffLineInfoFromMetadata reads mechanism #2's per-line OSC metadata. The
// payload is positional and ';'-delimited — version;type;new-line;old-line;file
// — with the file last so it may itself contain ';', and old-line empty unless
// the line is a deletion. See diff-line-metadata-notes.md §9.2.
func (self *StagingHelper) diffLineInfoFromMetadata(v *gocui.View, viewLineIdx int) (types.DiffLineInfo, bool) {
payload, ok := v.DiffLineMetadataInLine(viewLineIdx)
if !ok {
func (self *StagingHelper) diffLineInfoFromMetadata(payload string) (types.DiffLineInfo, bool) {
if payload == "" {
return types.DiffLineInfo{}, false
}
@ -199,13 +227,13 @@ func (self *StagingHelper) diffLineInfoFromMetadata(v *gocui.View, viewLineIdx i
}, true
}
func (self *StagingHelper) diffLineInfoFromBuffer(v *gocui.View, viewLineIdx int) (types.DiffLineInfo, bool) {
bufferLineIdx, ok := v.BufferLineForViewLine(viewLineIdx)
if !ok {
return types.DiffLineInfo{}, false
func (self *StagingHelper) diffLineInfoFromBuffer(contents []gocui.DiffLineContent, idx int) (types.DiffLineInfo, bool) {
texts := make([]string, len(contents))
for i, c := range contents {
texts[i] = c.Text
}
parsed, ok := parseDiffLineFromBuffer(v.BufferLines(), bufferLineIdx)
parsed, ok := parseDiffLineFromBuffer(texts, idx)
if !ok {
return types.DiffLineInfo{}, false
}
@ -218,12 +246,7 @@ func (self *StagingHelper) diffLineInfoFromBuffer(v *gocui.View, viewLineIdx int
}, true
}
func (self *StagingHelper) diffLineInfoFromHyperlink(v *gocui.View, viewLineIdx int) (types.DiffLineInfo, bool) {
hyperlink, ok := v.HyperLinkInLine(viewLineIdx, "lazygit-edit:")
if !ok {
return types.DiffLineInfo{}, false
}
func (self *StagingHelper) diffLineInfoFromHyperlink(hyperlink string) (types.DiffLineInfo, bool) {
matches := lazygitEditURLRegexp.FindStringSubmatch(hyperlink)
if matches == nil {
return types.DiffLineInfo{}, false