v0.7.1 ff50ee4

scrollback_test.go RawHistoryFolder

1package vt
2
3import (
4	"testing"
5
6	renderer "github.com/bnema/vev-vt/core"
7)
8
9func evictedTexts(rows [][]renderer.Cell) []string {
10	out := make([]string, len(rows))
11	for i, row := range rows {
12		runes := make([]rune, len(row))
13		for x, c := range row {
14			runes[x] = c.Rune
15		}
16		out[i] = string(runes)
17	}
18	return out
19}
20
21func TestOnLineEvicted(t *testing.T) {
22	tests := []struct {
23		name      string
24		write     []byte
25		wantRows  []string
26		wantFrame []string
27	}{
28		{
29			name:      "newline scroll evicts top row",
30			write:     []byte("AAAA\r\nBBBB\r\nCCCC\r\n"),
31			wantRows:  []string{"AAAA"},
32			wantFrame: []string{"BBBB", "CCCC", "    "},
33		},
34		{
35			name:      "CSI S count evicts each top row in order",
36			write:     []byte("AAAA\r\nBBBB\r\nCCCC\x1b[2S"),
37			wantRows:  []string{"AAAA", "BBBB"},
38			wantFrame: []string{"CCCC", "    ", "    "},
39		},
40	}
41
42	for _, tt := range tests {
43		t.Run(tt.name, func(t *testing.T) {
44			s := NewScreen(4, 3)
45			var evicted [][]renderer.Cell
46			s.OnLineEvicted = func(row []renderer.Cell) {
47				evicted = append(evicted, append([]renderer.Cell(nil), row...))
48			}
49
50			s.Write(tt.write)
51
52			if got := evictedTexts(evicted); !equalStrings(got, tt.wantRows) {
53				t.Fatalf("evicted rows = %#v, want %#v", got, tt.wantRows)
54			}
55			for y, want := range tt.wantFrame {
56				if got := lineText(s, y); got != want {
57					t.Fatalf("line %d = %q, want %q", y, got, want)
58				}
59			}
60		})
61	}
62}
63
64func TestOnLineEvictedAltScreenAndRotation(t *testing.T) {
65	tests := []struct {
66		name     string
67		write    []byte
68		wantRows []string
69	}{
70		{
71			name:     "alternate screen scroll does not evict",
72			write:    []byte("main\x1b[?1049hAAAA\r\nBBBB\r\nCCCC\r\nDDDD\x1b[?1049lEEEE\r\n"),
73			wantRows: nil,
74		},
75		{
76			name:     "evicted row follows logical top after prior lineOffset rotation",
77			write:    []byte("AAAA\r\nBBBB\r\nCCCC\r\nDDDD\r\n"),
78			wantRows: []string{"AAAA", "BBBB"},
79		},
80	}
81
82	for _, tt := range tests {
83		t.Run(tt.name, func(t *testing.T) {
84			s := NewScreen(4, 3)
85			var evicted [][]renderer.Cell
86			s.OnLineEvicted = func(row []renderer.Cell) {
87				evicted = append(evicted, append([]renderer.Cell(nil), row...))
88			}
89
90			s.Write(tt.write)
91
92			if got := evictedTexts(evicted); !equalStrings(got, tt.wantRows) {
93				t.Fatalf("evicted rows = %#v, want %#v", got, tt.wantRows)
94			}
95			if err := s.frame.CheckInvariants(); err != nil {
96				t.Fatalf("frame invariants after scrollback callback: %v", err)
97			}
98		})
99	}
100}
101
102func TestEvictedLinesCarryTheirBound(t *testing.T) {
103	tests := []struct {
104		name     string
105		evict    func(s *Screen)
106		wantSoft bool
107	}{
108		{
109			name: "scroll evicts a soft-wrapped row",
110			evict: func(s *Screen) {
111				// Filling all four columns only leaves deferred wrap pending. The
112				// fifth printable character triggers the soft wrap before scrolling.
113				s.Write([]byte("abcde"))
114				s.Write([]byte("\r\n\r\n\r\n\r\n"))
115			},
116			wantSoft: true,
117		},
118		{
119			name: "scroll evicts a hard row",
120			evict: func(s *Screen) {
121				s.Write([]byte("ab\r\n\r\n\r\n\r\n"))
122			},
123			wantSoft: false,
124		},
125	}
126
127	for _, tc := range tests {
128		t.Run(tc.name, func(t *testing.T) {
129			s := NewScreenWithHistory(4, 2, HistoryConfig{MaxRows: 16, MaxBytes: 1 << 20})
130			tc.evict(s)
131			view := s.History().SealAndView()
132			if view.Len() == 0 {
133				t.Fatal("nothing was evicted to history")
134			}
135			if got := view.Bound(0).Soft; got != tc.wantSoft {
136				t.Errorf("Bound(0).Soft = %v, want %v", got, tc.wantSoft)
137			}
138		})
139	}
140}
141
142func TestReflowEvictedLinesCarryTheirBound(t *testing.T) {
143	s := NewScreenWithHistory(8, 3, HistoryConfig{MaxRows: 16, MaxBytes: 1 << 20})
144	// Fill the grid with wrapped content, then shrink so rows are evicted by
145	// the reflow path in buffer.resize rather than by scrolling.
146	s.Write([]byte("aaaaaaaabbbbbbbbcccccccc"))
147	s.Resize(4, 2)
148
149	view := s.History().SealAndView()
150	if view.Len() == 0 {
151		t.Fatal("reflow evicted nothing to history")
152	}
153	for i := range view.Len() {
154		bound := view.Bound(i)
155		if bound.End == 0 {
156			t.Errorf("Bound(%d).End = 0, want the reflowed row extent", i)
157		}
158	}
159}
160
161func equalStrings(a, b []string) bool {
162	if len(a) != len(b) {
163		return false
164	}
165	for i := range a {
166		if a[i] != b[i] {
167			return false
168		}
169	}
170	return true
171}

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