feat/data-dnd e05cb15

cascade_bands_test.go RawHistoryFolder

1package core
2
3import (
4	"slices"
5	"testing"
6)
7
8// cycle-column-width in a full band pushes the column at the far end of
9// the band out: the rightmost down, or the leftmost up from the last
10// column; the end of the cycle brings it back.
11func TestCascadeCyclePushesNeighbor(t *testing.T) {
12	for _, tc := range []struct {
13		name   string
14		n, k   int
15		focus  WindowID
16		pushed [][]WindowID
17	}{
18		{"middle pushes rightmost down", 3, 3, 2, [][]WindowID{{1, 2}, {3}}},
19		{"first pushes rightmost down", 3, 3, 1, [][]WindowID{{1, 2}, {3}}},
20		{"last pushes leftmost up", 3, 3, 3, [][]WindowID{{1}, {2, 3}}},
21		{"last of four pushes leftmost up", 4, 4, 4, [][]WindowID{{1}, {2, 3, 4}}},
22		{"second of four pushes rightmost down", 4, 4, 2, [][]WindowID{{1, 2, 3}, {4}}},
23		{"joins the band below", 5, 3, 2, [][]WindowID{{1, 2}, {3, 4, 5}}},
24	} {
25		t.Run(tc.name, func(t *testing.T) {
26			_, w := cascadeMovesCore(t, tc.n, tc.k, false)
27			w.FocusID(tc.focus)
28			before := bandIDs(w)
29			w.Apply(ActionCycleColumnWidth)
30			wantBands(t, w, tc.pushed...)
31			if id, _ := w.Focused(); id != tc.focus {
32				t.Fatalf("focus %d, want %d", id, tc.focus)
33			}
34			if got := w.Columns[w.Focus].Cells; got != 2 {
35				t.Fatalf("focused cells %d, want 2", got)
36			}
37			w.Apply(ActionCycleColumnWidth)
38			wantBands(t, w, before...)
39			for _, c := range w.Columns {
40				if c.Cells != 0 {
41					t.Fatalf("cells not back to equal: %+v", w.Columns)
42				}
43			}
44		})
45	}
46}
47
48// A pushed column that moved meanwhile stays where it is.
49func TestCascadeCycleRestoreSkipsMovedColumn(t *testing.T) {
50	c, w := cascadeMovesCore(t, 3, 3, false)
51	w.FocusID(2)
52	w.Apply(ActionCycleColumnWidth) // {1 2} {3}
53	w.FocusID(3)
54	c.applyAction(ActionMoveWindowUp) // {3 1 2}: 3 is back by hand.
55	w.FocusID(2)
56	w.Apply(ActionCycleColumnWidth)
57	wantBands(t, w, []WindowID{3, 1, 2})
58	if got := cellsOf(w); !slices.Equal(got, []int{0, 0, 0}) {
59		t.Fatalf("cells %v, want equal", got)
60	}
61}
62
63// maximize-column isolates the column: the left ones go up, the right ones
64// down, together; maximizing again forms the band back.
65func TestCascadeMaximizeSplitsAndRestoresBand(t *testing.T) {
66	for _, tc := range []struct {
67		name  string
68		n, k  int
69		focus WindowID
70		split [][]WindowID
71	}{
72		{"middle", 3, 3, 2, [][]WindowID{{1}, {2}, {3}}},
73		{"first", 3, 3, 1, [][]WindowID{{1}, {2, 3}}},
74		{"left side moves together", 4, 4, 3, [][]WindowID{{1, 2}, {3}, {4}}},
75		{"left joins the band above", 5, 3, 5, [][]WindowID{{1, 2, 3}, {4}, {5}}},
76	} {
77		t.Run(tc.name, func(t *testing.T) {
78			_, w := cascadeMovesCore(t, tc.n, tc.k, false)
79			w.FocusID(tc.focus)
80			before := bandIDs(w)
81			w.ToggleFullWidth()
82			wantBands(t, w, tc.split...)
83			if !w.Columns[w.Focus].FullWidth {
84				t.Fatal("not maximized")
85			}
86			w.ToggleFullWidth()
87			wantBands(t, w, before...)
88			if w.Columns[w.Focus].FullWidth {
89				t.Fatal("still maximized")
90			}
91		})
92	}
93}
94
95// Columns on the left that fit the band above join it at its end.
96func TestCascadeMaximizeLeftJoinsBandAbove(t *testing.T) {
97	c, w := cascadeMovesCore(t, 5, 3, false) // {1 2 3} {4 5}
98	w.FocusID(2)
99	c.applyAction(ActionMoveWindowDown) // {1 3} {4 2 5}
100	w.FocusID(2)
101	w.ToggleFullWidth()
102	wantBands(t, w, []WindowID{1, 3, 4}, []WindowID{2}, []WindowID{5})
103	w.ToggleFullWidth()
104	wantBands(t, w, []WindowID{1, 3}, []WindowID{4, 2, 5})
105}
106
107// No column is hidden by a maximized cascade column: focusing each band
108// shows its windows.
109func TestCascadeMaximizeHidesNothing(t *testing.T) {
110	_, w := cascadeMovesCore(t, 3, 3, false)
111	w.FocusID(2)
112	w.ToggleFullWidth() // {1} {2*} {3}
113	for id := WindowID(1); id <= 3; id++ {
114		w.FocusID(id)
115		var found bool
116		for _, p := range w.Layout() {
117			if p.ID == id {
118				found = true
119				if p.Hidden || p.Rect.W <= 0 || p.Rect.H <= 0 {
120					t.Fatalf("window %d not shown: %+v", id, p)
121				}
122			}
123		}
124		if !found {
125			t.Fatalf("no placement for window %d", id)
126		}
127	}
128}
129
130// A new window joins the focused band when it has room, else it starts a
131// band of its own right below it; other bands do not move.
132func TestCascadeSpawnInFocusedBand(t *testing.T) {
133	_, w := cascadeMovesCore(t, 5, 3, false) // {1 2 3} {4 5}
134	w.FocusID(4)
135	w.AddWindow(6)
136	wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{4, 5, 6})
137	w.FocusID(2)
138	w.AddWindow(7)
139	wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{7}, []WindowID{4, 5, 6})
140	if id, _ := w.Focused(); id != 7 {
141		t.Fatalf("focus %d, want 7", id)
142	}
143	w.AddWindow(8)
144	wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{7, 8}, []WindowID{4, 5, 6})
145}
146
147// A band keeps its columns: closing one leaves the others in place, and a
148// band left empty goes away.
149func TestCascadeCloseKeepsBands(t *testing.T) {
150	c, w := cascadeMovesCore(t, 5, 3, false)
151	w.FocusID(5)
152	c.applyAction(ActionMoveWindowDown) // {1 2 3} {4} {5}
153	w.RemoveWindow(2)
154	wantBands(t, w, []WindowID{1, 3}, []WindowID{4}, []WindowID{5})
155	w.RemoveWindow(4)
156	wantBands(t, w, []WindowID{1, 3}, []WindowID{5})
157}
158
159// A smaller max-columns sends the extra columns of a band to a new band
160// right below it; a larger one keeps the bands.
161func TestCascadeMaxColumnsShrinkSplitsBand(t *testing.T) {
162	_, w := cascadeMovesCore(t, 4, 3, false) // {1 2 3} {4}
163	w.SetMaxColumns(2)
164	wantBands(t, w, []WindowID{1, 2}, []WindowID{3}, []WindowID{4})
165	w.SetMaxColumns(3)
166	wantBands(t, w, []WindowID{1, 2}, []WindowID{3}, []WindowID{4})
167}
168
169// Switching to cascade from another overflow packs the columns in order.
170func TestCascadeOverflowSwitchRepacks(t *testing.T) {
171	m := newMonitor("", "")
172	m.SetOutput(900, 600)
173	m.SetMaxColumns(3)
174	m.SetOverflow(OverflowCascade)
175	for id := WindowID(1); id <= 4; id++ {
176		m.AddWindow(id)
177	}
178	w := m.Current()
179	w.FocusID(2)
180	w.ToggleFullWidth() // {1} {2*} {3 4}
181	m.SetOverflow(OverflowScroll)
182	m.SetOverflow(OverflowCascade)
183	wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{4})
184}
185
186// Moves inside a band keep the band starts in place.
187func TestCascadeMoveColumnKeepsBandStarts(t *testing.T) {
188	c, w := cascadeMovesCore(t, 5, 3, false)
189	w.FocusID(4)
190	c.applyAction(ActionMoveColumnRight)
191	wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{5, 4})
192}
193
194// Review regressions: push memory survives a maximize in between, finds a
195// column whose first window closed, and leaves columns without room in
196// place.
197func TestCascadeRestoreBandMemory(t *testing.T) {
198	t.Run("cycle then maximize", func(t *testing.T) {
199		_, w := cascadeMovesCore(t, 3, 3, false)
200		w.FocusID(2)
201		w.Apply(ActionCycleColumnWidth) // {1 2} {3}
202		w.ToggleFullWidth()             // {1} {2*} {3}
203		w.ToggleFullWidth()             // {1 2 3}
204		wantBands(t, w, []WindowID{1, 2, 3})
205	})
206	t.Run("cycle then maximize from either end keeps the order", func(t *testing.T) {
207		for _, focus := range []WindowID{1, 3} {
208			_, w := cascadeMovesCore(t, 3, 3, false)
209			w.FocusID(focus)
210			w.Apply(ActionCycleColumnWidth)
211			w.ToggleFullWidth()
212			w.ToggleFullWidth()
213			wantBands(t, w, []WindowID{1, 2, 3})
214		}
215	})
216	t.Run("a window that left the pushed column does not bring its new column", func(t *testing.T) {
217		_, w := cascadeMovesCore(t, 5, 4, false) // {1 2 3 4} {5}
218		w.FocusID(4)
219		w.ConsumeOrExpel(-1) // {1 2 3/4} {5}
220		w.FocusID(2)
221		w.ToggleFullWidth() // {1} {2*} {3/4 5}
222		w.stackWindow(4, 5) // {1} {2*} {3 5/4}
223		w.FocusID(5)
224		w.MoveColumn(-1) // {1} {2*} {5/4 3}
225		w.FocusID(2)
226		w.ToggleFullWidth()
227		wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{5})
228	})
229	t.Run("a window expelled from the pushed column stays", func(t *testing.T) {
230		_, w := cascadeMovesCore(t, 5, 4, false) // {1 2 3 4} {5}
231		w.FocusID(4)
232		w.ConsumeOrExpel(-1) // {1 2 3/4} {5}
233		w.FocusID(2)
234		w.ToggleFullWidth() // {1} {2*} {3/4 5}
235		w.FocusID(4)
236		w.ConsumeOrExpel(-1) // {1} {2*} {4 3 5}
237		w.FocusID(2)
238		w.ToggleFullWidth()
239		wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{4, 5})
240	})
241	t.Run("a pushed column moved away has no stand-in", func(t *testing.T) {
242		c, w := cascadeMovesCore(t, 5, 4, false) // {1 2 3 4} {5}
243		w.FocusID(4)
244		w.ConsumeOrExpel(-1) // {1 2 3/4} {5}
245		w.FocusID(2)
246		w.ToggleFullWidth() // {1} {2*} {3/4 5}
247		w.FocusID(4)
248		w.ConsumeOrExpel(-1) // {1} {2*} {4 3 5}
249		w.FocusID(3)
250		c.applyAction(ActionMoveWindowDown) // {1} {2*} {4 5} {3}
251		w.FocusID(2)
252		w.ToggleFullWidth()
253		wantBands(t, w, []WindowID{1, 2}, []WindowID{4, 5}, []WindowID{3})
254	})
255	t.Run("first window of a pushed column closed", func(t *testing.T) {
256		_, w := cascadeMovesCore(t, 4, 3, false) // {1 2 3} {4}
257		w.FocusID(4)
258		w.ConsumeOrExpel(-1) // {1 2 3/4}
259		w.FocusID(2)
260		w.ToggleFullWidth() // {1} {2*} {3/4}
261		w.RemoveWindow(3)
262		w.FocusID(2)
263		w.ToggleFullWidth()
264		wantBands(t, w, []WindowID{1, 2, 4})
265	})
266	t.Run("no room keeps the other band as it is", func(t *testing.T) {
267		c, w := cascadeMovesCore(t, 3, 3, false)
268		w.FocusID(2)
269		w.Apply(ActionCycleColumnWidth) // {1 2} {3}
270		w.AddWindow(6)                  // {1 2 6} {3}
271		w.FocusID(3)
272		w.AddWindow(5) // {1 2 6} {3 5}
273		w.AddWindow(4) // {1 2 6} {3 5 4}
274		c.applyAction(ActionMoveColumnLeft)
275		c.applyAction(ActionMoveColumnLeft) // {1 2 6} {4 3 5}
276		w.FocusID(2)
277		w.Apply(ActionCycleColumnWidth) // End of the cycle: 3 has no room.
278		wantBands(t, w, []WindowID{1, 2, 6}, []WindowID{4, 3, 5})
279		if got := cellsOf(w)[:3]; !slices.Equal(got, []int{0, 0, 0}) {
280			t.Fatalf("cells %v, want equal", got)
281		}
282	})
283}
284
285// Quiet placement (window rules) and slot windows join the focused band
286// like a new window, keeping the focus.
287func TestCascadeQuietAndSlotPlacement(t *testing.T) {
288	_, w := cascadeMovesCore(t, 5, 3, false) // {1 2 3} {4 5}
289	w.FocusID(4)
290	w.addColumnQuiet(Column{Windows: []WindowID{6}})
291	wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{4, 5, 6})
292	w.FocusID(2)
293	w.addColumnQuiet(Column{Windows: []WindowID{7}})
294	wantBands(t, w, []WindowID{1, 2, 3}, []WindowID{7}, []WindowID{4, 5, 6})
295	if id, _ := w.Focused(); id != 2 {
296		t.Fatalf("focus %d, want 2", id)
297	}
298
299	_, w = cascadeMovesCore(t, 2, 3, false) // {1 2}
300	w.AddSlotWindow(9, 1, 0)
301	wantBands(t, w, []WindowID{9, 1, 2})
302	if id, _ := w.Focused(); id != 2 {
303		t.Fatalf("focus %d, want 2", id)
304	}
305}
306
307// Reapplying the config with a named max-columns above the default keeps
308// the named workspace's bands.
309func TestCascadeNamedMaxColumnsKeepsBands(t *testing.T) {
310	m := newMonitor("", "")
311	m.SetOutput(900, 600)
312	m.SetOverflow(OverflowCascade)
313	m.SetMaxColumns(3)
314	m.SetNamed([]NamedWorkspace{{Name: "wide", MaxColumns: 5}})
315	m.ToggleNamed("wide")
316	w := m.Current()
317	for id := WindowID(1); id <= 5; id++ {
318		w.AddWindow(id)
319	}
320	wantBands(t, w, []WindowID{1, 2, 3, 4, 5})
321	m.SetMaxColumns(3)
322	wantBands(t, w, []WindowID{1, 2, 3, 4, 5})
323}
324
325func bandIDs(w *Workspace) [][]WindowID {
326	var out [][]WindowID
327	w.walkBands(func(_, first, last int) bool {
328		var b []WindowID
329		for _, c := range w.Columns[first : last+1] {
330			b = append(b, c.Windows[0])
331		}
332		out = append(out, slices.Clone(b))
333		return true
334	})
335	return out
336}

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