From cfe356b08d6d9795efe4b27e4563aa0ade946832 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Wed, 10 Jun 2026 07:38:41 +0200 Subject: [PATCH] Resolve a diff line's identity from a content snapshot, not the live view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- pkg/gocui/view.go | 42 ++++++++++++++ pkg/gui/controllers/helpers/staging_helper.go | 57 +++++++++++++------ 2 files changed, 82 insertions(+), 17 deletions(-) diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index fa3729dd8..4b0ff228d 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -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 diff --git a/pkg/gui/controllers/helpers/staging_helper.go b/pkg/gui/controllers/helpers/staging_helper.go index 5b31dcc8d..bed1c6a29 100644 --- a/pkg/gui/controllers/helpers/staging_helper.go +++ b/pkg/gui/controllers/helpers/staging_helper.go @@ -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