diff --git a/pkg/gocui/escape.go b/pkg/gocui/escape.go index 2fe84e3be..174da7461 100644 --- a/pkg/gocui/escape.go +++ b/pkg/gocui/escape.go @@ -485,8 +485,10 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) { case stateOSCMetadata: switch { case characterEquals(ch, 0x07): + ei.dropMetadataIfHandshake() ei.state = stateNone case characterEquals(ch, 0x1b): + ei.dropMetadataIfHandshake() ei.state = stateOSCEndEscape default: ei.metadata.Write(ch) @@ -507,6 +509,18 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) { return false, nil } +// dropMetadataIfHandshake discards a just-completed OSC 1717 payload that carries no +// fields (no ';'). A metadata-aware pager emits such a version-only record once, as +// its first output, to announce it speaks the protocol (so we can probe it; see the +// raw-diff fallback in the lazygit staging helper). It isn't per-line metadata, so it +// must not linger in the accumulator and attach to the following line. Per-line +// payloads always have fields, so they're kept. +func (ei *escapeInterpreter) dropMetadataIfHandshake() { + if !strings.Contains(ei.metadata.String(), ";") { + ei.metadata.Reset() + } +} + func (ei *escapeInterpreter) outputCSI() error { n := len(ei.csiParam) for i := 0; i < n; { diff --git a/pkg/gocui/view_test.go b/pkg/gocui/view_test.go index c4618fe8c..a727456ed 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -217,6 +217,42 @@ func TestDiffLineMetadataPayloads(t *testing.T) { }, v.DiffLineMetadataPayloads()) } +func TestDiffLineMetadataHandshakeSwallowed(t *testing.T) { + v := NewView("name", 0, 0, 80, 10, OutputNormal) + + // A metadata-aware pager emits a version-only handshake (an OSC 1717 with no + // fields) as its first output, immediately before the diff, to announce it speaks + // the protocol. It must be swallowed whole: no visible bytes, no phantom line, and + // crucially it must not attach as metadata to the diff header that follows it. + osc := func(payload string) string { return "\x1b]1717;" + payload + "\x1b\\" } + v.writeString(osc("1") + strings.Join([]string{ + "diff --git a/foo.txt b/foo.txt", + osc("1;a;1;;foo.txt") + "added", + }, "\n")) + + // The handshake produced no phantom line and no visible bytes. + assert.Equal(t, []string{ + "diff --git a/foo.txt b/foo.txt", + "added", + }, v.BufferLines()) + + // The handshake didn't bleed onto the header line, and the real per-line metadata + // after it still applies. + type result struct { + payload string + ok bool + } + got := make([]result, len(v.buf.lines)) + for y := range v.buf.lines { + payload, ok := v.DiffLineMetadataInLine(y) + got[y] = result{payload, ok} + } + assert.Equal(t, []result{ + {"", false}, + {"1;a;1;;foo.txt", true}, + }, got) +} + // 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