v0.7.0 36c429d

scroll_down_test.go RawHistoryFolder

1package ansi_test
2
3import (
4	"bytes"
5	"testing"
6
7	vt "github.com/bnema/vev-vt"
8	"github.com/bnema/vev-vt/ansi"
9)
10
11func TestBidirectionalScrollReplay(t *testing.T) {
12	for _, down := range []bool{false, true} {
13		for _, count := range []int{1, 3} {
14			frame := vt.NewFrame(12, 10)
15			for y := range frame.Height {
16				frame.FillRow(y, 0, frame.Width, vt.Cell{Rune: rune('a' + y), Style: vt.Style{Foreground: y, Background: -1}})
17			}
18			r := ansi.New(ansi.Capabilities{})
19			terminal := vt.NewScreen(frame.Width, frame.Height)
20			initial, err := r.Draw(frame, nil)
21			if err != nil {
22				t.Fatal(err)
23			}
24			terminal.Write(initial)
25			kind, exposed := vt.DamageScrollUp, 8-count
26			if down {
27				kind, exposed = vt.DamageScrollDown, 2
28				frame.ScrollDown(2, 7, count)
29			} else {
30				frame.ScrollUp(2, 7, count)
31			}
32			for y := exposed; y < exposed+count; y++ {
33				frame.FillRow(y, 0, frame.Width, vt.Cell{Rune: 'X', Style: vt.DefaultStyle()})
34			}
35			damage := []vt.Damage{
36				{Kind: kind, Y: 2, Width: 12, Height: 6, Count: count},
37				{Kind: vt.DamageText, Y: exposed, Width: 12, Height: count},
38			}
39			prepared, err := r.Prepare(frame, damage, false)
40			if err != nil {
41				t.Fatal(err)
42			}
43			if !bytes.Contains(prepared.Bytes(), []byte("\x1b[3;8r")) {
44				t.Fatalf("missing scroll region: %q", prepared.Bytes())
45			}
46			terminal.Write(prepared.Bytes())
47			prepared.Commit()
48			for y := range frame.Height {
49				for x := range frame.Width {
50					if !frame.Cell(x, y).Equal(terminal.Cell(x, y)) {
51						t.Fatalf("down=%v count=%d cell %d,%d mismatch", down, count, x, y)
52					}
53				}
54			}
55			if err := frame.CheckInvariants(); err != nil {
56				t.Fatal(err)
57			}
58			unchanged, err := r.Draw(frame, nil)
59			if err != nil || len(unchanged) != 0 {
60				t.Fatalf("committed shadow differs: %q, %v", unchanged, err)
61			}
62		}
63	}
64}
65
66func TestDownwardScrollMismatchFallsBack(t *testing.T) {
67	committed := vt.NewFrame(10, 8)
68	frame := committed.Clone()
69	frame.Set(0, 4, vt.Cell{Rune: 'X', Style: vt.DefaultStyle()})
70	for _, width := range []int{9, 10} {
71		candidate, err := ansi.PlanDelta(frame, []vt.Damage{{Kind: vt.DamageScrollDown, Y: 1, Width: width, Height: 6, Count: 1}}, committed, false)
72		if err != nil {
73			t.Fatal(err)
74		}
75		if !candidate.Plan.Snapshot || candidate.Plan.Scroll.Height != 0 {
76			t.Fatalf("unsafe scroll accepted: %+v", candidate.Plan)
77		}
78	}
79}
80
81// A stale commit followed by the current draw's commit, or a Reset between
82// Prepare and Commit, must never leave a shadow that disagrees with the
83// terminal: the next Prepare is a full snapshot.
84func TestCommitAfterStaleCommitOrResetKeepsTerminalExact(t *testing.T) {
85	for _, tc := range []string{"stale-then-current", "reset-between"} {
86		t.Run(tc, func(t *testing.T) {
87			frame := vt.NewFrame(10, 4)
88			for y := range frame.Height {
89				frame.FillRow(y, 0, frame.Width, vt.Cell{Rune: rune('a' + y), Style: vt.DefaultStyle()})
90			}
91			r := ansi.New(ansi.Capabilities{})
92			terminal := vt.NewScreen(frame.Width, frame.Height)
93			initial, err := r.Draw(frame, nil)
94			if err != nil {
95				t.Fatal(err)
96			}
97			terminal.Write(initial)
98
99			frame.Set(1, 1, vt.Cell{Rune: 'A', Style: vt.DefaultStyle()})
100			a, err := r.Prepare(frame, []vt.Damage{{Kind: vt.DamageText, X: 1, Y: 1, Width: 1, Height: 1}}, false)
101			if err != nil {
102				t.Fatal(err)
103			}
104			terminal.Write(a.Bytes())
105			// The caller reverts the cell before the next draw; only row 2 is damaged.
106			frame.Set(1, 1, vt.Cell{Rune: 'b', Style: vt.DefaultStyle()})
107			frame.Set(2, 2, vt.Cell{Rune: 'B', Style: vt.DefaultStyle()})
108			if tc == "reset-between" {
109				r.Reset()
110				a.Commit()
111			} else {
112				b, err := r.Prepare(frame, []vt.Damage{{Kind: vt.DamageText, X: 2, Y: 2, Width: 1, Height: 1}}, false)
113				if err != nil {
114					t.Fatal(err)
115				}
116				terminal.Write(b.Bytes())
117				a.Commit()
118				b.Commit()
119			}
120			next, err := r.Prepare(frame, nil, false)
121			if err != nil {
122				t.Fatal(err)
123			}
124			terminal.Write(next.Bytes())
125			next.Commit()
126			for y := range frame.Height {
127				for x := range frame.Width {
128					if !frame.Cell(x, y).Equal(terminal.Cell(x, y)) {
129						t.Fatalf("cell %d,%d = %+v, want %+v", x, y, terminal.Cell(x, y), frame.Cell(x, y))
130					}
131				}
132			}
133		})
134	}
135}
136
137func TestStaleCommitForcesSnapshotAndReplaysCorrectly(t *testing.T) {
138	for _, deliverB := range []bool{false, true} {
139		frame := vt.NewFrame(10, 4)
140		for y := range frame.Height {
141			frame.FillRow(y, 0, frame.Width, vt.Cell{Rune: rune('a' + y), Style: vt.DefaultStyle()})
142		}
143		r := ansi.New(ansi.Capabilities{})
144		terminal := vt.NewScreen(frame.Width, frame.Height)
145		initial, err := r.Draw(frame, nil)
146		if err != nil {
147			t.Fatal(err)
148		}
149		terminal.Write(initial)
150
151		frame.Set(1, 1, vt.Cell{Rune: 'A', Style: vt.DefaultStyle()})
152		a, err := r.Prepare(frame, []vt.Damage{{Kind: vt.DamageText, X: 1, Y: 1, Width: 1, Height: 1}}, false)
153		if err != nil {
154			t.Fatal(err)
155		}
156		terminal.Write(a.Bytes())
157		frame.Set(2, 2, vt.Cell{Rune: 'B', Style: vt.Style{Bold: true, Foreground: 2, Background: -1}})
158		b, err := r.Prepare(frame, []vt.Damage{{Kind: vt.DamageText, X: 2, Y: 2, Width: 1, Height: 1}}, false)
159		if err != nil {
160			t.Fatal(err)
161		}
162		if deliverB {
163			terminal.Write(b.Bytes())
164		}
165		a.Commit() // superseded by b: must drop the committed shadow
166
167		next, err := r.Prepare(frame, nil, false)
168		if err != nil {
169			t.Fatal(err)
170		}
171		if len(next.Bytes()) == 0 || !bytes.Contains(next.Bytes(), []byte("a")) || !bytes.Contains(next.Bytes(), []byte("d")) {
172			t.Fatalf("deliverB=%v: next prepare is not a full snapshot: %q", deliverB, next.Bytes())
173		}
174		terminal.Write(next.Bytes())
175		next.Commit()
176		for y := range frame.Height {
177			for x := range frame.Width {
178				if !frame.Cell(x, y).Equal(terminal.Cell(x, y)) {
179					t.Fatalf("deliverB=%v: cell %d,%d = %+v, want %+v", deliverB, x, y, terminal.Cell(x, y), frame.Cell(x, y))
180				}
181			}
182		}
183		unchanged, err := r.Draw(frame, nil)
184		if err != nil || len(unchanged) != 0 {
185			t.Fatalf("committed shadow differs after snapshot: %q, %v", unchanged, err)
186		}
187	}
188}

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