layout_test.go RawHistoryFolder

1package layout
2
3import (
4	"errors"
5	"testing"
6
7	"github.com/bnema/vev/internal/domain"
8	"github.com/stretchr/testify/require"
9)
10
11func TestSolveSplitGeometry(t *testing.T) {
12	t.Parallel()
13
14	tests := []struct {
15		name string
16		root *Node
17		area domain.Rect
18		want map[PaneID]domain.Rect
19	}{
20		{
21			name: "horizontal remainder to first children",
22			root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{NewLeaf("a"), NewLeaf("b"), NewLeaf("c")}},
23			area: domain.Rect{Width: 64, Height: 5},
24			want: map[PaneID]domain.Rect{"a": {Width: 21, Height: 5}, "b": {X: 22, Width: 21, Height: 5}, "c": {X: 44, Width: 20, Height: 5}},
25		},
26		{
27			name: "vertical remainder to first children",
28			root: &Node{Kind: Split, Dir: Vertical, Children: []*Node{NewLeaf("a"), NewLeaf("b")}},
29			area: domain.Rect{Width: 20, Height: 7},
30			want: map[PaneID]domain.Rect{"a": {Width: 20, Height: 3}, "b": {Y: 4, Width: 20, Height: 3}},
31		},
32	}
33
34	for _, tt := range tests {
35		t.Run(tt.name, func(t *testing.T) {
36			got, ok := Solve(tt.root, tt.area)
37			require.True(t, ok)
38			require.Len(t, got, len(tt.want))
39			for _, p := range got {
40				require.Equal(t, tt.want[p.ID], p.Content)
41			}
42		})
43	}
44}
45
46func TestRepeatedSplitHalvesFocusedPane(t *testing.T) {
47	t.Parallel()
48
49	tests := []struct {
50		name   string
51		dir    Direction
52		area   domain.Rect
53		first  map[PaneID]domain.Rect
54		second map[PaneID]domain.Rect
55	}{
56		{
57			name: "horizontal",
58			dir:  Right,
59			area: domain.Rect{Width: 123, Height: 5},
60			first: map[PaneID]domain.Rect{
61				"a": {Width: 61, Height: 5},
62				"b": {X: 62, Width: 61, Height: 5},
63			},
64			second: map[PaneID]domain.Rect{
65				"a": {Width: 61, Height: 5},
66				"b": {X: 62, Width: 30, Height: 5},
67				"c": {X: 93, Width: 30, Height: 5},
68			},
69		},
70		{
71			name: "vertical",
72			dir:  Down,
73			area: domain.Rect{Width: 40, Height: 15},
74			first: map[PaneID]domain.Rect{
75				"a": {Width: 40, Height: 7},
76				"b": {Y: 8, Width: 40, Height: 7},
77			},
78			second: map[PaneID]domain.Rect{
79				"a": {Width: 40, Height: 7},
80				"b": {Y: 8, Width: 40, Height: 3},
81				"c": {Y: 12, Width: 40, Height: 3},
82			},
83		},
84	}
85
86	for _, tt := range tests {
87		t.Run(tt.name, func(t *testing.T) {
88			tr := NewTree("a")
89			require.NoError(t, tr.Split("a", tt.dir, true, "b", tt.area))
90			first, ok := Solve(tr.Root, tt.area)
91			require.True(t, ok)
92			require.Equal(t, tt.first, contents(first))
93
94			require.NoError(t, tr.Split("b", tt.dir, true, "c", tt.area))
95			second, ok := Solve(tr.Root, tt.area)
96			require.True(t, ok)
97			require.Equal(t, tt.second, contents(second))
98		})
99	}
100}
101
102func TestSolveNestedTrees(t *testing.T) {
103	t.Parallel()
104	root := &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
105		NewLeaf("left"),
106		{Kind: Split, Dir: Vertical, Children: []*Node{NewLeaf("top"), NewLeaf("bottom")}},
107	}}
108	got, ok := Solve(root, domain.Rect{Width: 61, Height: 5})
109	require.True(t, ok)
110	require.Equal(t, map[PaneID]domain.Rect{
111		"left":   {Width: 30, Height: 5},
112		"top":    {X: 31, Width: 30, Height: 2},
113		"bottom": {X: 31, Y: 3, Width: 30, Height: 2},
114	}, contents(got))
115}
116
117func TestSolveStackFitAndOverflow(t *testing.T) {
118	t.Parallel()
119	stack := &Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("b"), NewLeaf("c")}, Expanded: "b"}
120	got, ok := Solve(stack, domain.Rect{Width: 20, Height: 4})
121	require.True(t, ok)
122	require.Equal(t, []Placement{
123		{ID: "a", TitleBar: domain.Rect{Width: 20, Height: 1}, Collapsed: true, InStack: true},
124		{ID: "b", Content: domain.Rect{Y: 1, Width: 20, Height: 2}, InStack: true},
125		{ID: "c", TitleBar: domain.Rect{Y: 3, Width: 20, Height: 1}, Collapsed: true, InStack: true},
126	}, got)
127
128	_, ok = Solve(stack, domain.Rect{Width: 20, Height: 3})
129	require.False(t, ok)
130}
131
132func TestSolveStackExpandedMemberHasNoTitleBar(t *testing.T) {
133	t.Parallel()
134	area := domain.Rect{X: 5, Y: 2, Width: 20, Height: 5}
135	tests := []struct {
136		name     string
137		expanded PaneID
138		want     []Placement
139	}{
140		{
141			name:     "edge member expanded",
142			expanded: "a",
143			want: []Placement{
144				{ID: "a", Content: domain.Rect{X: 5, Y: 2, Width: 20, Height: 3}, InStack: true},
145				{ID: "b", TitleBar: domain.Rect{X: 5, Y: 5, Width: 20, Height: 1}, Collapsed: true, InStack: true},
146				{ID: "c", TitleBar: domain.Rect{X: 5, Y: 6, Width: 20, Height: 1}, Collapsed: true, InStack: true},
147			},
148		},
149		{
150			name:     "middle member expanded",
151			expanded: "b",
152			want: []Placement{
153				{ID: "a", TitleBar: domain.Rect{X: 5, Y: 2, Width: 20, Height: 1}, Collapsed: true, InStack: true},
154				{ID: "b", Content: domain.Rect{X: 5, Y: 3, Width: 20, Height: 3}, InStack: true},
155				{ID: "c", TitleBar: domain.Rect{X: 5, Y: 6, Width: 20, Height: 1}, Collapsed: true, InStack: true},
156			},
157		},
158		{
159			name:     "other edge member expanded",
160			expanded: "c",
161			want: []Placement{
162				{ID: "a", TitleBar: domain.Rect{X: 5, Y: 2, Width: 20, Height: 1}, Collapsed: true, InStack: true},
163				{ID: "b", TitleBar: domain.Rect{X: 5, Y: 3, Width: 20, Height: 1}, Collapsed: true, InStack: true},
164				{ID: "c", Content: domain.Rect{X: 5, Y: 4, Width: 20, Height: 3}, InStack: true},
165			},
166		},
167	}
168	for _, tt := range tests {
169		t.Run(tt.name, func(t *testing.T) {
170			stack := &Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("b"), NewLeaf("c")}, Expanded: tt.expanded}
171			got, ok := Solve(stack, area)
172			require.True(t, ok)
173			require.Equal(t, tt.want, got)
174
175			// Invariant: total consumed rows equal area.Height, and rows are
176			// contiguous and non-overlapping across title bars and content.
177			rows := make(map[int]PaneID, area.Height)
178			for _, p := range got {
179				if p.TitleBar.Height > 0 {
180					require.Zero(t, p.Content.Height, "expanded member must not have both a title bar and content")
181					for y := p.TitleBar.Y; y < p.TitleBar.Y+p.TitleBar.Height; y++ {
182						_, dup := rows[y]
183						require.False(t, dup, "row %d already occupied", y)
184						rows[y] = p.ID
185					}
186				} else {
187					require.Equal(t, tt.expanded, p.ID, "only the expanded member may omit its title bar")
188					for y := p.Content.Y; y < p.Content.Y+p.Content.Height; y++ {
189						_, dup := rows[y]
190						require.False(t, dup, "row %d already occupied", y)
191						rows[y] = p.ID
192					}
193				}
194			}
195			require.Len(t, rows, area.Height)
196			for y := area.Y; y < area.Y+area.Height; y++ {
197				_, ok := rows[y]
198				require.True(t, ok, "row %d not covered", y)
199			}
200		})
201	}
202}
203
204func TestSplitStackedLeafSplitsWholeStack(t *testing.T) {
205	t.Parallel()
206	tr := &Tree{
207		Root:  &Node{Kind: Stack, Children: []*Node{NewLeaf("one"), NewLeaf("two")}, Expanded: "two"},
208		Focus: "two",
209	}
210	area := domain.Rect{Width: 41, Height: 4}
211
212	require.NoError(t, tr.Split("two", Right, true, "three", area))
213	placements, ok := Solve(tr.Root, area)
214	require.True(t, ok)
215	require.Equal(t, PaneID("three"), tr.Focus)
216	require.Equal(t, domain.Rect{Y: 1, Width: 20, Height: 3}, contents(placements)["two"], "existing stack should remain visible on the left")
217	require.Equal(t, domain.Rect{X: 21, Width: 20, Height: 4}, contents(placements)["three"], "new split pane should appear to the right of the stack")
218}
219
220func TestStackNewEmptyTreeReturnsError(t *testing.T) {
221	t.Parallel()
222	tr := &Tree{}
223	require.ErrorIs(t, tr.StackNew("missing", "new", domain.Rect{Width: 20, Height: 2}), ErrNotFound)
224	require.Nil(t, tr.Root)
225	require.Empty(t, tr.Focus)
226}
227
228func TestSplitEmptyTreeReturnsError(t *testing.T) {
229	t.Parallel()
230	tr := &Tree{}
231	require.ErrorIs(t, tr.Split("missing", Right, true, "new", domain.Rect{Width: 41, Height: 2}), ErrNotFound)
232	require.Nil(t, tr.Root)
233	require.Empty(t, tr.Focus)
234}
235
236func TestNilTreeCloneIsSafe(t *testing.T) {
237	t.Parallel()
238	var tr *Tree
239	require.Nil(t, tr.clone())
240	require.Nil(t, tr.Clone())
241}
242
243func TestTreeCloneReturnsIndependentCopy(t *testing.T) {
244	t.Parallel()
245	tr := &Tree{
246		Root: &Node{
247			Kind: Split,
248			Dir:  Horizontal,
249			Children: []*Node{
250				NewLeaf("a"),
251				NewLeaf("b"),
252			},
253		},
254		Focus: "b",
255	}
256
257	clone := tr.Clone()
258	require.Equal(t, tr, clone)
259
260	clone.Root.Children[0].Leaf = "changed"
261	clone.Focus = "changed"
262	require.Equal(t, PaneID("a"), tr.Root.Children[0].Leaf)
263	require.Equal(t, PaneID("b"), tr.Focus)
264}
265
266func TestTooSmallRefusalsDoNotMutate(t *testing.T) {
267	t.Parallel()
268	tests := []struct {
269		name string
270		op   func(*Tree) error
271	}{
272		{
273			name: "split refuses below min width",
274			op: func(tr *Tree) error {
275				return tr.Split("a", Right, true, "b", domain.Rect{Width: 40, Height: 2})
276			},
277		},
278		{
279			name: "stack refuses overflow",
280			op: func(tr *Tree) error {
281				require.NoError(t, tr.StackNew("a", "b", domain.Rect{Width: 20, Height: 3}))
282				return tr.StackNew("b", "c", domain.Rect{Width: 20, Height: 3})
283			},
284		},
285	}
286
287	for _, tt := range tests {
288		t.Run(tt.name, func(t *testing.T) {
289			tr := NewTree("a")
290			before := tr.clone()
291			err := tt.op(tr)
292			require.ErrorIs(t, err, ErrTooSmall)
293			if tt.name == "stack refuses overflow" {
294				require.Equal(t, PaneID("b"), tr.Focus)
295				require.Equal(t, []PaneID{"a", "b"}, leafIDs(tr.Root))
296				return
297			}
298			require.Equal(t, before, tr)
299		})
300	}
301}
302
303func TestTreeSurgeryShapes(t *testing.T) {
304	t.Parallel()
305
306	t.Run("split preserves and dissolves containers", func(t *testing.T) {
307		tr := NewTree("a")
308		require.NoError(t, tr.Split("a", Right, true, "b", domain.Rect{Width: 41, Height: 2}))
309		require.Equal(t, Split, tr.Root.Kind)
310		require.Equal(t, Horizontal, tr.Root.Dir)
311		require.Equal(t, []PaneID{"a", "b"}, leafIDs(tr.Root))
312		require.NoError(t, tr.Close("a"))
313		require.Equal(t, Leaf, tr.Root.Kind)
314		require.Equal(t, PaneID("b"), tr.Root.Leaf)
315	})
316
317	t.Run("nested split inserts on matching axis", func(t *testing.T) {
318		tr := NewTree("a")
319		require.NoError(t, tr.Split("a", Right, true, "b", domain.Rect{Width: 41, Height: 4}))
320		require.NoError(t, tr.Split("b", Right, true, "c", domain.Rect{Width: 62, Height: 4}))
321		require.Len(t, tr.Root.Children, 3)
322		require.Equal(t, []PaneID{"a", "b", "c"}, leafIDs(tr.Root))
323		require.NoError(t, tr.Split("b", Down, true, "d", domain.Rect{Width: 62, Height: 5}))
324		require.Equal(t, Vertical, tr.Root.Children[1].Dir)
325		require.Equal(t, []PaneID{"a", "b", "d", "c"}, leafIDs(tr.Root))
326	})
327}
328
329func TestToggleStackNestedShapes(t *testing.T) {
330	t.Parallel()
331	area := domain.Rect{Width: 62, Height: 5}
332	tests := []struct {
333		name       string
334		root       *Node
335		target     PaneID
336		wantKind   Kind
337		wantDir    SplitDir
338		wantErr    error
339		wantLeaves []PaneID
340	}{
341		{
342			name: "nested stack toggles directly containing stack",
343			root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
344				NewLeaf("a"),
345				{Kind: Stack, Children: []*Node{NewLeaf("b"), NewLeaf("c")}, Expanded: "b"},
346			}},
347			target:     "b",
348			wantKind:   Split,
349			wantDir:    Vertical,
350			wantLeaves: []PaneID{"b", "c"},
351		},
352		{
353			name: "nested split toggles directly containing two-leaf split",
354			root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
355				NewLeaf("a"),
356				{Kind: Split, Dir: Vertical, Children: []*Node{NewLeaf("b"), NewLeaf("c")}},
357			}},
358			target:     "b",
359			wantKind:   Stack,
360			wantLeaves: []PaneID{"b", "c"},
361		},
362		{
363			name: "nested non toggleable split reports not toggleable",
364			root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
365				NewLeaf("a"),
366				{Kind: Split, Dir: Vertical, Children: []*Node{NewLeaf("b"), NewLeaf("c"), NewLeaf("d")}},
367			}},
368			target:  "b",
369			wantErr: ErrNotToggleable,
370		},
371		{
372			name: "three leaf split reports not toggleable not too small",
373			root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
374				NewLeaf("a"), NewLeaf("b"), NewLeaf("c"),
375			}},
376			target:  "b",
377			wantErr: ErrNotToggleable,
378		},
379	}
380	for _, tt := range tests {
381		t.Run(tt.name, func(t *testing.T) {
382			tr := &Tree{Root: tt.root, Focus: tt.target}
383			err := tr.ToggleStack(tt.target, area)
384			if tt.wantErr != nil {
385				require.ErrorIs(t, err, tt.wantErr)
386				require.NotErrorIs(t, err, ErrTooSmall)
387				return
388			}
389			require.NoError(t, err)
390			container := tr.Root.Children[1]
391			require.Equal(t, tt.wantKind, container.Kind)
392			if tt.wantKind == Split {
393				require.Equal(t, tt.wantDir, container.Dir)
394			}
395			require.Equal(t, tt.wantLeaves, LeafIDs(container))
396		})
397	}
398}
399
400func TestCloseRefocusPrefersSibling(t *testing.T) {
401	t.Parallel()
402	tr := &Tree{Focus: "c", Root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
403		NewLeaf("a"),
404		{Kind: Split, Dir: Vertical, Children: []*Node{NewLeaf("b"), NewLeaf("c")}},
405		NewLeaf("d"),
406	}}}
407	require.NoError(t, tr.Close("c"))
408	require.Equal(t, PaneID("b"), tr.Focus)
409}
410
411func TestCloseNotFoundDoesNotMutate(t *testing.T) {
412	t.Parallel()
413	tr := &Tree{Focus: "b", Root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
414		NewLeaf("a"),
415		{Kind: Stack, Children: []*Node{NewLeaf("b"), NewLeaf("c")}, Expanded: "b"},
416	}}}
417	before := tr.clone()
418	require.ErrorIs(t, tr.Close("missing"), ErrNotFound)
419	require.Equal(t, before, tr)
420}
421
422func TestSolveRejectsStackWithMissingExpanded(t *testing.T) {
423	t.Parallel()
424	_, ok := Solve(&Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("b")}, Expanded: "missing"}, domain.Rect{Width: 20, Height: 4})
425	require.False(t, ok)
426}
427
428func TestExportedLeafHelpers(t *testing.T) {
429	t.Parallel()
430	root := &Node{Kind: Split, Dir: Horizontal, Children: []*Node{NewLeaf("a"), NewLeaf("b")}}
431	require.True(t, ContainsLeaf(root, "b"))
432	require.False(t, ContainsLeaf(root, "c"))
433	require.Equal(t, []PaneID{"a", "b"}, LeafIDs(root))
434}
435
436func TestFocusDir(t *testing.T) {
437	t.Parallel()
438	area := domain.Rect{Width: 62, Height: 5}
439	tests := []struct {
440		name  string
441		focus PaneID
442		dir   Direction
443		want  PaneID
444	}{
445		{"right chooses overlapping neighbor", "a", Right, "b"},
446		{"left chooses overlapping neighbor", "b", Left, "a"},
447		{"down chooses vertical neighbor", "b", Down, "c"},
448		{"up chooses vertical neighbor", "c", Up, "b"},
449	}
450	for _, tt := range tests {
451		t.Run(tt.name, func(t *testing.T) {
452			tr := &Tree{Focus: tt.focus, Root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
453				NewLeaf("a"),
454				{Kind: Split, Dir: Vertical, Children: []*Node{NewLeaf("b"), NewLeaf("c")}},
455			}}}
456			require.NoError(t, tr.FocusDir(tt.dir, area))
457			require.Equal(t, tt.want, tr.Focus)
458		})
459	}
460
461	t.Run("stack local vertical focus expands member", func(t *testing.T) {
462		tr := &Tree{Focus: "b", Root: &Node{Kind: Stack, Children: []*Node{NewLeaf("a"), NewLeaf("b"), NewLeaf("c")}, Expanded: "b"}}
463		require.NoError(t, tr.FocusDir(Down, domain.Rect{Width: 20, Height: 4}))
464		require.Equal(t, PaneID("c"), tr.Focus)
465		require.Equal(t, PaneID("c"), tr.Root.Expanded)
466		require.NoError(t, tr.FocusDir(Up, domain.Rect{Width: 20, Height: 4}))
467		require.Equal(t, PaneID("b"), tr.Focus)
468		require.Equal(t, PaneID("b"), tr.Root.Expanded)
469	})
470
471	t.Run("no pane in direction", func(t *testing.T) {
472		tr := NewTree("a")
473		err := tr.FocusDir(Left, domain.Rect{Width: 20, Height: 2})
474		require.True(t, errors.Is(err, ErrNoPane))
475	})
476}
477
478func TestFocusDirEnteringStackExpandsMember(t *testing.T) {
479	t.Parallel()
480	tests := []struct {
481		name  string
482		root  *Node
483		area  domain.Rect
484		focus PaneID
485		dir   Direction
486		want  PaneID
487	}{
488		{
489			name:  "from left",
490			area:  domain.Rect{Width: 62, Height: 8},
491			focus: "a",
492			dir:   Right,
493			root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
494				{Kind: Split, Dir: Vertical, Children: []*Node{NewLeaf("x"), NewLeaf("a")}},
495				{Kind: Stack, Children: []*Node{NewLeaf("b"), NewLeaf("c"), NewLeaf("d"), NewLeaf("e")}, Expanded: "b"},
496			}},
497			want: "c",
498		},
499		{
500			name:  "from right",
501			area:  domain.Rect{Width: 62, Height: 8},
502			focus: "a",
503			dir:   Left,
504			root: &Node{Kind: Split, Dir: Horizontal, Children: []*Node{
505				{Kind: Stack, Children: []*Node{NewLeaf("b"), NewLeaf("c"), NewLeaf("d"), NewLeaf("e")}, Expanded: "b"},
506				{Kind: Split, Dir: Vertical, Children: []*Node{NewLeaf("x"), NewLeaf("a")}},
507			}},
508			want: "c",
509		},
510		{
511			name:  "from above",
512			area:  domain.Rect{Width: 41, Height: 7},
513			focus: "a",
514			dir:   Down,
515			root: &Node{Kind: Split, Dir: Vertical, Children: []*Node{
516				NewLeaf("a"),
517				{Kind: Stack, Children: []*Node{NewLeaf("b"), NewLeaf("c")}, Expanded: "c"},
518			}},
519			want: "b",
520		},
521		{
522			name:  "from below",
523			area:  domain.Rect{Width: 41, Height: 7},
524			focus: "a",
525			dir:   Up,
526			root: &Node{Kind: Split, Dir: Vertical, Children: []*Node{
527				{Kind: Stack, Children: []*Node{NewLeaf("b"), NewLeaf("c")}, Expanded: "b"},
528				NewLeaf("a"),
529			}},
530			want: "c",
531		},
532	}
533	for _, tt := range tests {
534		t.Run(tt.name, func(t *testing.T) {
535			tr := &Tree{Root: tt.root, Focus: tt.focus}
536			var stack *Node
537			findStack(tr.Root, &stack)
538			require.NotEqual(t, tt.want, stack.Expanded)
539			require.NoError(t, tr.FocusDir(tt.dir, tt.area))
540			require.Equal(t, tt.want, tr.Focus)
541			require.Equal(t, tt.want, stack.Expanded)
542		})
543	}
544}
545
546func TestFocusDirStackLocalSkipsNonLeafChildren(t *testing.T) {
547	t.Parallel()
548	tr := &Tree{
549		Root: &Node{Kind: Stack, Children: []*Node{
550			NewLeaf("a"),
551			{Kind: Split, Dir: Horizontal, Children: []*Node{NewLeaf("nested")}},
552			NewLeaf("b"),
553		}, Expanded: "a"},
554		Focus: "a",
555	}
556
557	require.NoError(t, tr.FocusDir(Down, domain.Rect{Width: 80, Height: 6}))
558	require.Equal(t, PaneID("b"), tr.Focus)
559	require.Equal(t, PaneID("b"), tr.Root.Expanded)
560}
561
562func contents(ps []Placement) map[PaneID]domain.Rect {
563	out := make(map[PaneID]domain.Rect, len(ps))
564	for _, p := range ps {
565		out[p.ID] = p.Content
566	}
567	return out
568}
569
570func findStack(n *Node, out **Node) {
571	if n == nil || *out != nil {
572		return
573	}
574	if n.Kind == Stack {
575		*out = n
576		return
577	}
578	for _, child := range n.Children {
579		findStack(child, out)
580	}
581}

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