diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 94c0cb607..eba320419 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -1832,6 +1832,33 @@ func (v *View) DiffLineMetadataInLine(y int) (string, bool) { return "", false } +// DiffLineMetadataPayloads returns, per unwrapped buffer line, the distinct +// OSC-1717 metadata payloads carried by that line's cells, in left-to-right order. +// A single-column rendering tags every cell of a line with the same payload (one +// entry); a side-by-side rendering tags each side differently, so a changed row +// yields one payload per side (and a context row, where both sides match, still +// one). It is the multi-record counterpart of DiffLineContent.Metadata, which keeps +// only the first payload — enough to identify a single-column row, but it drops the +// other side of a side-by-side row. Staging a selection uses this to act on every +// change a row covers. Taken under the write lock in one pass so the payloads stay +// consistent with the buffer even if a concurrent re-render is rebuilding it. +func (v *View) DiffLineMetadataPayloads() [][]string { + v.writeMutex.Lock() + defer v.writeMutex.Unlock() + + result := make([][]string, len(v.buf.lines)) + for i, line := range v.buf.lines { + var payloads []string + for _, c := range line.cells { + if c.metadata != "" && !slices.Contains(payloads, c.metadata) { + payloads = append(payloads, c.metadata) + } + } + result[i] = payloads + } + return result +} + // 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-1717 diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index 357e9b163..1b1b4b88b 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -196,6 +196,27 @@ func TestDiffLineMetadata(t *testing.T) { assert.Equal(t, "@@ a header line with no metadata @@", v.BufferLines()[3]) } +func TestDiffLineMetadataPayloads(t *testing.T) { + v := NewView("name", 0, 0, 80, 10, OutputNormal) + + osc := func(payload string) string { return "\x1b]1717;" + payload + "\x1b\\" } + v.writeString(strings.Join([]string{ + // A single-column row: one payload tags the whole line. + osc("1;c;1;;foo.txt") + "context", + // A side-by-side change row: the deletion tags the left half and the + // addition replacing it tags the right half of the same rendered line. + osc("1;d;2;2;foo.txt") + "old2 " + osc("1;a;2;;foo.txt") + "new2", + // A header line with no metadata. + "@@ header @@", + }, "\n")) + + assert.Equal(t, [][]string{ + {"1;c;1;;foo.txt"}, + {"1;d;2;2;foo.txt", "1;a;2;;foo.txt"}, + nil, + }, v.DiffLineMetadataPayloads()) +} + // When a re-render produces fewer view lines than the previous one, // refreshViewLinesIfNeeded must truncate viewLines to the new content. If it // didn't (it used to overwrite in place and keep the tail), a reader could map a diff --git a/pkg/gui/controllers/helpers/staging_helper.go b/pkg/gui/controllers/helpers/staging_helper.go index e1d52611d..5c22135b3 100644 --- a/pkg/gui/controllers/helpers/staging_helper.go +++ b/pkg/gui/controllers/helpers/staging_helper.go @@ -170,6 +170,12 @@ func (self *StagingHelper) GetDiffLineInfo(windowName string, viewLineIdx int) ( // are skipped (Transform emits context regardless of the included set, so only // change lines need collecting — see §21.3), and view lines that wrap to the same // buffer line are de-duplicated. +// +// A side-by-side rendering carries more than one record per row — a deletion on the +// left, the addition replacing it on the right — and staging includes both (you +// can't stage one side of a side-by-side row; accepted restriction). So each row's +// metadata payloads are all resolved; rows without metadata (no pager, or the +// buffer-parse / hyperlink backends) fall back to their single resolved record. func (self *StagingHelper) ChangeLinesInViewRange(windowName string, first int, last int) []types.DiffLineInfo { v, _ := self.c.GocuiGui().View(self.windowHelper.GetViewNameForWindow(windowName)) if v == nil { @@ -177,6 +183,7 @@ func (self *StagingHelper) ChangeLinesInViewRange(windowName string, first int, } resolved := self.resolveDiffLines(v.DiffLineContents()) + payloadsByLine := v.DiffLineMetadataPayloads() var infos []types.DiffLineInfo lastBufferLine := -1 for viewLine := first; viewLine <= last; viewLine++ { @@ -185,7 +192,14 @@ func (self *StagingHelper) ChangeLinesInViewRange(windowName string, first int, continue } lastBufferLine = bufferLine - if bufferLine < len(resolved) && resolved[bufferLine].ok && resolved[bufferLine].info.IsChange() { + + if bufferLine < len(payloadsByLine) && len(payloadsByLine[bufferLine]) > 0 { + for _, payload := range payloadsByLine[bufferLine] { + if info, ok := self.diffLineInfoFromMetadata(payload); ok && info.IsChange() { + infos = append(infos, info) + } + } + } else if bufferLine < len(resolved) && resolved[bufferLine].ok && resolved[bufferLine].info.IsChange() { infos = append(infos, resolved[bufferLine].info) } }