resize_test.go RawHistoryFolder

1package layout
2
3import (
4	"testing"
5
6	"github.com/bnema/vev/internal/domain"
7	"github.com/stretchr/testify/require"
8)
9
10func TestResizeFocusTransfersSolvedExtent(t *testing.T) {
11	t.Parallel()
12
13	tests := []struct {
14		name  string
15		root  *Node
16		focus PaneID
17		axis  Axis
18		delta int
19		area  domain.Rect
20		want  map[PaneID]domain.Rect
21	}{
22		{
23			name:  "grow width takes from right neighbor",
24			root:  split(Horizontal, NewLeaf("a"), NewLeaf("b")),
25			focus: "a",
26			axis:  Width,
27			delta: 5,
28			area:  domain.Rect{Width: 51, Height: 2},
29			want: map[PaneID]domain.Rect{
30				"a": {Width: 30, Height: 2},
31				"b": {X: 31, Width: 20, Height: 2},
32			},
33		},
34		{
35			name:  "shrink width gives to right neighbor",
36			root:  split(Horizontal, weightedLeaf("a", 30), weightedLeaf("b", 20)),
37			focus: "a",
38			axis:  Width,
39			delta: -5,
40			area:  domain.Rect{Width: 51, Height: 2},
41			want: map[PaneID]domain.Rect{
42				"a": {Width: 25, Height: 2},
43				"b": {X: 26, Width: 25, Height: 2},
44			},
45		},
46		{
47			name: "grow height takes from bottom neighbor",
48			root: split(Vertical,
49				weightedLeaf("a", 4), weightedLeaf("b", 3),
50			),
51			focus: "a",
52			axis:  Height,
53			delta: 1,
54			area:  domain.Rect{Width: 20, Height: 8},
55			want: map[PaneID]domain.Rect{
56				"a": {Width: 20, Height: 5},
57				"b": {Y: 6, Width: 20, Height: 2},
58			},
59		},
60	}
61
62	for _, tt := range tests {
63		t.Run(tt.name, func(t *testing.T) {
64			tr := &Tree{Root: tt.root, Focus: tt.focus}
65			require.NoError(t, tr.ResizeFocus(tt.axis, tt.delta, tt.area))
66			placements, ok := Solve(tr.Root, tt.area)
67			require.True(t, ok)
68			require.Equal(t, tt.want, contents(placements))
69		})
70	}
71}
72
73func TestResizeFocusSelectsNearestTargetAndAdjacentSibling(t *testing.T) {
74	t.Parallel()
75
76	t.Run("nearest matching ancestor", func(t *testing.T) {
77		inner := split(Horizontal, NewLeaf("a"), NewLeaf("b"))
78		tr := &Tree{Root: split(Horizontal, weightedNode(inner, 2), weightedLeaf("c", 1)), Focus: "a"}
79		area := domain.Rect{Width: 122, Height: 2}
80		before, ok := Solve(tr.Root, area)
81		require.True(t, ok)
82
83		require.NoError(t, tr.ResizeFocus(Width, 5, area))
84		after, ok := Solve(tr.Root, area)
85		require.True(t, ok)
86		require.Equal(t, contents(before)["c"], contents(after)["c"])
87		require.Equal(t, contents(before)["a"].Width+5, contents(after)["a"].Width)
88		require.Equal(t, contents(before)["b"].Width-5, contents(after)["b"].Width)
89	})
90
91	t.Run("direct child subtree is resized as one target", func(t *testing.T) {
92		subtree := split(Vertical, NewLeaf("a"), NewLeaf("d"))
93		tr := &Tree{Root: split(Horizontal, subtree, NewLeaf("b")), Focus: "a"}
94		area := domain.Rect{Width: 51, Height: 5}
95		require.NoError(t, tr.ResizeFocus(Width, 5, area))
96		placements, ok := Solve(tr.Root, area)
97		require.True(t, ok)
98		require.Equal(t, 30, contents(placements)["a"].Width)
99		require.Equal(t, 30, contents(placements)["d"].Width)
100		require.Equal(t, 20, contents(placements)["b"].Width)
101	})
102
103	t.Run("stack is resized as one target", func(t *testing.T) {
104		stackNode := &Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("d")}, Expanded: "a"}
105		tr := &Tree{Root: split(Horizontal, stackNode, NewLeaf("b")), Focus: "d"}
106		area := domain.Rect{Width: 51, Height: 3}
107		require.NoError(t, tr.ResizeFocus(Width, 5, area))
108		placements, ok := Solve(tr.Root, area)
109		require.True(t, ok)
110		require.Equal(t, 30, contents(placements)["a"].Width)
111		var collapsedTitleWidth int
112		for _, placement := range placements {
113			if placement.ID == "d" {
114				collapsedTitleWidth = placement.TitleBar.Width
115			}
116		}
117		require.Equal(t, 30, collapsedTitleWidth)
118		require.Equal(t, 20, contents(placements)["b"].Width)
119	})
120
121	t.Run("right donor at minimum falls back to left", func(t *testing.T) {
122		tr := &Tree{Root: split(Horizontal,
123			weightedLeaf("a", 40), weightedLeaf("b", 20), weightedLeaf("c", 20),
124		), Focus: "b"}
125		area := domain.Rect{Width: 82, Height: 2}
126		require.NoError(t, tr.ResizeFocus(Width, 5, area))
127		placements, ok := Solve(tr.Root, area)
128		require.True(t, ok)
129		require.Equal(t, map[PaneID]int{"a": 35, "b": 25, "c": 20}, placementWidths(placements))
130	})
131
132	t.Run("last target falls back to left neighbor", func(t *testing.T) {
133		tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 30), weightedLeaf("b", 20)), Focus: "b"}
134		area := domain.Rect{Width: 51, Height: 2}
135		require.NoError(t, tr.ResizeFocus(Width, 5, area))
136		placements, ok := Solve(tr.Root, area)
137		require.True(t, ok)
138		require.Equal(t, map[PaneID]int{"a": 25, "b": 25}, placementWidths(placements))
139	})
140}
141
142func TestResizeFocusMinimumAndTransactionalFailures(t *testing.T) {
143	t.Parallel()
144
145	minInt := -int(^uint(0)>>1) - 1
146	tests := []struct {
147		name  string
148		tree  *Tree
149		axis  Axis
150		delta int
151		area  domain.Rect
152		err   error
153		want  map[PaneID]int
154	}{
155		{
156			name:  "partial growth uses every available donor cell",
157			tree:  &Tree{Root: split(Horizontal, weightedLeaf("a", 20), weightedLeaf("b", 22)), Focus: "a"},
158			axis:  Width,
159			delta: 5,
160			area:  domain.Rect{Width: 43, Height: 2},
161			want:  map[PaneID]int{"a": 22, "b": 20},
162		},
163		{
164			name:  "minimum integer clamps without overflow",
165			tree:  &Tree{Root: split(Horizontal, weightedLeaf("a", 30), weightedLeaf("b", 20)), Focus: "a"},
166			axis:  Width,
167			delta: minInt,
168			area:  domain.Rect{Width: 51, Height: 2},
169			want:  map[PaneID]int{"a": 20, "b": 30},
170		},
171		{
172			name: "partial growth stops at recursive donor minimum",
173			tree: &Tree{Root: split(Horizontal,
174				weightedLeaf("a", 20),
175				weightedNode(split(Horizontal, NewLeaf("b"), NewLeaf("d")), 43),
176			), Focus: "a"},
177			axis:  Width,
178			delta: 5,
179			area:  domain.Rect{Width: 64, Height: 2},
180			want:  map[PaneID]int{"a": 22, "b": 20, "d": 20},
181		},
182		{
183			name:  "no donor capacity",
184			tree:  &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"},
185			axis:  Width,
186			delta: 2,
187			area:  domain.Rect{Width: 41, Height: 2},
188			err:   ErrTooSmall,
189		},
190		{
191			name:  "target cannot shrink",
192			tree:  &Tree{Root: split(Horizontal, NewLeaf("a"), weightedLeaf("b", 2)), Focus: "a"},
193			axis:  Width,
194			delta: -2,
195			area:  domain.Rect{Width: 61, Height: 2},
196			err:   ErrTooSmall,
197		},
198		{
199			name:  "wrong axis",
200			tree:  &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"},
201			axis:  Height,
202			delta: 1,
203			area:  domain.Rect{Width: 41, Height: 2},
204			err:   ErrNotInSplit,
205		},
206		{
207			name:  "single pane",
208			tree:  NewTree("a"),
209			axis:  Width,
210			delta: 2,
211			area:  domain.Rect{Width: 20, Height: 2},
212			err:   ErrNotInSplit,
213		},
214		{
215			name:  "invalid area rolls back",
216			tree:  &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"},
217			axis:  Width,
218			delta: 2,
219			area:  domain.Rect{Width: 20, Height: 2},
220			err:   ErrTooSmall,
221		},
222	}
223
224	for _, tt := range tests {
225		t.Run(tt.name, func(t *testing.T) {
226			before := tt.tree.Clone()
227			err := tt.tree.ResizeFocus(tt.axis, tt.delta, tt.area)
228			if tt.err != nil {
229				require.ErrorIs(t, err, tt.err)
230				require.Equal(t, before, tt.tree, "failed resize must not mutate the tree")
231				return
232			}
233			require.NoError(t, err)
234			placements, ok := Solve(tt.tree.Root, tt.area)
235			require.True(t, ok)
236			require.Equal(t, tt.want, placementWidths(placements))
237		})
238	}
239}
240
241func TestResizeFocusRetainsProportionsAcrossTerminalSizes(t *testing.T) {
242	t.Parallel()
243
244	tr := &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"}
245	small := domain.Rect{Width: 81, Height: 2}
246	require.NoError(t, tr.ResizeFocus(Width, 10, small))
247
248	placements, ok := Solve(tr.Root, domain.Rect{Width: 161, Height: 2})
249	require.True(t, ok)
250	require.Equal(t, map[PaneID]int{"a": 100, "b": 60}, placementWidths(placements))
251	placements, ok = Solve(tr.Root, small)
252	require.True(t, ok)
253	require.Equal(t, map[PaneID]int{"a": 50, "b": 30}, placementWidths(placements))
254}
255
256func TestEqualize(t *testing.T) {
257	t.Parallel()
258
259	tests := []struct {
260		name string
261		tree *Tree
262		area domain.Rect
263		want map[PaneID]int
264	}{
265		{
266			name: "weighted siblings become equal",
267			tree: &Tree{Root: split(Horizontal, weightedLeaf("a", 3), weightedLeaf("b", 1)), Focus: "a"},
268			area: domain.Rect{Width: 81, Height: 2},
269			want: map[PaneID]int{"a": 40, "b": 40},
270		},
271		{
272			name: "recursive minimum constrains only the large child",
273			tree: &Tree{Root: split(Horizontal,
274				weightedNode(split(Horizontal, weightedLeaf("a", 5), weightedLeaf("b", 1)), 9),
275				weightedLeaf("c", 1), weightedLeaf("d", 1),
276			), Focus: "a"},
277			area: domain.Rect{Width: 102, Height: 2},
278			want: map[PaneID]int{"a": 20, "b": 20, "c": 30, "d": 29},
279		},
280	}
281
282	for _, tt := range tests {
283		t.Run(tt.name, func(t *testing.T) {
284			require.NoError(t, tt.tree.Equalize(tt.area))
285			placements, ok := Solve(tt.tree.Root, tt.area)
286			require.True(t, ok)
287			require.Equal(t, tt.want, placementWidths(placements))
288		})
289	}
290}
291
292func TestEqualizeFailureIsTransactional(t *testing.T) {
293	t.Parallel()
294
295	tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 3), weightedLeaf("b", 1)), Focus: "a"}
296	before := tr.Clone()
297	require.ErrorIs(t, tr.Equalize(domain.Rect{Width: 20, Height: 2}), ErrTooSmall)
298	require.Equal(t, before, tr)
299}
300
301func TestCanResize(t *testing.T) {
302	t.Parallel()
303
304	tests := []struct {
305		name string
306		tree *Tree
307		want bool
308	}{
309		{name: "single pane", tree: NewTree("a"), want: false},
310		{name: "focused split child", tree: &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "a"}, want: true},
311		{name: "focused stack member under split", tree: &Tree{Root: split(Horizontal, &Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("b")}, Expanded: "a"}, NewLeaf("c")), Focus: "b"}, want: true},
312		{name: "stack without split", tree: &Tree{Root: &Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("b")}, Expanded: "a"}, Focus: "a"}, want: false},
313		{name: "missing focus", tree: &Tree{Root: split(Horizontal, NewLeaf("a"), NewLeaf("b")), Focus: "missing"}, want: false},
314	}
315
316	for _, tt := range tests {
317		t.Run(tt.name, func(t *testing.T) {
318			require.Equal(t, tt.want, tt.tree.CanResize())
319		})
320	}
321}

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