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, 266}},
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, 266}},
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		{"cascade full band", OverflowCascade, 4, 4},
121		{"cascade full bands", OverflowCascade, 3, 6},
122		{"fixed full row", OverflowFixed, 4, 4},
123		{"fixed spiral", OverflowFixed, 2, 4},
124		{"max-columns 1", OverflowCascade, 1, 3},
125	} {
126		t.Run(tc.name, func(t *testing.T) {
127			w := cellsWorkspace(tc.o, tc.k, tc.n)
128			before := w.columnRects()
129			for _, a := range []Action{ActionCycleColumnWidth, "set-column-width +1", "set-column-width -1"} {
130				w.Apply(a)
131				if got := cellsOf(w); !reflect.DeepEqual(got, make([]int, tc.n)) {
132					t.Fatalf("%s: cells %v", a, got)
133				}
134				if got := w.columnRects(); !reflect.DeepEqual(got, before) {
135					t.Fatalf("%s: rects %v, want %v", a, got, before)
136				}
137			}
138		})
139	}
140}
141
142// set-column-width +N/-N grows or shrinks by cells without cycling, and a
143// column never goes below one cell (AC5).
144func TestSetColumnWidthCells(t *testing.T) {
145	for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
146		t.Run(string(o), func(t *testing.T) {
147			w := cellsWorkspace(o, 4, 2)
148			w.FocusID(2)
149			for _, s := range []struct {
150				a     Action
151				cells []int
152			}{
153				{"set-column-width +1", []int{1, 3}},
154				{"set-column-width +1", []int{1, 3}}, // no donor: no cycle
155				{"set-column-width -1", []int{2, 2}},
156				{"set-column-width -1", []int{3, 1}},
157				{"set-column-width -1", []int{3, 1}}, // never below one cell
158				{"set-column-width +2", []int{1, 3}},
159				{"set-column-width -3", []int{3, 1}},
160			} {
161				w.Apply(s.a)
162				if got := cellsOf(w); !reflect.DeepEqual(got, s.cells) {
163					t.Fatalf("%s: cells %v, want %v", s.a, got, s.cells)
164				}
165			}
166			// The leftmost column at the lane's end gives to its left neighbor.
167			w.FocusID(1)
168			w.Apply("set-column-width -1")
169			if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2}) {
170				t.Fatalf("cells %v", got)
171			}
172		})
173	}
174	t.Run("shrink gives to the right, or to the left at the end", func(t *testing.T) {
175		w := cellsWorkspace(OverflowCascade, 6, 3)
176		w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 2, 2, 2
177		w.FocusID(2)
178		w.Apply("set-column-width -1")
179		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 3}) {
180			t.Fatalf("cells %v", got)
181		}
182		w.FocusID(3)
183		w.Apply("set-column-width -1")
184		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 2}) {
185			t.Fatalf("cells %v", got)
186		}
187	})
188	t.Run("unnormalized lane: the shrink gives the remainder to the leftmost", func(t *testing.T) {
189		w := cellsWorkspace(OverflowCascade, 5, 3)
190		w.FocusID(3)
191		w.Apply("set-column-width -1") // base 2,2,1 and column 3 has one cell
192		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0}) {
193			t.Fatalf("cells %v: nothing is written when nothing changes", got)
194		}
195		w.FocusID(2)
196		w.Apply("set-column-width -1") // 2,2,1 -> 2,1,2
197		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 2}) {
198			t.Fatalf("cells %v", got)
199		}
200	})
201}
202
203func TestGrowColumnScrollBounds(t *testing.T) {
204	w := cellsWorkspace(OverflowScroll, 4, 2)
205	w.FocusID(2)
206	w.Apply("set-column-width -1") // auto is 2 cells
207	if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 1}) {
208		t.Fatalf("cells %v", got)
209	}
210	for range 3 {
211		w.Apply("set-column-width -1")
212	}
213	if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 1}) {
214		t.Fatalf("below one cell: %v", got)
215	}
216	for range 6 {
217		w.Apply("set-column-width +1")
218	}
219	if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 4}) {
220		t.Fatalf("above max-columns: %v", got)
221	}
222}
223
224func TestGrowColumnEdgeCases(t *testing.T) {
225	t.Run("a float focused changes nothing", func(t *testing.T) {
226		for _, o := range []Overflow{OverflowScroll, OverflowCascade, OverflowFixed} {
227			w := cellsWorkspace(o, 4, 2)
228			w.AddFloating(9, 100, 100)
229			w.Apply(ActionCycleColumnWidth)
230			w.Apply("set-column-width +1")
231			w.Apply("set-column-width -1")
232			if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
233				t.Fatalf("%s: cells %v", o, got)
234			}
235		}
236	})
237	t.Run("a maximized column unmaximizes, then grows", func(t *testing.T) {
238		for _, o := range []Overflow{OverflowScroll, OverflowCascade, OverflowFixed} {
239			w := cellsWorkspace(o, 4, 2)
240			w.ToggleFullWidth()
241			w.Apply(ActionCycleColumnWidth)
242			if w.Columns[w.Focus].FullWidth || w.Columns[w.Focus].Cells != 3 {
243				t.Fatalf("%s: %+v", o, w.Columns)
244			}
245		}
246	})
247	t.Run("a fullscreen window hides the layout: no edit", func(t *testing.T) {
248		w := cellsWorkspace(OverflowFixed, 4, 2)
249		w.ToggleFullscreen()
250		w.Apply(ActionCycleColumnWidth)
251		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
252			t.Fatalf("cells %v", got)
253		}
254	})
255	t.Run("the donor is the largest, then the nearest, right first", func(t *testing.T) {
256		w := cellsWorkspace(OverflowCascade, 5, 3)
257		w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 2, 1, 2
258		w.FocusID(2)
259		w.Apply(ActionCycleColumnWidth)
260		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 1}) {
261			t.Fatalf("tie: cells %v", got)
262		}
263		// C is focused with {2,2,1}: A and B tie, the nearer one gives.
264		w.FocusID(3)
265		w.Apply(ActionCycleColumnWidth)
266		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 1, 2}) {
267			t.Fatalf("largest: cells %v", got)
268		}
269		w.Columns[0].Cells, w.Columns[1].Cells, w.Columns[2].Cells = 3, 1, 1
270		w.FocusID(2)
271		w.Apply(ActionCycleColumnWidth)
272		if got := cellsOf(w); !reflect.DeepEqual(got, []int{2, 2, 1}) {
273			t.Fatalf("largest: cells %v", got)
274		}
275	})
276	t.Run("only the focused band changes", func(t *testing.T) {
277		w := cellsWorkspace(OverflowCascade, 4, 6)
278		// Band 1 holds columns 5 and 6.
279		w.Apply(ActionCycleColumnWidth)
280		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0, 1, 3}) {
281			t.Fatalf("cells %v", got)
282		}
283		if got := widthsOf(w); !reflect.DeepEqual(got, []int{200, 200, 200, 200, 200, 600}) {
284			t.Fatalf("widths %v", got)
285		}
286		w.Apply(ActionCycleColumnWidth)
287		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0, 0, 0}) {
288			t.Fatalf("cells %v", got)
289		}
290		w.FocusID(1) // a full band
291		w.Apply(ActionCycleColumnWidth)
292		if got := cellsOf(w); !reflect.DeepEqual(got, make([]int, 6)) {
293			t.Fatalf("cells %v", got)
294		}
295	})
296}
297
298// A column set above max-columns (moved to a narrower workspace, or after a
299// reload) renders proportionally and cycles back to auto.
300func TestCellsAboveMaxColumns(t *testing.T) {
301	t.Run("lane", func(t *testing.T) {
302		for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
303			w := cellsWorkspace(o, 2, 2)
304			w.Columns[0].Cells = 3
305			if got := widthsOf(w); !reflect.DeepEqual(got, []int{600, 200}) {
306				t.Fatalf("%s: widths %v", o, got)
307			}
308			if o == OverflowCascade {
309				continue
310			}
311			// A narrower K does not rewrite the stored cells.
312			if got := cellsOf(w); !reflect.DeepEqual(got, []int{3, 0}) {
313				t.Fatalf("cells %v", got)
314			}
315		}
316	})
317	t.Run("a full uneven lane cycles back to equal", func(t *testing.T) {
318		for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
319			w := cellsWorkspace(o, 4, 4)
320			w.Columns[1].Cells = 2
321			w.Columns[0].Cells, w.Columns[2].Cells = 1, 1
322			w.Apply("set-column-width +1")
323			if got := cellsOf(w); !reflect.DeepEqual(got, []int{1, 2, 1, 0}) {
324				t.Fatalf("%s: set-column-width cycled: %v", o, got)
325			}
326			w.Apply(ActionCycleColumnWidth)
327			if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0, 0, 0}) {
328				t.Fatalf("%s: cells %v", o, got)
329			}
330		}
331	})
332	t.Run("a lone column with cells cycles back to auto", func(t *testing.T) {
333		for _, o := range []Overflow{OverflowCascade, OverflowFixed} {
334			w := cellsWorkspace(o, 4, 1)
335			w.Columns[0].Cells = 2
336			w.Apply(ActionCycleColumnWidth)
337			if got := cellsOf(w); !reflect.DeepEqual(got, []int{0}) {
338				t.Fatalf("%s: cells %v", o, got)
339			}
340		}
341	})
342	t.Run("scroll", func(t *testing.T) {
343		w := cellsWorkspace(OverflowScroll, 2, 2)
344		w.Columns[0].Cells = 3
345		w.FocusID(1)
346		if got := widthsOf(w)[0]; got != 800 {
347			t.Fatalf("width %d, at most max-columns cells", got)
348		}
349		w.Apply(ActionCycleColumnWidth)
350		if got := cellsOf(w); !reflect.DeepEqual(got, []int{0, 0}) {
351			t.Fatalf("cells %v", got)
352		}
353	})
354	t.Run("a moved column keeps its cells", func(t *testing.T) {
355		src := cellsWorkspace(OverflowScroll, 4, 2)
356		src.Columns[1].Cells = 3
357		dst := cellsWorkspace(OverflowScroll, 2, 1)
358		col, ok := src.takeColumn()
359		if !ok {
360			t.Fatal("no column")
361		}
362		dst.receive(col, 1)
363		if got := cellsOf(dst); !reflect.DeepEqual(got, []int{0, 3}) {
364			t.Fatalf("cells %v", got)
365		}
366	})
367}
368
369// The overview blocks the grow and resize binds, like the other window
370// mutations, and its cascade preview keeps the columns' cells.
371func TestOverviewKeepsCells(t *testing.T) {
372	m := newMonitor("", "")
373	m.SetOutput(900, 600)
374	m.SetOverflow(OverflowCascade)
375	m.SetMaxColumns(4)
376	for id := WindowID(1); id <= 2; id++ {
377		m.AddWindow(id)
378	}
379	w := m.Current()
380	w.Columns[0].Cells, w.Columns[1].Cells = 1, 3
381	screen := w.columnRects()
382	if screen[0].W != 225 || screen[1].W != 675 {
383		t.Fatalf("screen widths %v, want 225 and 675", screen)
384	}
385	m.ToggleOverview()
386	tiles, _, _ := w.previewTiles()
387	for i, p := range tiles {
388		if want := screen[i]; p.Rect.W != want.W || p.Rect.X != want.X-w.Usable.X || p.Rect.H != want.H {
389			t.Fatalf("column %d: preview %+v, screen %+v", i, p.Rect, want)
390		}
391	}
392	for _, a := range []Action{ActionCycleColumnWidth, "set-column-width +1", "set-column-width -1"} {
393		m.Apply(a)
394		if got := cellsOf(w); !reflect.DeepEqual(got, []int{1, 3}) {
395			t.Fatalf("%s in the overview: cells %v", a, got)
396		}
397	}
398}

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 23

Tags 10

↑↓ moveenter openesc close