v0.2.0 80c61e3

weights_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 TestWeightedSplitAllocation(t *testing.T) {
11	t.Parallel()
12
13	tests := []struct {
14		name string
15		root *Node
16		area domain.Rect
17		want map[PaneID]int
18	}{
19		{
20			name: "zero weights preserve equal split and remainder order",
21			root: split(Horizontal, NewLeaf("a"), NewLeaf("b"), NewLeaf("c")),
22			area: domain.Rect{Width: 64, Height: 2},
23			want: map[PaneID]int{"a": 21, "b": 21, "c": 20},
24		},
25		{
26			name: "two to one weights",
27			root: split(Horizontal, weightedLeaf("a", 2), weightedLeaf("b", 1)),
28			area: domain.Rect{Width: 61, Height: 2},
29			want: map[PaneID]int{"a": 40, "b": 20},
30		},
31		{
32			name: "largest remainder ties use child order",
33			root: split(Horizontal, weightedLeaf("a", 1), weightedLeaf("b", 1), weightedLeaf("c", 2)),
34			area: domain.Rect{Width: 84, Height: 2},
35			want: map[PaneID]int{"a": 21, "b": 20, "c": 41},
36		},
37		{
38			name: "minimum constrained children pin and remainder redistributes proportionally",
39			root: split(Horizontal, weightedLeaf("a", 1), weightedLeaf("b", 1), weightedLeaf("c", 8)),
40			area: domain.Rect{Width: 102, Height: 2},
41			want: map[PaneID]int{"a": 20, "b": 20, "c": 60},
42		},
43		{
44			name: "negative and zero in memory weights use default",
45			root: split(Horizontal, weightedLeaf("a", -4), weightedLeaf("b", 0), weightedLeaf("c", 2)),
46			area: domain.Rect{Width: 82, Height: 2},
47			want: map[PaneID]int{"a": 20, "b": 20, "c": 40},
48		},
49	}
50
51	for _, tt := range tests {
52		t.Run(tt.name, func(t *testing.T) {
53			placements, ok := Solve(tt.root, tt.area)
54			require.True(t, ok)
55			for id, want := range tt.want {
56				require.Equal(t, want, contents(placements)[id].Width)
57			}
58		})
59	}
60}
61
62func TestDistributePinsChildrenFromOneQuotaSnapshot(t *testing.T) {
63	t.Parallel()
64
65	allocations, ok := distribute(100, []int{20, 33, 42}, []float64{1, 4, 5})
66	require.True(t, ok)
67	require.Equal(t, []int{20, 36, 44}, allocations)
68	require.Equal(t, 100, allocations[0]+allocations[1]+allocations[2])
69}
70
71func TestWeightedSplitNestedMinimumRedistributesWithFreshDenominator(t *testing.T) {
72	t.Parallel()
73
74	root := split(Horizontal,
75		weightedNode(split(Horizontal, NewLeaf("a"), NewLeaf("b")), 2),
76		weightedLeaf("c", 2),
77		weightedLeaf("d", 6),
78	)
79
80	rects, ok := splitChildRects(root, domain.Rect{Width: 132, Height: 2})
81	require.True(t, ok)
82	require.Equal(t, []int{41, 22, 67}, []int{rects[0].Width, rects[1].Width, rects[2].Width})
83}
84
85func TestWeightedSplitRecursiveMinima(t *testing.T) {
86	t.Parallel()
87
88	tests := []struct {
89		name   string
90		root   *Node
91		area   domain.Rect
92		wantOK bool
93		want   map[PaneID]domain.Rect
94	}{
95		{
96			name: "same axis nested minima sum children and separators",
97			root: split(Horizontal,
98				weightedNode(split(Horizontal, NewLeaf("a"), NewLeaf("b")), 1),
99				weightedLeaf("c", 9),
100			),
101			area:   domain.Rect{Width: 102, Height: 2},
102			wantOK: true,
103			want: map[PaneID]domain.Rect{
104				"a": {Width: 20, Height: 2},
105				"b": {X: 21, Width: 20, Height: 2},
106				"c": {X: 42, Width: 60, Height: 2},
107			},
108		},
109		{
110			name: "cross axis nested minima take maximum",
111			root: split(Horizontal,
112				split(Vertical, NewLeaf("a"), NewLeaf("b")),
113				NewLeaf("c"),
114			),
115			area:   domain.Rect{Width: 41, Height: 5},
116			wantOK: true,
117		},
118		{
119			name: "stack vertical minimum is titles plus one content row",
120			root: split(Vertical,
121				weightedNode(&Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("b"), NewLeaf("c")}, Expanded: "b"}, 1),
122				weightedLeaf("d", 9),
123			),
124			area:   domain.Rect{Width: 20, Height: 7},
125			wantOK: true,
126		},
127		{
128			name: "insufficient same axis recursive minimum fails without placements",
129			root: split(Horizontal,
130				split(Horizontal, NewLeaf("a"), NewLeaf("b")),
131				NewLeaf("c"),
132			),
133			area:   domain.Rect{Width: 61, Height: 2},
134			wantOK: false,
135		},
136		{
137			name: "insufficient cross axis recursive minimum fails without placements",
138			root: split(Horizontal,
139				split(Vertical, NewLeaf("a"), NewLeaf("b")),
140				NewLeaf("c"),
141			),
142			area:   domain.Rect{Width: 41, Height: 4},
143			wantOK: false,
144		},
145	}
146
147	for _, tt := range tests {
148		t.Run(tt.name, func(t *testing.T) {
149			placements, ok := Solve(tt.root, tt.area)
150			require.Equal(t, tt.wantOK, ok)
151			if !tt.wantOK {
152				require.Nil(t, placements)
153				return
154			}
155			if tt.want != nil {
156				require.Equal(t, tt.want, contents(placements))
157			}
158		})
159	}
160}
161
162func TestWeightSameAxisSplitNormalizesSolvedExtentsBeforeInsertion(t *testing.T) {
163	t.Parallel()
164
165	tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 2), weightedLeaf("b", 1)), Focus: "a"}
166	area := domain.Rect{Width: 82, Height: 2}
167	require.NoError(t, tr.Split("b", Right, true, "c", area))
168
169	require.Equal(t, 54.0, tr.Root.Children[0].Weight)
170	require.Equal(t, 27.0, tr.Root.Children[1].Weight)
171	require.Zero(t, tr.Root.Children[2].Weight)
172	placements, ok := Solve(tr.Root, area)
173	require.True(t, ok)
174	require.Equal(t, map[PaneID]int{"a": 40, "b": 20, "c": 20}, placementWidths(placements))
175}
176
177func TestWeightCrossAxisSplitTransfersParentShareAndClearsMembers(t *testing.T) {
178	t.Parallel()
179
180	tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 3), weightedLeaf("b", 1)), Focus: "a"}
181	require.NoError(t, tr.Split("a", Down, true, "c", domain.Rect{Width: 81, Height: 5}))
182
183	wrapper := tr.Root.Children[0]
184	require.Equal(t, Vertical, wrapper.Dir)
185	require.Equal(t, 3.0, wrapper.Weight)
186	require.Zero(t, wrapper.Children[0].Weight)
187	require.Zero(t, wrapper.Children[1].Weight)
188	placements, ok := Solve(tr.Root, domain.Rect{Width: 81, Height: 5})
189	require.True(t, ok)
190	require.Equal(t, 60, contents(placements)["a"].Width)
191	require.Equal(t, 20, contents(placements)["b"].Width)
192}
193
194func TestWeightStackNewTransfersParentShareAndClearsMembers(t *testing.T) {
195	t.Parallel()
196
197	tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 3), weightedLeaf("b", 1)), Focus: "a"}
198	require.NoError(t, tr.StackNew("a", "c", domain.Rect{Width: 81, Height: 3}))
199
200	stackNode := tr.Root.Children[0]
201	require.Equal(t, Stack, stackNode.Kind)
202	require.Equal(t, 3.0, stackNode.Weight)
203	require.Zero(t, stackNode.Children[0].Weight)
204	require.Zero(t, stackNode.Children[1].Weight)
205	placements, ok := Solve(tr.Root, domain.Rect{Width: 81, Height: 3})
206	require.True(t, ok)
207	require.Equal(t, 60, contents(placements)["c"].Width)
208	require.Equal(t, 20, contents(placements)["b"].Width)
209}
210
211func TestWeightStackNewClearsExistingStackMemberWeights(t *testing.T) {
212	t.Parallel()
213
214	tr := &Tree{Root: &Node{Kind: Stack, Children: []*Node{weightedLeaf("a", 3), weightedLeaf("b", 1)}, Expanded: "a", Weight: 4}, Focus: "a"}
215	require.NoError(t, tr.StackNew("a", "c", domain.Rect{Width: 20, Height: 4}))
216
217	require.Equal(t, 4.0, tr.Root.Weight)
218	for _, child := range tr.Root.Children {
219		require.Zero(t, child.Weight)
220	}
221}
222
223func TestWeightToggleStackClearsMemberWeights(t *testing.T) {
224	t.Parallel()
225
226	tests := []struct {
227		name     string
228		root     *Node
229		wantKind Kind
230	}{
231		{
232			name: "split to stack",
233			root: &Node{Kind: Split, Dir: Vertical, Weight: 3, Children: []*Node{
234				weightedLeaf("a", 2), weightedLeaf("b", 1),
235			}},
236			wantKind: Stack,
237		},
238		{
239			name: "stack to split",
240			root: &Node{Kind: Stack, Weight: 3, Children: []*Node{
241				weightedLeaf("a", 2), weightedLeaf("b", 1),
242			}, Expanded: "a"},
243			wantKind: Split,
244		},
245	}
246
247	for _, tt := range tests {
248		t.Run(tt.name, func(t *testing.T) {
249			tr := &Tree{Root: split(Horizontal, tt.root, weightedLeaf("c", 1)), Focus: "a"}
250			area := domain.Rect{Width: 81, Height: 5}
251			require.NoError(t, tr.ToggleStack("a", area))
252			container := tr.Root.Children[0]
253			require.Equal(t, tt.wantKind, container.Kind)
254			require.Equal(t, 3.0, container.Weight)
255			for _, child := range container.Children {
256				require.Zero(t, child.Weight)
257			}
258			placements, ok := Solve(tr.Root, area)
259			require.True(t, ok)
260			require.Equal(t, 60, contents(placements)["a"].Width)
261			require.Equal(t, 20, contents(placements)["c"].Width)
262			if tt.wantKind == Split {
263				require.Equal(t, map[PaneID]int{"a": 2, "b": 2, "c": 5}, placementHeights(placements))
264			}
265		})
266	}
267}
268
269func TestWeightClosePromotionTransfersContainerShare(t *testing.T) {
270	t.Parallel()
271
272	tests := []struct {
273		name      string
274		container *Node
275	}{
276		{
277			name:      "split",
278			container: split(Vertical, NewLeaf("a"), NewLeaf("b")),
279		},
280		{
281			name: "stack",
282			container: &Node{Kind: Stack, Children: []*Node{
283				NewLeaf("a"), NewLeaf("b"),
284			}, Expanded: "a"},
285		},
286	}
287
288	for _, tt := range tests {
289		t.Run(tt.name, func(t *testing.T) {
290			tr := &Tree{Root: split(Horizontal,
291				weightedNode(tt.container, 3),
292				weightedLeaf("c", 1),
293			), Focus: "a"}
294			area := domain.Rect{Width: 81, Height: 5}
295			before, ok := Solve(tr.Root, area)
296			require.True(t, ok)
297			require.Equal(t, 60, contents(before)["a"].Width)
298
299			require.NoError(t, tr.Close("a"))
300			require.Equal(t, PaneID("b"), tr.Root.Children[0].Leaf)
301			require.Equal(t, 3.0, tr.Root.Children[0].Weight)
302			after, ok := Solve(tr.Root, area)
303			require.True(t, ok)
304			require.Equal(t, 60, contents(after)["b"].Width)
305			require.Equal(t, 20, contents(after)["c"].Width)
306		})
307	}
308}
309
310func TestWeightSplitStackWrapperTransfersShare(t *testing.T) {
311	t.Parallel()
312
313	tr := &Tree{Root: &Node{Kind: Stack, Weight: 4, Children: []*Node{
314		weightedLeaf("a", 2), weightedLeaf("b", 1),
315	}, Expanded: "a"}, Focus: "a"}
316	require.NoError(t, tr.Split("a", Right, true, "c", domain.Rect{Width: 41, Height: 3}))
317
318	require.Equal(t, 4.0, tr.Root.Weight)
319	require.Zero(t, tr.Root.Children[0].Weight)
320	require.Equal(t, Stack, tr.Root.Children[0].Kind)
321	for _, member := range tr.Root.Children[0].Children {
322		require.Zero(t, member.Weight)
323	}
324}
325
326func TestWeightConsumeOrExpelPaneSemantics(t *testing.T) {
327	t.Parallel()
328
329	tests := []struct {
330		name   string
331		tree   *Tree
332		target PaneID
333		dir    Direction
334		area   domain.Rect
335		check  func(*testing.T, *Tree, []Placement, []Placement)
336	}{
337		{
338			name: "horizontal insertion preserves normalized sibling shares and gives expelled column fair-share weight",
339			tree: &Tree{Root: weightedNode(horizontal(
340				weightedNode(vertical("a", "b"), 3),
341				weightedLeaf("c", 1),
342			), 11), Focus: "b"},
343			target: "b",
344			dir:    Right,
345			area:   domain.Rect{Width: 202, Height: 5},
346			check: func(t *testing.T, tree *Tree, before, after []Placement) {
347				require.Equal(t, map[PaneID]int{"a": 151, "b": 151, "c": 50}, placementWidths(before))
348				require.Equal(t, 11.0, tree.Root.Weight)
349				require.Equal(t, []float64{151, 100.5, 50}, childWeights(tree.Root))
350				require.Equal(t, []PaneID{"a", "b", "c"}, LeafIDs(tree.Root))
351				require.Equal(t, map[PaneID]int{"a": 100, "b": 67, "c": 33}, placementWidths(after))
352			},
353		},
354		{
355			name: "vertical consume preserves solved member shares and gives moved pane fair-share weight",
356			tree: &Tree{Root: weightedNode(horizontal(
357				weightedNode(verticalNodes(weightedLeaf("a", 3), weightedLeaf("b", 1)), 2),
358				weightedLeaf("c", 1),
359			), 9), Focus: "c"},
360			target: "c",
361			dir:    Left,
362			area:   domain.Rect{Width: 101, Height: 20},
363			check: func(t *testing.T, tree *Tree, before, after []Placement) {
364				require.Equal(t, map[PaneID]int{"a": 14, "b": 5, "c": 20}, placementHeights(before))
365				require.Equal(t, Vertical, tree.Root.Dir)
366				require.Equal(t, 9.0, tree.Root.Weight, "root promotion retains the removed horizontal wrapper share")
367				require.Equal(t, []float64{14, 5, 9.5}, childWeights(tree.Root))
368				require.Equal(t, map[PaneID]int{"a": 9, "b": 3, "c": 6}, placementHeights(after))
369			},
370		},
371		{
372			name: "stack consume clears member weights and preserves normalized outer column share",
373			tree: &Tree{Root: horizontal(
374				weightedLeaf("x", 1),
375				weightedNode(&Node{Kind: Stack, Children: []*Node{weightedLeaf("a", 8), weightedLeaf("b", 4)}, Expanded: "a"}, 3),
376				weightedLeaf("c", 1),
377			), Focus: "c"},
378			target: "c",
379			dir:    Left,
380			area:   domain.Rect{Width: 103, Height: 4},
381			check: func(t *testing.T, tree *Tree, before, after []Placement) {
382				require.Equal(t, map[PaneID]int{"x": 20, "a": 61, "b": 0, "c": 20}, placementWidths(before))
383				require.Len(t, tree.Root.Children, 2)
384				stackColumn := tree.Root.Children[1]
385				require.Equal(t, Stack, stackColumn.Kind)
386				require.Equal(t, 61.0, stackColumn.Weight)
387				require.Equal(t, PaneID("c"), stackColumn.Expanded)
388				require.Equal(t, []float64{0, 0, 0}, childWeights(stackColumn))
389				require.Equal(t, 77, placementSpanWidth(after, "a"))
390				require.Equal(t, 77, placementSpanWidth(after, "b"))
391				require.Equal(t, 77, placementSpanWidth(after, "c"))
392			},
393		},
394		{
395			name: "stack expel clears remaining weights and repairs expanded member",
396			tree: &Tree{Root: weightedNode(&Node{Kind: Stack, Children: []*Node{
397				weightedLeaf("a", 5), weightedLeaf("b", 4), weightedLeaf("c", 3),
398			}, Expanded: "b"}, 7), Focus: "b"},
399			target: "b",
400			dir:    Right,
401			area:   domain.Rect{Width: 101, Height: 4},
402			check: func(t *testing.T, tree *Tree, _, after []Placement) {
403				require.Equal(t, Horizontal, tree.Root.Dir)
404				require.Equal(t, 7.0, tree.Root.Weight)
405				require.Equal(t, []float64{0, 0}, childWeights(tree.Root))
406				stackColumn := tree.Root.Children[0]
407				require.Equal(t, PaneID("a"), stackColumn.Expanded)
408				require.Equal(t, []float64{0, 0}, childWeights(stackColumn))
409				require.Equal(t, 50, placementSpanWidth(after, "a"))
410				require.Equal(t, 50, placementSpanWidth(after, "c"))
411				require.Equal(t, 50, placementSpanWidth(after, "b"))
412			},
413		},
414		{
415			name: "column collapse copies normalized container weight to promoted leaf",
416			tree: &Tree{Root: horizontal(
417				weightedLeaf("x", 1),
418				weightedNode(vertical("a", "b"), 3),
419				weightedLeaf("y", 1),
420			), Focus: "a"},
421			target: "a",
422			dir:    Left,
423			area:   domain.Rect{Width: 103, Height: 5},
424			check: func(t *testing.T, tree *Tree, _, after []Placement) {
425				require.Equal(t, []PaneID{"x", "a", "b", "y"}, LeafIDs(tree.Root))
426				require.Equal(t, Leaf, tree.Root.Children[2].Kind)
427				require.Equal(t, PaneID("b"), tree.Root.Children[2].Leaf)
428				require.Equal(t, 61.0, tree.Root.Children[2].Weight)
429				weights := childWeights(tree.Root)
430				require.Equal(t, 20.0, weights[0])
431				require.InDelta(t, 101.0/3.0, weights[1], 1e-9)
432				require.Equal(t, 61.0, weights[2])
433				require.Equal(t, 20.0, weights[3])
434				require.Equal(t, map[PaneID]int{"x": 20, "a": 21, "b": 39, "y": 20}, placementWidths(after))
435			},
436		},
437		{
438			name:   "sole-column expel creates equal default horizontal children and retains wrapper weight",
439			tree:   &Tree{Root: weightedNode(verticalNodes(weightedLeaf("a", 3), weightedLeaf("b", 1)), 9), Focus: "a"},
440			target: "a",
441			dir:    Left,
442			area:   domain.Rect{Width: 101, Height: 5},
443			check: func(t *testing.T, tree *Tree, _, after []Placement) {
444				require.Equal(t, Horizontal, tree.Root.Dir)
445				require.Equal(t, 9.0, tree.Root.Weight)
446				require.Equal(t, []float64{0, 0}, childWeights(tree.Root))
447				require.Equal(t, map[PaneID]int{"a": 50, "b": 50}, placementWidths(after))
448			},
449		},
450		{
451			name: "expel from equal columns yields three homogeneous columns",
452			tree: &Tree{Root: horizontal(
453				NewLeaf("a"),
454				verticalNodes(NewLeaf("b"), NewLeaf("c")),
455			), Focus: "c"},
456			target: "c",
457			dir:    Right,
458			area:   domain.Rect{Width: 122, Height: 10},
459			check: func(t *testing.T, tree *Tree, _, after []Placement) {
460				require.Equal(t, []PaneID{"a", "b", "c"}, LeafIDs(tree.Root))
461				require.Equal(t, map[PaneID]int{"a": 40, "b": 40, "c": 40}, placementWidths(after))
462			},
463		},
464	}
465
466	for _, tt := range tests {
467		t.Run(tt.name, func(t *testing.T) {
468			beforeIDs := LeafIDs(tt.tree.Root)
469			requireUniquePaneIDs(t, beforeIDs)
470			before, solved := Solve(tt.tree.Root, tt.area)
471			require.True(t, solved, "weight fixture must solve before surgery")
472
473			changed, err := tt.tree.ConsumeOrExpelPane(tt.target, tt.dir, tt.area)
474			require.NoError(t, err)
475			require.True(t, changed)
476			require.Equal(t, tt.target, tt.tree.Focus)
477
478			afterIDs := LeafIDs(tt.tree.Root)
479			require.ElementsMatch(t, beforeIDs, afterIDs)
480			require.Len(t, afterIDs, len(beforeIDs))
481			requireUniquePaneIDs(t, afterIDs)
482			after, solved := Solve(tt.tree.Root, tt.area)
483			require.True(t, solved, "weighted result must solve")
484			tt.check(t, tt.tree, before, after)
485		})
486	}
487}
488
489func childWeights(node *Node) []float64 {
490	weights := make([]float64, len(node.Children))
491	for i, child := range node.Children {
492		weights[i] = child.Weight
493	}
494	return weights
495}
496
497func placementSpanWidth(placements []Placement, id PaneID) int {
498	for _, placement := range placements {
499		if placement.ID != id {
500			continue
501		}
502		if placement.Content.Width > 0 {
503			return placement.Content.Width
504		}
505		return placement.TitleBar.Width
506	}
507	return 0
508}
509
510func TestWeightCloneCopiesWeightIndependently(t *testing.T) {
511	t.Parallel()
512
513	tr := &Tree{Root: split(Horizontal, weightedLeaf("a", 2), weightedLeaf("b", 1)), Focus: "a"}
514	clone := tr.Clone()
515	require.Equal(t, tr, clone)
516	clone.Root.Children[0].Weight = 9
517	require.Equal(t, 2.0, tr.Root.Children[0].Weight)
518}
519
520func split(dir SplitDir, children ...*Node) *Node {
521	return &Node{Kind: Split, Dir: dir, Children: children}
522}
523
524func weightedLeaf(id PaneID, weight float64) *Node {
525	return &Node{Kind: Leaf, Leaf: id, Weight: weight}
526}
527
528func weightedNode(n *Node, weight float64) *Node {
529	n.Weight = weight
530	return n
531}
532
533func placementWidths(placements []Placement) map[PaneID]int {
534	out := make(map[PaneID]int, len(placements))
535	for _, placement := range placements {
536		out[placement.ID] = placement.Content.Width
537	}
538	return out
539}
540
541func placementHeights(placements []Placement) map[PaneID]int {
542	out := make(map[PaneID]int, len(placements))
543	for _, placement := range placements {
544		out[placement.ID] = placement.Content.Height
545	}
546	return out
547}

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