main ece7682

frame_test.go RawHistoryFolder

1package ui
2
3import (
4	"testing"
5
6	renderer "github.com/bnema/vev-vt"
7	"github.com/bnema/vev/internal/domain"
8)
9
10func cell(r rune, style renderer.Style, continuation bool) renderer.Cell {
11	return renderer.Cell{Rune: r, Style: style, Continuation: continuation}
12}
13
14func TestBlitFrameAnchorsAndClips(t *testing.T) {
15	style := renderer.Style{Foreground: 1, Background: 2}
16	tests := []struct {
17		name                                   string
18		anchor                                 VerticalAnchor
19		srcH, srcW, rectX, rectY, rectW, rectH int
20		want                                   string
21	}{
22		{"top taller", VerticalAnchorTop, 4, 2, 1, 1, 2, 2, "abcd"},
23		{"bottom taller", VerticalAnchorBottom, 4, 2, 1, 1, 2, 2, "efgh"},
24		{"top shorter", VerticalAnchorTop, 1, 2, 1, 1, 2, 2, "ab  "},
25		{"bottom shorter", VerticalAnchorBottom, 1, 2, 1, 1, 2, 2, "  ab"},
26	}
27	for _, tt := range tests {
28		t.Run(tt.name, func(t *testing.T) {
29			src := FrameView{Width: tt.srcW, Height: tt.srcH, Rows: make([][]renderer.Cell, tt.srcH)}
30			for y := range src.Rows {
31				src.Rows[y] = make([]renderer.Cell, tt.srcW)
32				for x := range src.Rows[y] {
33					src.Rows[y][x] = cell(rune('a'+y*tt.srcW+x), style, false)
34				}
35			}
36			dst := renderer.NewFrame(5, 5)
37			BlitFrame(dst, domain.Rect{X: tt.rectX, Y: tt.rectY, Width: tt.rectW, Height: tt.rectH}, src, tt.anchor)
38			got := ""
39			for y := 1; y < 3; y++ {
40				for x := 1; x < 3; x++ {
41					got += string(dst.At(x, y).Rune)
42				}
43			}
44			if got != tt.want {
45				t.Fatalf("got %q want %q", got, tt.want)
46			}
47		})
48	}
49}
50
51func TestBlitFrameNeverSplitsWidePairsAtClippedEdges(t *testing.T) {
52	style := renderer.Style{Foreground: 7, Background: 8, Bold: true}
53	wide := []renderer.Cell{
54		cell('界', style, false),
55		cell(0, style, true),
56		cell('x', renderer.DefaultStyle(), false),
57	}
58	mark := cell('!', renderer.Style{Foreground: 3}, false)
59	tests := []struct {
60		name string
61		rect domain.Rect
62		dst  renderer.Frame
63		want []renderer.Cell
64	}{
65		{
66			name: "left clipped continuation remains untouched",
67			rect: domain.Rect{X: -1, Y: 0, Width: 3, Height: 1},
68			dst:  markedFrame(3, 1, mark),
69			want: []renderer.Cell{mark, wide[2], mark},
70		},
71		{
72			name: "right clipped leader remains untouched",
73			rect: domain.Rect{X: 0, Y: 0, Width: 3, Height: 1},
74			dst:  markedFrame(1, 1, mark),
75			want: []renderer.Cell{mark},
76		},
77		{
78			name: "complete pair retains exact cells",
79			rect: domain.Rect{X: 1, Y: 0, Width: 3, Height: 1},
80			dst:  markedFrame(4, 1, mark),
81			want: []renderer.Cell{mark, wide[0], wide[1], wide[2]},
82		},
83	}
84	for _, tt := range tests {
85		t.Run(tt.name, func(t *testing.T) {
86			BlitFrame(tt.dst, tt.rect, FrameView{Rows: [][]renderer.Cell{wide}, Width: len(wide), Height: 1}, VerticalAnchorTop)
87			assertRowEqual(t, tt.dst, 0, tt.want)
88		})
89	}
90}
91
92func TestBlitFrameBoundsExtremeRectanglesToVisibleIntersection(t *testing.T) {
93	maxInt := int(^uint(0) >> 1)
94	minInt := -maxInt - 1
95	style := renderer.Style{Foreground: 9}
96	src := FrameView{Rows: [][]renderer.Cell{{cell('a', style, false)}}, Width: 1, Height: 1}
97	mark := cell('!', renderer.Style{Background: 4}, false)
98	tests := []struct {
99		name string
100		rect domain.Rect
101		want []renderer.Cell
102	}{
103		{"huge visible width copies only source span", domain.Rect{X: 0, Y: 0, Width: maxInt, Height: maxInt}, []renderer.Cell{src.Rows[0][0], mark}},
104		{"max coordinate overflow is outside frame", domain.Rect{X: maxInt - 1, Y: maxInt - 1, Width: maxInt, Height: maxInt}, []renderer.Cell{mark, mark}},
105		{"min coordinate overflow is outside frame", domain.Rect{X: minInt, Y: minInt, Width: maxInt, Height: maxInt}, []renderer.Cell{mark, mark}},
106	}
107	for _, tt := range tests {
108		t.Run(tt.name, func(t *testing.T) {
109			dst := markedFrame(2, 1, mark)
110			BlitFrame(dst, tt.rect, src, VerticalAnchorTop)
111			assertRowEqual(t, dst, 0, tt.want)
112		})
113	}
114}
115
116func TestBlitFrameUsesLogicalDestinationRowsAndPreservesClippedSentinels(t *testing.T) {
117	mark := cell('!', renderer.Style{Foreground: 3}, false)
118	dst := renderer.NewFrame(3, 3)
119	dst.ScrollUp(0, 2, 1)
120	for y := range dst.Height {
121		for x := range dst.Width {
122			dst.Set(x, y, mark)
123		}
124	}
125	src := FrameView{Rows: [][]renderer.Cell{
126		{cell('a', renderer.Style{Foreground: 1}, false), cell('b', renderer.Style{Foreground: 2}, false)},
127		{cell('c', renderer.Style{Foreground: 4}, false), cell('d', renderer.Style{Foreground: 5}, false)},
128	}, Width: 2, Height: 2}
129
130	BlitFrame(dst, domain.Rect{X: -1, Y: 1, Width: 3, Height: 2}, src, VerticalAnchorTop)
131
132	assertRowEqual(t, dst, 0, []renderer.Cell{mark, mark, mark})
133	assertRowEqual(t, dst, 1, []renderer.Cell{src.Rows[0][1], mark, mark})
134	assertRowEqual(t, dst, 2, []renderer.Cell{src.Rows[1][1], mark, mark})
135}
136
137func TestBlitFrameClipsEveryEdgeAndPreservesUntouchedSentinels(t *testing.T) {
138	mark := cell('!', renderer.Style{Foreground: 3}, false)
139	src := FrameView{Rows: [][]renderer.Cell{
140		{cell('a', renderer.DefaultStyle(), false), cell('b', renderer.DefaultStyle(), false), cell('c', renderer.DefaultStyle(), false)},
141		{cell('d', renderer.DefaultStyle(), false), cell('e', renderer.DefaultStyle(), false), cell('f', renderer.DefaultStyle(), false)},
142		{cell('g', renderer.DefaultStyle(), false), cell('h', renderer.DefaultStyle(), false), cell('i', renderer.DefaultStyle(), false)},
143	}, Width: 3, Height: 3}
144
145	leftTop := markedFrame(3, 3, mark)
146	BlitFrame(leftTop, domain.Rect{X: -1, Y: -1, Width: 3, Height: 3}, src, VerticalAnchorTop)
147	assertRowEqual(t, leftTop, 0, []renderer.Cell{src.Rows[1][1], src.Rows[1][2], mark})
148	assertRowEqual(t, leftTop, 1, []renderer.Cell{src.Rows[2][1], src.Rows[2][2], mark})
149	assertRowEqual(t, leftTop, 2, []renderer.Cell{mark, mark, mark})
150
151	rightBottom := markedFrame(3, 3, mark)
152	BlitFrame(rightBottom, domain.Rect{X: 2, Y: 2, Width: 3, Height: 3}, src, VerticalAnchorTop)
153	assertRowEqual(t, rightBottom, 0, []renderer.Cell{mark, mark, mark})
154	assertRowEqual(t, rightBottom, 1, []renderer.Cell{mark, mark, mark})
155	assertRowEqual(t, rightBottom, 2, []renderer.Cell{mark, mark, src.Rows[0][0]})
156}
157
158func TestBlitFrameInvalidNoOp(t *testing.T) {
159	dst := renderer.NewFrame(2, 2)
160	before := dst.Clone()
161	cases := []struct {
162		r domain.Rect
163		s FrameView
164		a VerticalAnchor
165	}{
166		{domain.Rect{}, FrameView{}, VerticalAnchorTop},
167		{domain.Rect{Width: 1, Height: 1}, FrameView{Width: 0, Height: 1}, VerticalAnchorTop},
168		{domain.Rect{Width: 1, Height: 1}, FrameView{Width: 1, Height: 1, Rows: [][]renderer.Cell{{}}}, VerticalAnchor(99)},
169	}
170	for _, tc := range cases {
171		BlitFrame(dst, tc.r, tc.s, tc.a)
172	}
173	if !framesEqual(dst, before) {
174		t.Fatal("invalid blit changed destination")
175	}
176}
177
178func markedFrame(width, height int, mark renderer.Cell) renderer.Frame {
179	frame := renderer.NewFrame(width, height)
180	for y := range height {
181		for x := range width {
182			frame.Set(x, y, mark)
183		}
184	}
185	return frame
186}
187
188func assertRowEqual(t *testing.T, frame renderer.Frame, y int, want []renderer.Cell) {
189	t.Helper()
190	if len(want) != frame.Width {
191		t.Fatalf("want row length %d, frame width %d", len(want), frame.Width)
192	}
193	for x := range want {
194		if got := frame.At(x, y); !got.Equal(want[x]) {
195			t.Fatalf("cell %d,%d = %#v, want %#v", x, y, got, want[x])
196		}
197	}
198}

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 12

Tags 11

↑↓ moveenter openesc close