main f2d166b

column_cells_test.go RawHistoryFolder

1package core
2
3import (
4	"reflect"
5	"testing"
6)
7
8// cellsWorkspace is n columns on an 800 px wide output, no gaps, so a cell
9// of max-columns k is 800/k px.
10func cellsWorkspace(o Overflow, k, n int) *Workspace {
11	w := &Workspace{Overflow: o, MaxColumns: k}
12	w.SetOutput(800, 600)
13	for id := 1; id <= n; id++ {
14		w.AddWindow(WindowID(id))
15	}
16	return w
17}
18
19func cellsOf(w *Workspace) []int {
20	got := make([]int, len(w.Columns))
21	for i, c := range w.Columns {
22		got[i] = c.Cells
23	}
24	return got
25}
26
27func widthsOf(w *Workspace) []int {
28	got := make([]int, len(w.Columns))
29	for i, r := range w.columnRects() {
30		got[i] = r.W
31	}
32	return got
33}
34
35// The grow cycle of cycle-column-width, in the three overflows (AC1 to AC4).
36func TestGrowColumnCycle(t *testing.T) {
37	type step struct {
38		cells  []int
39		widths []int // nil: not checked
40	}
41	for _, tc := range []struct {
42		name  string
43		o     Overflow
44		k, n  int
45		focus int // window ID
46		steps []step
47	}{
48		{"cascade two columns", OverflowCascade, 4, 2, 2, []step{
49			{[]int{1, 3}, []int{200, 600}},
50			{[]int{0, 0}, []int{400, 400}},
51			{[]int{1, 3}, []int{200, 600}},
52		}},
53		{"fixed two columns", OverflowFixed, 4, 2, 2, []step{
54			{[]int{1, 3}, []int{200, 600}},
55			{[]int{0, 0}, []int{400, 400}},
56		}},
57		{"cascade three columns", OverflowCascade, 4, 3, 2, []step{
58			{[]int{1, 2, 1}, []int{200, 400, 200}},
59			{[]int{0, 0, 0}, []int{266, 266, 268}},
60		}},
61		{"fixed three columns", OverflowFixed, 4, 3, 2, []step{
62			{[]int{1, 2, 1}, []int{200, 400, 200}},
63			{[]int{0, 0, 0}, []int{266, 266, 268}},
64		}},
65		{"remainder goes to the focused column and is the step", OverflowCascade, 5, 3, 1, []step{
66			// 5 cells over 3 columns: 1 each and the 2 left to the focused one.
67			{[]int{3, 1, 1}, nil},
68			// No column has a cell to give: equal shares again.
69			{[]int{0, 0, 0}, nil},
70			{[]int{3, 1, 1}, nil},
71		}},
72		{"an even lane steps at once, the nearest tied donor first", OverflowCascade, 6, 3, 3, []step{
73			{[]int{2, 1, 3}, nil},
74			{[]int{1, 1, 4}, nil},
75			{[]int{0, 0, 0}, nil},
76		}},
77		{"scroll unset column", OverflowScroll, 4, 2, 2, []step{
78			{[]int{0, 3}, []int{400, 600}},
79			{[]int{0, 4}, []int{400, 800}},
80			{[]int{0, 0}, []int{400, 400}},
81			{[]int{0, 3}, []int{400, 600}},
82		}},
83		{"scroll lone column already fills the width", OverflowScroll, 4, 1, 1, []step{
84			{[]int{0}, []int{800}},
85		}},
86		{"scroll three columns", OverflowScroll, 4, 3, 1, []step{
87			{[]int{2, 0, 0}, nil},
88			{[]int{3, 0, 0}, nil},
89			{[]int{4, 0, 0}, nil},
90			{[]int{0, 0, 0}, nil},
91		}},
92	} {
93		t.Run(tc.name, func(t *testing.T) {
94			w := cellsWorkspace(tc.o, tc.k, tc.n)
95			w.FocusID(WindowID(tc.focus))
96			for i, s := range tc.steps {
97				w.Apply(ActionCycleColumnWidth)
98				if got := cellsOf(w); !reflect.DeepEqual(got, s.cells) {
99					t.Fatalf("step %d: cells %v, want %v", i, got, s.cells)
100				}
101				if s.widths != nil {
102					if got := widthsOf(w); !reflect.DeepEqual(got, s.widths) {
103						t.Fatalf("step %d: widths %v, want %v", i, got, s.widths)
104					}
105				}
106			}
107		})
108	}
109}
110
111// A lone column and a full lane do not change (AC3).
112func TestGrowColumnNoop(t *testing.T) {
113	for _, tc := range []struct {
114		name string
115		o    Overflow
116		k, n int
117	}{
118		{"cascade lone", OverflowCascade, 4, 1},
119		{"fixed lone", OverflowFixed, 4, 1},
120		{"fixed full row", OverflowFixed, 4, 4},
121		{"fixed spiral", OverflowFixed, 2, 4},
122		{"max-columns 1", OverflowCascade, 1, 3},
123	} {
124		t.Run(tc.name, func(t *testing.T) {
125			w := cellsWorkspace(tc.o, tc.k, tc.n)
126			before := w.columnRects()
127			for _, a := range []Action{ActionCycleColumnWidth, "set-column-width +1", "set-column-width -1"} {
128				w.Apply(a)
129				if got := cellsOf(w); !reflect.DeepEqual(got, make([]int, tc.n)) {
130					t.Fatalf("%s: cells %v", a, got)
131				}
132				if got := w.columnRects(); !reflect.DeepEqual(got, before) {
133					t.Fatalf("%s: rects %v, want %v", a, got, before)
134				}
135			}
136		})
137	}
138}
139
140// set-column-width +N/-N grows or shrinks by cells without cycling, and a
141// column never goes below one cell (AC5).
142func TestSetColumnWidthCells(t *testing.T) {
143	for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
144		t.Run(string(o), func(t *testing.T) {
145			w := cellsWorkspace(o, 4, 2)
146			w.FocusID(2)
147			for _, s := range []struct {
148				a     Action
149				cells []int
150			}{
151				{"set-column-width +1", []int{1, 3}},
152				{"set-column-width +1", []int{1, 3}}, // no donor: no cycle
153				{"set-column-width -1", []int{2, 2}},
154				{"set-column-width -1", []int{3, 1}},
155				{"set-column-width -1", []int{3, 1}}, // never below one cell
156				{"set-column-width +2", []int{1, 3}},
157				{"set-column-width -3", []int{3, 1}},
158			} {
159				w.Apply(s.a)
160				if got := cellsOf(w); !reflect.DeepEqual(got, s.cells) {
161					t.Fatalf("%s: cells %v, want %v", s.a, got, s.cells)
162				}
163			}
164			// The leftmost column at the lane's end gives to its left neighbor.
165			w.FocusID(1)
166			w.Apply("set-column-width -1")
167			if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2}) {
168				t.Fatalf("cells %v", got)
169			}
170		})
171	}
172	t.Run("shrink gives to the right, or to the left at the end", func(t *testing.T) {
173		w := cellsWorkspace(OverflowCascade, 6, 3)
174		w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 2, 2, 2
175		w.FocusID(2)
176		w.Apply("set-column-width -1")
177		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 3}) {
178			t.Fatalf("cells %v", got)
179		}
180		w.FocusID(3)
181		w.Apply("set-column-width -1")
182		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 2}) {
183			t.Fatalf("cells %v", got)
184		}
185	})
186	t.Run("unnormalized lane: the shrink gives the remainder to the leftmost", func(t *testing.T) {
187		w := cellsWorkspace(OverflowCascade, 5, 3)
188		w.FocusID(3)
189		w.Apply("set-column-width -1") // base 2,2,1 and column 3 has one cell
190		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0}) {
191			t.Fatalf("cells %v: nothing is written when nothing changes", got)
192		}
193		w.FocusID(2)
194		w.Apply("set-column-width -1") // 2,2,1 -> 2,1,2
195		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 2}) {
196			t.Fatalf("cells %v", got)
197		}
198	})
199}
200
201func TestGrowColumnScrollBounds(t *testing.T) {
202	w := cellsWorkspace(OverflowScroll, 4, 2)
203	w.FocusID(2)
204	w.Apply("set-column-width -1") // auto is 2 cells
205	if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 1}) {
206		t.Fatalf("cells %v", got)
207	}
208	for range 3 {
209		w.Apply("set-column-width -1")
210	}
211	if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 1}) {
212		t.Fatalf("below one cell: %v", got)
213	}
214	for range 6 {
215		w.Apply("set-column-width +1")
216	}
217	if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 4}) {
218		t.Fatalf("above max-columns: %v", got)
219	}
220}
221
222func TestGrowColumnEdgeCases(t *testing.T) {
223	t.Run("a float focused changes nothing", func(t *testing.T) {
224		for _, o := range []Overflow{OverflowScroll, OverflowCascade, OverflowFixed} {
225			w := cellsWorkspace(o, 4, 2)
226			w.AddFloating(9, 100, 100)
227			w.Apply(ActionCycleColumnWidth)
228			w.Apply("set-column-width +1")
229			w.Apply("set-column-width -1")
230			if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
231				t.Fatalf("%s: cells %v", o, got)
232			}
233		}
234	})
235	t.Run("a maximized column unmaximizes, then grows", func(t *testing.T) {
236		for _, o := range []Overflow{OverflowScroll, OverflowCascade, OverflowFixed} {
237			w := cellsWorkspace(o, 4, 2)
238			w.ToggleFullWidth()
239			w.Apply(ActionCycleColumnWidth)
240			if w.Columns[w.Focus].FullWidth || w.Columns[w.Focus].Cells != 3 {
241				t.Fatalf("%s: %+v", o, w.Columns)
242			}
243		}
244	})
245	t.Run("a fullscreen window hides the layout: no edit", func(t *testing.T) {
246		w := cellsWorkspace(OverflowFixed, 4, 2)
247		w.ToggleFullscreen()
248		w.Apply(ActionCycleColumnWidth)
249		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
250			t.Fatalf("cells %v", got)
251		}
252	})
253	t.Run("the donor is the largest, then the nearest, right first", func(t *testing.T) {
254		w := cellsWorkspace(OverflowCascade, 5, 3)
255		w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 2, 1, 2
256		w.FocusID(2)
257		w.Apply(ActionCycleColumnWidth)
258		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 1}) {
259			t.Fatalf("tie: cells %v", got)
260		}
261		// C is focused with {2,2,1}: A and B tie, the nearer one gives.
262		w.FocusID(3)
263		w.Apply(ActionCycleColumnWidth)
264		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 2}) {
265			t.Fatalf("largest: cells %v", got)
266		}
267		w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 3, 1, 1
268		w.FocusID(2)
269		w.Apply(ActionCycleColumnWidth)
270		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 1}) {
271			t.Fatalf("largest: cells %v", got)
272		}
273	})
274	t.Run("only the focused band changes", func(t *testing.T) {
275		w := cellsWorkspace(OverflowCascade, 4, 6)
276		// Band 1 holds columns 5 and 6.
277		w.Apply(ActionCycleColumnWidth)
278		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0, 1, 3}) {
279			t.Fatalf("cells %v", got)
280		}
281		if got := widthsOf(w); !reflect.DeepEqual(got, []int{200, 200, 200, 200, 200, 600}) {
282			t.Fatalf("widths %v", got)
283		}
284		w.Apply(ActionCycleColumnWidth)
285		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0, 0, 0}) {
286			t.Fatalf("cells %v", got)
287		}
288		w.FocusID(1) // A full band pushes a neighbor down (cascade_cycle_test).
289		w.Apply(ActionCycleColumnWidth)
290		if got := cellsOf(w)[4:]; !reflect.DeepEqual(got, []int{0, 0}) {
291			t.Fatalf("other band changed: cells %v", cellsOf(w))
292		}
293	})
294}
295
296// A column set above max-columns (moved to a narrower workspace, or after a
297// reload) renders proportionally and cycles back to auto.
298func TestCellsAboveMaxColumns(t *testing.T) {
299	t.Run("lane", func(t *testing.T) {
300		for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
301			w := cellsWorkspace(o, 2, 2)
302			w.Columns[0].Cells = 3
303			if got := widthsOf(w); !reflect.DeepEqual(got, []int{600, 200}) {
304				t.Fatalf("%s: widths %v", o, got)
305			}
306			if o == OverflowCascade {
307				continue
308			}
309			// A narrower K does not rewrite the stored cells.
310			if got := cellsOf(w); !reflect.DeepEqual(got, []int{3, 0}) {
311				t.Fatalf("cells %v", got)
312			}
313		}
314	})
315	t.Run("a full uneven lane cycles back to equal", func(t *testing.T) {
316		for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
317			w := cellsWorkspace(o, 4, 4)
318			w.Columns[1].Cells = 2
319			w.Columns[0].Cells, w.Columns[2].Cells = 1, 1
320			w.Apply("set-column-width +1")
321			if got := cellsOf(w); !reflect.DeepEqual(got, []int{1, 2, 1, 0}) {
322				t.Fatalf("%s: set-column-width cycled: %v", o, got)
323			}
324			w.Apply(ActionCycleColumnWidth)
325			if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0}) {
326				t.Fatalf("%s: cells %v", o, got)
327			}
328		}
329	})
330	t.Run("a lone column with cells cycles back to auto", func(t *testing.T) {
331		for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
332			w := cellsWorkspace(o, 4, 1)
333			w.Columns[0].Cells = 2
334			w.Apply(ActionCycleColumnWidth)
335			if got := cellsOf(w); !reflect.DeepEqual(got, []int{0}) {
336				t.Fatalf("%s: cells %v", o, got)
337			}
338		}
339	})
340	t.Run("scroll", func(t *testing.T) {
341		w := cellsWorkspace(OverflowScroll, 2, 2)
342		w.Columns[0].Cells = 3
343		w.FocusID(1)
344		if got := widthsOf(w)[0]; got != 800 {
345			t.Fatalf("width %d, at most max-columns cells", got)
346		}
347		w.Apply(ActionCycleColumnWidth)
348		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
349			t.Fatalf("cells %v", got)
350		}
351	})
352	t.Run("a moved column keeps its cells", func(t *testing.T) {
353		src := cellsWorkspace(OverflowScroll, 4, 2)
354		src.Columns[1].Cells = 3
355		dst := cellsWorkspace(OverflowScroll, 2, 1)
356		col, ok := src.takeColumn()
357		if !ok {
358			t.Fatal("no column")
359		}
360		dst.receive(col, 1)
361		if got := cellsOf(dst); !reflect.DeepEqual(got, []int{0, 3}) {
362			t.Fatalf("cells %v", got)
363		}
364	})
365}
366
367// The overview blocks the grow and resize binds, like the other window
368// mutations, and its cascade preview keeps the columns' cells.
369func TestOverviewKeepsCells(t *testing.T) {
370	m := newMonitor("", "")
371	m.SetOutput(900, 600)
372	m.SetOverflow(OverflowCascade)
373	m.SetMaxColumns(4)
374	for id := WindowID(1); id <= 2; id++ {
375		m.AddWindow(id)
376	}
377	w := m.Current()
378	w.Columns[0].Cells, w.Columns[1].Cells = 1, 3
379	screen := w.columnRects()
380	if screen[0].W != 225 || screen[1].W != 675 {
381		t.Fatalf("screen widths %v, want 225 and 675", screen)
382	}
383	m.ToggleOverview()
384	tiles, _, _ := w.previewTiles()
385	for i, p := range tiles {
386		if want := screen[i]; p.Rect.W != want.W || p.Rect.X != want.X-w.Usable.X || p.Rect.H != want.H {
387			t.Fatalf("column %d: preview %+v, screen %+v", i, p.Rect, want)
388		}
389	}
390	for _, a := range []Action{ActionCycleColumnWidth, "set-column-width +1", "set-column-width -1"} {
391		m.Apply(a)
392		if got := cellsOf(w); !reflect.DeepEqual(got, []int{1, 3}) {
393			t.Fatalf("%s in the overview: cells %v", a, got)
394		}
395	}
396}

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 25

Tags 10

↑↓ moveenter openesc close