diff --git a/pkg/gocui/escape.go b/pkg/gocui/escape.go index b5e84674c..0226a59fb 100644 --- a/pkg/gocui/escape.go +++ b/pkg/gocui/escape.go @@ -489,8 +489,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) @@ -511,6 +513,18 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) { return false, nil } +// dropMetadataIfHandshake discards a just-completed OSC 1717 payload that +// carries nothing beyond the version. A diff renderer emits such a record ahead +// of everything else to announce that it speaks the protocol, so that a host can +// find that out by asking rather than by inspecting a rendering. It says nothing +// about any line, so it must not attach to the line that follows it; a per-line +// record always has fields, and is 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 31e0e7c05..c74ba97a2 100644 --- a/pkg/gocui/view_test.go +++ b/pkg/gocui/view_test.go @@ -314,6 +314,23 @@ func TestDiffLineContentsOfWrappedLine(t *testing.T) { } } +func TestDiffLineContentsSwallowsHandshake(t *testing.T) { + v := NewView("name", 0, 0, 80, 10, OutputNormal) + + // A diff renderer announces itself with a version-only record before the + // diff. It must leave no trace: no visible bytes, no line of its own, and + // above all no record on the line that follows it. + v.writeString(osc1717("1") + strings.Join([]string{ + "diff --git a/foo.txt b/foo.txt", + osc1717("1;a;1;;foo.txt") + "added", + }, "\n")) + + assert.Equal(t, []DiffLineContent{ + {Text: "diff --git a/foo.txt b/foo.txt"}, + {Text: "added", Metadata: []string{"1;a;1;;foo.txt"}}, + }, v.DiffLineContents()) +} + // An async re-render builds into an off-screen buffer and swaps it in once it // has enough to paint, so readers keep seeing the previous render — coherent and // consistent — until the new content appears in one step. See View.offscreen.