1package vt
2
3import (
4 "testing"
5
6 "github.com/stretchr/testify/require"
7)
8
9// A deferred wrap out of the last row of a scroll region scrolls that region,
10// and buffer.scrollUp deliberately severs the link on the last moved row. That
11// sever is right for a linefeed and wrong when the wrap itself caused the
12// scroll, so these cases pin both sides of the distinction.
13func TestSoftLinkSurvivesTheScrollItCaused(t *testing.T) {
14 const width = 8
15 cases := []struct {
16 name string
17 height int
18 input string
19 // wantBounds is indexed like the live grid.
20 wantBounds map[int]LineBound
21 // wantRows pins the grid text so a bound is never read off a row that
22 // holds something other than what the case describes.
23 wantRows map[int]string
24 }{
25 {
26 // The bug: the cursor sits on the last row, as it does in any full
27 // shell session, so the wrap scrolls and the link must survive.
28 name: "narrow wrap out of the bottom row keeps the link",
29 height: 4,
30 input: "1\r\n2\r\n3\r\n4\r\n5\r\nabcdefghij",
31 wantBounds: map[int]LineBound{
32 2: {End: 8, Soft: true},
33 3: {End: 2},
34 },
35 wantRows: map[int]string{2: "abcdefgh", 3: "ij "},
36 },
37 {
38 // Same shape at the wide-rune wrap site: the wide rune does not fit
39 // in the last column, so the writer abandons that cell and wraps.
40 name: "wide-rune wrap out of the bottom row keeps the link",
41 height: 4,
42 input: "1\r\n2\r\n3\r\nabcdefg漢",
43 wantBounds: map[int]LineBound{
44 2: {End: 7, Soft: true},
45 3: {End: 2},
46 },
47 wantRows: map[int]string{2: "abcdefg "},
48 },
49 {
50 // The sever that must survive: a full-width row ending in an explicit
51 // newline is a hard line that merely happens to fill the grid.
52 name: "explicit newline after a full-width bottom row severs",
53 height: 4,
54 input: "1\r\n2\r\n3\r\nabcdefgh\r\n",
55 wantBounds: map[int]LineBound{
56 2: {End: 8, Soft: false},
57 },
58 wantRows: map[int]string{2: "abcdefgh"},
59 },
60 {
61 // A DECSTBM region scroll is a local mutation, not a wrap: it must
62 // still sever at the row above the region (top-1), whose
63 // continuation the scroll just destroyed, and at the last moved row.
64 name: "DECSTBM region scroll severs at both edges",
65 height: 4,
66 input: "abcdefghij" + "\x1b[2;3r" + "\x1b[3;1H" + "\n",
67 wantBounds: map[int]LineBound{
68 0: {End: 8, Soft: false},
69 1: {End: 0, Soft: false},
70 },
71 wantRows: map[int]string{0: "abcdefgh"},
72 },
73 {
74 // A region exactly one row tall blanks the wrapped row in place
75 // instead of moving it up, so no surviving grid row may be linked.
76 // setScrollRegion rejects top >= bottom, so the only reachable
77 // one-row region is a one-row screen.
78 name: "wrap on a one-row screen links nothing on the grid",
79 height: 1,
80 input: "abcdefghij",
81 wantBounds: map[int]LineBound{
82 0: {End: 2, Soft: false},
83 },
84 wantRows: map[int]string{0: "ij "},
85 },
86 }
87 for _, tc := range cases {
88 t.Run(tc.name, func(t *testing.T) {
89 s := NewScreenWithHistory(width, tc.height, HistoryConfig{MaxRows: 64, MaxBytes: 1 << 20})
90 s.Write([]byte(tc.input))
91
92 bounds := s.LineBounds()
93 for y, want := range tc.wantBounds {
94 require.Equal(t, want, bounds[y], "LineBounds()[%d]", y)
95 }
96 for y, want := range tc.wantRows {
97 require.Equal(t, want, lineText(s, y), "row %d text", y)
98 }
99 })
100 }
101}
102
103// The soft link also drives reflow, so restoring it fixes resize too: a line
104// wrapped out of the bottom row used to be two unrelated physical rows, and
105// widening the pane left it split instead of rejoining it.
106func TestResizeRejoinsALineWrappedOutOfTheBottomRow(t *testing.T) {
107 s := NewScreenWithHistory(8, 4, HistoryConfig{MaxRows: 64, MaxBytes: 1 << 20})
108 s.Write([]byte("1\r\n2\r\n3\r\n4\r\n5\r\nabcdefghij"))
109
110 s.Resize(16, 4)
111
112 require.Equal(t, "abcdefghij ", lineText(s, 2), "the wrapped line must rejoin on one row")
113 require.Equal(t, LineBound{End: 10}, s.LineBounds()[2])
114 require.Equal(t, " ", lineText(s, 3), "its continuation row must be consumed by the rejoin")
115}
116
117// A wrap that scrolls the primary screen must hand history the same soft link
118// it leaves on the grid, so a line wrapped at the bottom of a full pane still
119// rejoins after it scrolls away.
120func TestSoftLinkFromABottomRowWrapReachesHistory(t *testing.T) {
121 const (
122 width = 8
123 height = 4
124 )
125 s := NewScreenWithHistory(width, height, HistoryConfig{MaxRows: 64, MaxBytes: 1 << 20})
126 // Wrap out of the bottom row, then push both physical rows into history.
127 s.Write([]byte("1\r\n2\r\n3\r\n4\r\n5\r\nabcdefghij\r\nx\r\ny\r\nz\r\n"))
128
129 view := s.History().SealAndView()
130 require.Equal(t, 7, view.Len(), "history row count")
131 require.Equal(t, "abcdefgh", rowString(view.Row(5)))
132 require.Equal(t, LineBound{End: 8, Soft: true}, view.Bound(5), "the wrapped row must reach history soft")
133 require.Equal(t, "ij ", rowString(view.Row(6)))
134 require.Equal(t, LineBound{End: 2, Soft: false}, view.Bound(6), "its continuation is a hard line end")
135}