v0.5.0 31affde

wrap_scroll_soft_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 13

↑↓ moveenter openesc close