feat/hints a471fa4

overview_unfold_test.go RawHistoryFolder

1package core
2
3import "testing"
4
5// cards are the shown stash placements of w in ps, in stash order.
6func shownCards(t *testing.T, w *Workspace, ps []Placement) []Placement {
7	t.Helper()
8	var out []Placement
9	for _, e := range w.Stash {
10		if p := previewOf(t, ps, e.ID); !p.Hidden {
11			out = append(out, p)
12		}
13	}
14	return out
15}
16
17// Selected, the cascade pile unfolds into a carousel under the lane: every
18// card side by side, none over another, the selection the only framed one,
19// in the middle. Left and right walk it, wrapping round, without leaving
20// the workspace.
21func TestOverviewCascadeStashUnfolds(t *testing.T) {
22	for _, d := range cascadeDrivers {
23		t.Run(d.name, func(t *testing.T) {
24			m := cascadeOverview()
25			withStash(m, 3)
26			w := m.Current()
27			w.FocusID(10)
28			m.ToggleOverview()
29			d.move(m, 0, 1)
30			at := m.cardAt(w)
31			if at < 0 {
32				t.Fatal("setup: no card selected")
33			}
34			ps := m.Layout()
35			cards := shownCards(t, w, ps)
36			if len(cards) != 3 {
37				t.Fatalf("%d cards shown, want 3", len(cards))
38			}
39			lane := previewOf(t, ps, 10).Rect
40			// Left to right: the card before the selection, the
41			// selection, the one after it (wrapping round).
42			row := []Placement{cards[(at+2)%3], cards[at], cards[(at+1)%3]}
43			for i, c := range row {
44				if c.Rect.Y <= lane.Y+lane.H {
45					t.Fatalf("card %d %v not under the lane %v", c.ID, c.Rect, lane)
46				}
47				if i > 0 && (c.Rect.Y != row[0].Rect.Y || c.Rect.X < row[i-1].Rect.X+row[i-1].Rect.W) {
48					t.Fatalf("cards not side by side: %v then %v", row[i-1].Rect, c.Rect)
49				}
50				if c.Focused != (i == 1) {
51					t.Fatalf("card %d focused %v, selection %d", c.ID, c.Focused, at)
52				}
53			}
54			for i := range 4 {
55				d.move(m, -1, 0)
56				if want := (at - i - 1 + 6) % 3; m.Current() != w || m.cardAt(w) != want {
57					t.Fatalf("left %d: card %d, want %d", i, m.cardAt(w), want)
58				}
59			}
60			for i := range 4 {
61				before := m.cardAt(w)
62				d.move(m, 1, 0)
63				if m.Current() != w || m.cardAt(w) != (before+1)%3 {
64					t.Fatalf("right %d: card %d, was %d", i, m.cardAt(w), before)
65				}
66			}
67		})
68	}
69}
70
71// Fewer than three cards keep the stash order without wrapping round: each
72// side has the card its move selects, and past the ends the selection
73// changes workspace, as from a lane.
74func TestOverviewCascadeSmallCarousel(t *testing.T) {
75	for n := 1; n <= 2; n++ {
76		m := cascadeOverview()
77		withStash(m, n)
78		w := m.Current()
79		w.FocusID(10)
80		m.ToggleOverview()
81		m.selectCard(w, 0)
82		if n == 2 {
83			ps := m.Layout()
84			if a, b := previewOf(t, ps, w.Stash[0].ID).Rect.X, previewOf(t, ps, w.Stash[1].ID).Rect.X; b <= a {
85				t.Fatalf("2 cards: the next card at %d, not right of the selection at %d", b, a)
86			}
87			m.OverviewMove(1, 0)
88			if m.Current() != w || m.cardAt(w) != 1 {
89				t.Fatalf("2 cards: right selected card %d", m.cardAt(w))
90			}
91		}
92		m.OverviewMove(1, 0)
93		if m.Current() != m.Workspaces[2] {
94			t.Fatalf("%d cards: right past the last card stayed on workspace %d", n, indexOf(m.Workspaces, m.Current()))
95		}
96	}
97}
98
99// The carousel keeps the stash order, wrapping round: the stride between
100// neighbors is even, wider at the seam between the newest card and the
101// oldest.
102func TestOverviewCascadeCarouselSeam(t *testing.T) {
103	m := cascadeOverview()
104	withStash(m, 5)
105	w := m.Current()
106	m.ToggleOverview()
107	m.selectCard(w, 4)
108	ps := m.Layout()
109	x := func(i int) int { return previewOf(t, ps, w.Stash[i].ID).Rect.X }
110	stride := x(4) - x(3)
111	if stride <= 0 || x(3)-x(2) != stride {
112		t.Fatalf("left of the newest card: %d %d %d", x(2), x(3), x(4))
113	}
114	if seam := x(0) - x(4); seam <= stride {
115		t.Fatalf("seam %d not wider than the stride %d", seam, stride)
116	}
117	if x(1)-x(0) != stride {
118		t.Fatalf("right of the seam: %d %d", x(0), x(1))
119	}
120}
121
122// Down past the last lane enters the stash at its own selection; up leaves
123// it, and the pile folds again.
124func TestOverviewCascadeStashFolds(t *testing.T) {
125	m := cascadeOverview()
126	withStash(m, 3)
127	w := m.Current()
128	w.FocusID(10)
129	m.ToggleOverview()
130	folded := shownCards(t, w, m.Layout())
131	m.OverviewMove(0, 1)
132	m.OverviewMove(0, -1)
133	if m.cardAt(w) >= 0 {
134		t.Fatal("up did not leave the stash")
135	}
136	again := shownCards(t, w, m.Layout())
137	if len(again) != len(folded) {
138		t.Fatalf("folded again: %d cards, was %d", len(again), len(folded))
139	}
140	for i := range again {
141		if again[i].Rect != folded[i].Rect || again[i].Focused {
142			t.Fatalf("card %d folded at %v (focused %v), was %v", again[i].ID, again[i].Rect, again[i].Focused, folded[i].Rect)
143		}
144	}
145}
146
147// A carousel longer than the area keeps every selection centred in it,
148// with cards on both sides, on either axis; the cards past the area are
149// hidden.
150func TestOverviewCarouselScrolls(t *testing.T) {
151	for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} {
152		t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) {
153			m := cascadeOverview()
154			w := m.Current()
155			if axis == verticalAxis {
156				w.Overflow = OverflowScroll
157			}
158			withStash(m, 12)
159			m.ToggleOverview()
160			if m.overviewAxis() != axis {
161				t.Fatal("setup: wrong overview axis")
162			}
163			m.selectCard(w, 11)
164			u := w.overviewArea()
165			pos := func(r Rect) (int, int) {
166				if axis == verticalAxis {
167					return r.Y, r.Y + r.H/2 - (u.Y + u.H/2)
168				}
169				return r.X, r.X + r.W/2 - (u.X + u.W/2)
170			}
171			for step := range len(w.Stash) + 1 {
172				i := m.cardAt(w)
173				ps := m.Layout()
174				sel := previewOf(t, ps, w.Stash[i].ID)
175				at, off := pos(sel.Rect)
176				if sel.Hidden || !sel.Focused || abs(off) > 1 {
177					t.Fatalf("step %d: card %d not centred: %+v in %v", step, i, sel, u)
178				}
179				before, after, hidden := false, false, false
180				for _, e := range w.Stash {
181					p := previewOf(t, ps, e.ID)
182					if p.Hidden {
183						hidden = true
184						continue
185					}
186					q, _ := pos(p.Rect)
187					before, after = before || q < at, after || q > at
188				}
189				if !before || !after || !hidden {
190					t.Fatalf("step %d: cards before %v, after %v, hidden %v", step, before, after, hidden)
191				}
192				if axis == verticalAxis {
193					m.OverviewMove(0, 1)
194				} else {
195					m.OverviewMove(1, 0)
196				}
197			}
198		})
199	}
200}
201
202// The stash pile, folded or unfolded, is laid out straight into the
203// overview layout every frame: it adds no allocation to the same layout
204// without a stash.
205func TestOverviewStashLayoutAllocations(t *testing.T) {
206	allocs := func(m *Monitor) float64 {
207		buf := m.layoutInto(nil)
208		return testing.AllocsPerRun(50, func() { buf = m.layoutInto(buf) })
209	}
210	bare := cascadeOverview()
211	bare.ToggleOverview()
212	want := allocs(bare)
213	for _, unfolded := range []bool{false, true} {
214		m := cascadeOverview()
215		withStash(m, 6)
216		m.ToggleOverview()
217		if unfolded {
218			m.selectCard(m.Current(), 0)
219		}
220		if got := allocs(m); got > want {
221			t.Errorf("unfolded %v: %v allocs per layout, %v without a stash", unfolded, got, want)
222		}
223	}
224}
225
226// A scroll overview unfolds its pile into a carousel on the left of the
227// row: a column centred on the selection, the card before it above and the
228// one after it below, wrapping round. Up and down walk it round, right goes
229// back to the columns.
230func TestOverviewScrollStashUnfolds(t *testing.T) {
231	m := overviewMonitor()
232	w := m.Current()
233	for id := WindowID(40); id < 43; id++ {
234		w.AddWindow(id)
235		w.FocusID(id)
236		w.ToggleWindowStash()
237	}
238	w.FocusID(1)
239	m.ToggleOverview()
240	if m.overviewAxis() != verticalAxis {
241		t.Fatal("setup: not a vertical overview")
242	}
243	m.OverviewMove(-1, 0)
244	at := m.cardAt(w)
245	if at < 0 {
246		t.Fatal("left from the first column did not select the stash")
247	}
248	cards := shownCards(t, w, m.Layout())
249	if len(cards) != 3 {
250		t.Fatalf("%d cards shown, want 3", len(cards))
251	}
252	col := []Placement{cards[(at+2)%3], cards[at], cards[(at+1)%3]}
253	for i := 1; i < len(col); i++ {
254		if col[i].Rect.X != col[0].Rect.X || col[i].Rect.Y < col[i-1].Rect.Y+col[i-1].Rect.H {
255			t.Fatalf("cards not stacked down: %v then %v", col[i-1].Rect, col[i].Rect)
256		}
257	}
258	if !col[1].Focused {
259		t.Fatal("the selection is not in the middle")
260	}
261	m.OverviewMove(0, 1)
262	if want := (at + 1) % 3; m.cardAt(w) != want {
263		t.Fatalf("down: card %d, want %d", m.cardAt(w), want)
264	}
265	m.OverviewMove(0, -1)
266	m.OverviewMove(0, -1)
267	if want := (at + 2) % 3; m.cardAt(w) != want {
268		t.Fatalf("up twice: card %d, want %d", m.cardAt(w), want)
269	}
270	m.OverviewMove(1, 0)
271	if m.cardAt(w) >= 0 || m.overviewTarget() == 0 {
272		t.Fatalf("right: card %d, target %d", m.cardAt(w), m.overviewTarget())
273	}
274}
275
276// A stash-only workspace's carousel does not wrap round, drawn or walked:
277// the selection stays in the middle, the cards in stash order on either
278// side of it. Past the ends a vertical overview stops; a cascade one
279// changes workspace.
280func TestOverviewStashOnlyCarousel(t *testing.T) {
281	for _, axis := range []layoutAxis{horizontalAxis, verticalAxis} {
282		t.Run(map[layoutAxis]string{horizontalAxis: "cascade", verticalAxis: "scroll"}[axis], func(t *testing.T) {
283			m := overviewMonitor()
284			m.Focus(2)
285			m.AddWindow(30)
286			m.Focus(1)
287			w := m.Current()
288			w.Overflow = OverflowCascade
289			if axis == verticalAxis {
290				w.Overflow = OverflowScroll
291			}
292			w.ToggleWindowStash() // its own window
293			for id := WindowID(40); id < 44; id++ {
294				w.AddWindow(id)
295				w.FocusID(id)
296				w.ToggleWindowStash()
297			}
298			m.ToggleOverview()
299			if m.overviewAxis() != axis || len(w.Columns) != 0 || m.cardAt(w) < 0 {
300				t.Fatalf("setup: axis %d, %d columns, card %d", m.overviewAxis(), len(w.Columns), m.cardAt(w))
301			}
302			last := len(w.Stash) - 1
303			m.selectCard(w, last)
304			ps := m.Layout()
305			pos := func(i int) int {
306				if r := previewOf(t, ps, w.Stash[i].ID).Rect; axis == verticalAxis {
307					return r.Y
308				} else {
309					return r.X
310				}
311			}
312			for i := 1; i <= last; i++ {
313				if pos(i) <= pos(i-1) {
314					t.Fatalf("card %d at %d, not after card %d at %d", i, pos(i), i-1, pos(i-1))
315				}
316			}
317			if axis == verticalAxis {
318				m.OverviewMove(0, 1)
319				if m.Current() != w || m.cardAt(w) != last {
320					t.Fatalf("down past the last card: card %d", m.cardAt(w))
321				}
322				return
323			}
324			m.OverviewMove(1, 0)
325			if m.Current() == w {
326				t.Fatal("right past the last card stayed on the workspace")
327			}
328		})
329	}
330}
331
332// A carousel that does not wrap round and is longer than the area stops at
333// its edges, like a wide row: the first card at the start of the area, the
334// selection inside it.
335func TestOverviewStashOnlyCarouselScrolls(t *testing.T) {
336	m := overviewMonitor()
337	m.Focus(1)
338	w := m.Current()
339	w.Overflow = OverflowCascade
340	w.ToggleWindowStash() // its own window
341	for id := WindowID(40); id < 52; id++ {
342		w.AddWindow(id)
343		w.FocusID(id)
344		w.ToggleWindowStash()
345	}
346	m.ToggleOverview()
347	if w.wrapsCards() || m.cardAt(w) < 0 {
348		t.Fatalf("setup: wraps %v, card %d", w.wrapsCards(), m.cardAt(w))
349	}
350	u := w.overviewArea()
351	m.selectCard(w, 0)
352	ps := m.Layout()
353	if r := previewOf(t, ps, w.Stash[0].ID).Rect; r.X != u.X {
354		t.Fatalf("first card at %d, area starts at %d", r.X, u.X)
355	}
356	m.selectCard(w, len(w.Stash)-1)
357	ps = m.Layout()
358	if r := previewOf(t, ps, w.Stash[len(w.Stash)-1].ID).Rect; r.X+r.W != u.X+u.W {
359		t.Fatalf("last card ends at %d, area at %d", r.X+r.W, u.X+u.W)
360	}
361}
362
363// Stepping to a workspace whose stash has the focus selects its stack when
364// the carousel wraps round, so the next step goes on to the next workspace;
365// a carousel that does not wrap is still selected.
366func TestOverviewStepSkipsWrappingCarousel(t *testing.T) {
367	for _, overflow := range []Overflow{OverflowCascade, OverflowScroll} {
368		for _, n := range []int{2, 3} {
369			m := overviewMonitor()
370			m.Focus(2)
371			m.AddWindow(30)
372			m.Focus(1)
373			w := m.Current()
374			for id := WindowID(40); id < WindowID(40+n); id++ {
375				w.AddWindow(id)
376				w.FocusID(id)
377				w.ToggleWindowStash()
378			}
379			for _, ws := range m.Workspaces[:3] {
380				ws.Overflow = overflow
381			}
382			m.Focus(0)
383			if !w.stashFocused() {
384				t.Fatal("setup: the stash does not have the focus")
385			}
386			m.ToggleOverview()
387			m.overviewFocus(ActionFocusWorkspaceNext)
388			if m.Current() != w {
389				t.Fatalf("%s, %d cards: on workspace %d", overflow, n, indexOf(m.Workspaces, m.Current()))
390			}
391			if got, want := m.cardAt(w) >= 0, n < 3; got != want {
392				t.Fatalf("%s, %d cards: card selected %v, want %v", overflow, n, got, want)
393			}
394			if n < 3 {
395				continue
396			}
397			m.overviewFocus(ActionFocusWorkspaceNext)
398			if m.Current() != m.Workspaces[2] {
399				t.Fatalf("%s: the next step stayed on workspace %d", overflow, indexOf(m.Workspaces, m.Current()))
400			}
401		}
402	}
403}

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