v0.8.0 446d1e3

cascade_moves_test.go RawHistoryFolder

1package core
2
3import (
4	"slices"
5	"testing"
6)
7
8// cascadeMovesCore is a core whose focused screen holds a cascade of
9// windows 1..n in bands of maxCols. With sides, screens L and R sit on
10// either side of it; otherwise it is alone.
11func cascadeMovesCore(t *testing.T, n, maxCols int, sides bool) (*Core, *Workspace) {
12	t.Helper()
13	tiles := []tile{{"M", 900, 0, 900, 600}}
14	focus := 0
15	if sides {
16		tiles = []tile{{"L", 0, 0, 900, 600}, tiles[0], {"R", 1800, 0, 900, 600}}
17		focus = 1
18	}
19	c := layoutCore(focus, tiles...)
20	w := c.cur().mon.Current()
21	w.Overflow = OverflowCascade
22	w.SetMaxColumns(maxCols)
23	for id := WindowID(1); id <= WindowID(n); id++ {
24		w.AddWindow(id)
25	}
26	c.cur().mon.normalize() // the trailing empty workspace, as in a session
27	return c, w
28}
29
30// columnOrder is the first window of each column, in column order.
31func columnOrder(w *Workspace) []WindowID {
32	var ids []WindowID
33	for _, c := range w.Columns {
34		ids = append(ids, c.Windows[0])
35	}
36	return ids
37}
38
39func screenWorkspace(c *Core, name string) *Workspace {
40	for _, s := range c.screens {
41		if s.name() == name {
42			return s.mon.Current()
43		}
44	}
45	return nil
46}
47
48func wantColumns(t *testing.T, w *Workspace, want ...WindowID) {
49	t.Helper()
50	if got := columnOrder(w); !slices.Equal(got, want) {
51		t.Fatalf("columns %v, want %v", got, want)
52	}
53}
54
55func wantFocus(t *testing.T, c *Core, screen string, id WindowID) {
56	t.Helper()
57	if got := c.cur().name(); got != screen {
58		t.Fatalf("focused screen %q, want %q", got, screen)
59	}
60	if got, _ := c.cur().mon.Current().Focused(); got != id {
61		t.Fatalf("focused window %d, want %d", got, id)
62	}
63}
64
65// At the edge of its band, a cascade column goes to the neighbor monitor
66// on that side, and focus with it; bands {1 2 3} {4 5}.
67func TestCascadeBandEdgeMovesToNeighborMonitor(t *testing.T) {
68	for _, tc := range []struct {
69		name         string
70		from         WindowID
71		action       Action
72		screen       string
73		here, moved  []WindowID
74		focusedAfter WindowID
75	}{
76		{"move left from band start", 4, ActionMoveColumnLeft, "L", []WindowID{1, 2, 3, 5}, []WindowID{4}, 4},
77		{"move right from band end", 3, ActionMoveColumnRight, "R", []WindowID{1, 2, 4, 5}, []WindowID{3}, 3},
78		{"move right from last column", 5, ActionMoveColumnRight, "R", []WindowID{1, 2, 3, 4}, []WindowID{5}, 5},
79		{"focus left from band start", 4, ActionFocusColumnLeft, "L", []WindowID{1, 2, 3, 4, 5}, nil, 0},
80		{"focus right from band end", 3, ActionFocusColumnRight, "R", []WindowID{1, 2, 3, 4, 5}, nil, 0},
81	} {
82		t.Run(tc.name, func(t *testing.T) {
83			c, w := cascadeMovesCore(t, 5, 3, true)
84			w.FocusID(tc.from)
85			c.applyAction(tc.action)
86			wantColumns(t, w, tc.here...)
87			wantColumns(t, screenWorkspace(c, tc.screen), tc.moved...)
88			wantFocus(t, c, tc.screen, tc.focusedAfter)
89		})
90	}
91}
92
93// Inside a band, left/right stay on the screen.
94func TestCascadeInsideBandStaysOnMonitor(t *testing.T) {
95	c, w := cascadeMovesCore(t, 5, 3, true)
96	w.FocusID(5)
97	c.applyAction(ActionMoveColumnLeft)
98	wantColumns(t, w, 1, 2, 3, 5, 4)
99	wantFocus(t, c, "M", 5)
100	c.applyAction(ActionFocusColumnRight)
101	wantFocus(t, c, "M", 4)
102}
103
104// With no monitor on that side, left/right never cross into the neighbor
105// band: at the band edge they step the workspace (the first has none before
106// it; the last is the trailing empty one). up/down cross bands, and
107// move-column still crosses in column order.
108func TestCascadeBandEdgeWithoutMonitorFocusStepsMoveCrosses(t *testing.T) {
109	c, w := cascadeMovesCore(t, 5, 3, false)
110	m := c.cur().mon
111	w.FocusID(4)
112	c.applyAction(ActionFocusColumnLeft)
113	wantFocus(t, c, "M", 4)
114	if m.Active != 0 {
115		t.Fatalf("active %d, want 0", m.Active)
116	}
117	w.FocusID(3)
118	c.applyAction(ActionFocusColumnRight)
119	if m.Active != 1 || m.Current() == w {
120		t.Fatalf("active %d, want the trailing empty workspace", m.Active)
121	}
122	m.Focus(0)
123	w.FocusID(4)
124	c.applyAction(ActionMoveColumnLeft)
125	wantColumns(t, w, 1, 2, 4, 3, 5)
126	wantFocus(t, c, "M", 4)
127	w.FocusID(4)
128	c.applyAction(ActionMoveColumnRight)
129	wantColumns(t, w, 1, 2, 3, 4, 5)
130	wantFocus(t, c, "M", 4)
131}
132
133// With one column per band, every column is at both edges of its band.
134func TestCascadeOneColumnBandsLeaveForMonitor(t *testing.T) {
135	c, w := cascadeMovesCore(t, 2, 1, true)
136	w.FocusID(2)
137	c.applyAction(ActionMoveColumnLeft)
138	wantColumns(t, w, 1)
139	wantColumns(t, screenWorkspace(c, "L"), 2)
140	wantFocus(t, c, "L", 2)
141}
142
143// Fixed overflow keeps its column order edge: a non-edge column swaps.
144func TestFixedMoveColumnKeepsLinearEdge(t *testing.T) {
145	c, w := cascadeMovesCore(t, 2, 3, true)
146	w.Overflow = OverflowFixed
147	w.FocusID(2)
148	c.applyAction(ActionMoveColumnLeft)
149	wantColumns(t, w, 2, 1)
150	wantFocus(t, c, "M", 2)
151}
152
153// At the top or bottom of its column, move-window-up/down moves a cascade
154// column to the adjacent band, where focus-window-up/down goes; bands
155// {1 2 3} {4 5}.
156func TestCascadeMoveWindowCrossesBands(t *testing.T) {
157	c, w := cascadeMovesCore(t, 5, 3, false)
158	w.FocusID(5)
159	c.applyAction(ActionMoveWindowUp)
160	wantColumns(t, w, 1, 5, 3, 4, 2)
161	wantFocus(t, c, "M", 5)
162	c.applyAction(ActionMoveWindowDown)
163	wantColumns(t, w, 1, 2, 3, 4, 5)
164	wantFocus(t, c, "M", 5)
165}
166
167// The last band is partial: the move clamps to its last column, and going
168// back up keeps the new offset, as focus does.
169func TestCascadeMoveWindowPartialBand(t *testing.T) {
170	c, w := cascadeMovesCore(t, 5, 3, false)
171	w.FocusID(3)
172	c.applyAction(ActionMoveWindowDown)
173	wantColumns(t, w, 1, 2, 5, 4, 3)
174	c.applyAction(ActionMoveWindowUp)
175	wantColumns(t, w, 1, 3, 5, 4, 2)
176	wantFocus(t, c, "M", 3)
177}
178
179// No band past the first or last: nothing moves.
180func TestCascadeMoveWindowStopsAtOuterBands(t *testing.T) {
181	c, w := cascadeMovesCore(t, 5, 3, false)
182	w.FocusID(1)
183	c.applyAction(ActionMoveWindowUp)
184	wantFocus(t, c, "M", 1)
185	w.FocusID(4)
186	c.applyAction(ActionMoveWindowDown)
187	wantColumns(t, w, 1, 2, 3, 4, 5)
188	wantFocus(t, c, "M", 4)
189}
190
191// A maximized column leaving its band gives up maximization: the band it
192// joins shows all its columns.
193func TestCascadeMoveWindowEndsMaximization(t *testing.T) {
194	c, w := cascadeMovesCore(t, 5, 3, false)
195	w.FocusID(5)
196	w.ToggleFullWidth()
197	c.applyAction(ActionMoveWindowUp)
198	wantColumns(t, w, 1, 5, 3, 4, 2)
199	if w.Columns[1].FullWidth || len(w.maximized) != 0 {
200		t.Fatalf("still maximized: full width %t, MRU %v", w.Columns[1].FullWidth, w.maximized)
201	}
202}
203
204// A fullscreen tile at its band edge leaves for the neighbor monitor and
205// stays fullscreen, as a tile there does.
206func TestCascadeFullscreenBandEdgeFocusesMonitor(t *testing.T) {
207	c, w := cascadeMovesCore(t, 5, 3, true)
208	w.FocusID(3)
209	w.ToggleFullscreen()
210	c.applyAction(ActionFocusColumnRight)
211	if got := c.cur().name(); got != "R" {
212		t.Fatalf("focused screen %q, want R", got)
213	}
214	if id, _ := w.Focused(); w.fullscreen != 3 || id != 3 {
215		t.Fatalf("fullscreen %d, focused %d on M, want 3 and 3", w.fullscreen, id)
216	}
217}
218
219// From a native float over the columns, the first move leaves the float
220// on this monitor, even with the focused column at its band edge.
221func TestCascadeFloatAtBandEdgeStaysOnMonitor(t *testing.T) {
222	c, w := cascadeMovesCore(t, 5, 3, true)
223	w.FocusID(3)
224	w.AddFloating(9, 100, 100)
225	c.applyAction(ActionFocusColumnRight)
226	if got := c.cur().name(); got != "M" {
227		t.Fatalf("focused screen %q, want M", got)
228	}
229	if w.floatFocus {
230		t.Fatal("focus still on the float")
231	}
232}
233
234// A stacked window moves inside its column first, then the column changes
235// band.
236func TestCascadeMoveWindowStackedFirst(t *testing.T) {
237	c, w := cascadeMovesCore(t, 5, 3, false)
238	w.FocusID(5)
239	w.ConsumeOrExpel(-1) // bands {1 2 3} {4/5}
240	if got := w.Columns[3].Windows; !slices.Equal(got, []WindowID{4, 5}) {
241		t.Fatalf("stack %v, want [4 5]", got)
242	}
243	c.applyAction(ActionMoveWindowUp)
244	if got := w.Columns[3].Windows; !slices.Equal(got, []WindowID{5, 4}) {
245		t.Fatalf("stack %v, want [5 4]", got)
246	}
247	c.applyAction(ActionMoveWindowUp)
248	wantColumns(t, w, 5, 2, 3, 1)
249	wantFocus(t, c, "M", 5)
250}
251
252// A fullscreen window keeps its band.
253func TestCascadeMoveWindowFullscreenStays(t *testing.T) {
254	c, w := cascadeMovesCore(t, 5, 3, false)
255	w.FocusID(4)
256	w.ToggleFullscreen()
257	c.applyAction(ActionMoveWindowUp)
258	wantColumns(t, w, 1, 2, 3, 4, 5)
259}
260
261// Scroll overflow has no bands: up/down stop at the column edge.
262func TestScrollMoveWindowStopsAtColumnEdge(t *testing.T) {
263	c, w := cascadeMovesCore(t, 2, 3, false)
264	w.Overflow = OverflowScroll
265	w.FocusID(2)
266	c.applyAction(ActionMoveWindowUp)
267	c.applyAction(ActionMoveWindowDown)
268	wantColumns(t, w, 1, 2)
269	wantFocus(t, c, "M", 2)
270}

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