border_test.go RawHistoryFolder

1package core
2
3import (
4	"slices"
5	"testing"
6	"time"
7)
8
9func TestBorderAt(t *testing.T) {
10	for _, mode := range []Overflow{OverflowFixed, OverflowCascade, OverflowScroll} {
11		for _, gap := range []int{0, 10} {
12			w := spanWorkspace(mode, 2)
13			w.SetGaps(gap)
14			w.View = 0
15			w.view = slide{}
16			rs := w.columnRects()
17			x := rs[0].X + rs[0].W + gap/2
18			y := rs[0].Y + 100
19			b, ok := w.borderAt(x, y, 6)
20			if !ok || b.kind != borderColumn || b.col != 0 {
21				t.Fatal(mode, gap, b, ok)
22			}
23			if _, ok = w.borderAt(x-5, y, 6); !ok {
24				t.Fatal("padding")
25			}
26			if _, ok = w.borderAt(rs[0].X+20, y, 6); ok {
27				t.Fatal("tile interior")
28			}
29			w.Columns[0].Windows = append(w.Columns[0].Windows, 9)
30			rows := rowRects(rs[0], w.Columns[0], gap)
31			b, ok = w.borderAt(rs[0].X+20, rows[0].Y+rows[0].H+gap/2, 6)
32			if !ok || b.kind != borderRow {
33				t.Fatal("row", b, ok)
34			}
35			w.AddFloating(10, 400, 400)
36			f := w.floatRect(w.Floats[0])
37			if _, ok = w.borderAt(f.X+f.W/2, f.Y+f.H/2, 6); ok {
38				t.Fatal("float")
39			}
40			w.FocusID(1)
41			w.SetFullscreen(1, true)
42			if _, ok = w.borderAt(x, y, 6); ok {
43				t.Fatal("fullscreen")
44			}
45		}
46	}
47	w := spanWorkspace(OverflowFixed, 4)
48	rs := w.columnRects()
49	if _, ok := w.borderAt(rs[0].X+rs[0].W+5, 100, 8); ok {
50		t.Fatal("spiral")
51	}
52	w = spanWorkspace(OverflowFixed, 2)
53	w.Columns[w.Focus].FullWidth = true
54	if _, ok := w.borderAt(500, 100, 8); ok {
55		t.Fatal("maximized")
56	}
57	w = spanWorkspace(OverflowCascade, 4)
58	rs = w.columnRects()
59	if _, ok := w.borderAt(rs[2].X+rs[2].W+5, 100, 8); ok {
60		t.Fatal("different bands")
61	}
62	// Presentation offsets must be applied just like Layout.
63	w = spanWorkspace(OverflowScroll, 4)
64	w.View = 100
65	rs = w.columnRects()
66	b, ok := w.borderAt(rs[0].X+rs[0].W+5, 100, 6)
67	if !ok || b.col != 0 {
68		t.Fatal("view offset", b, ok)
69	}
70}
71
72func TestSetColumnBorder(t *testing.T) {
73	for _, mode := range []Overflow{OverflowFixed, OverflowCascade} {
74		for _, n := range []int{2, 3} {
75			w := spanWorkspace(mode, n)
76			before := w.columnRects()
77			b := border{kind: borderColumn, col: 0}
78			pos := before[0].X + before[0].W + 5 + 50
79			w.setBorder(b, pos)
80			after := w.columnRects()
81			if after[0].X+after[0].W+5 != pos {
82				t.Fatal("position", mode, n, pos, after)
83			}
84			if n == 3 && after[2] != before[2] {
85				t.Fatal("third moved", before, after)
86			}
87			if after[n-1].X+after[n-1].W != 990 {
88				t.Fatal("not pixel filled", after)
89			}
90			total := 0
91			for _, c := range w.Columns {
92				total += c.Span
93			}
94			if total != 10000 {
95				t.Fatal(total)
96			}
97			pair := w.Columns[0].Span + w.Columns[1].Span
98			w.setBorder(b, -1000)
99			after = w.columnRects()
100			if after[0].W < 48 || w.Columns[0].Span+w.Columns[1].Span != pair {
101				t.Fatal("minimum", after)
102			}
103			w.setBorder(b, 10000)
104			after = w.columnRects()
105			if after[1].W < 48 {
106				t.Fatal("right minimum", after)
107			}
108			w.resetBorder(b)
109			for _, c := range w.Columns {
110				if c.Span != 0 || c.spanStamp != 0 {
111					t.Fatal("reset")
112				}
113			}
114		}
115	}
116}
117
118func TestSetBorderCascadeBand(t *testing.T) {
119	w := spanWorkspace(OverflowCascade, 5)
120	before := w.columnRects()
121	w.setBorder(border{kind: borderColumn, col: 0}, 400)
122	after := w.columnRects()
123	if !slices.Equal(before[3:], after[3:]) || w.Columns[3].Span != 0 {
124		t.Fatal("other band changed", before, after)
125	}
126}
127
128func TestSetBorderScroll(t *testing.T) {
129	w := spanWorkspace(OverflowScroll, 3)
130	before := w.columnRects()
131	b := border{kind: borderColumn, col: 0}
132	pos := before[0].X + before[0].W + 5 + 60
133	w.setBorder(b, pos)
134	after := w.columnRects()
135	if after[0].X+after[0].W+5 != pos {
136		t.Fatal("position", after)
137	}
138	for i := 1; i < len(after); i++ {
139		if after[i].W != before[i].W || after[i].X != before[i].X+60 {
140			t.Fatal("right columns", before, after)
141		}
142	}
143	saved := w.Columns[1].Span
144	w.setBorder(b, 10000)
145	if w.columnRects()[0].W != 980 || w.Columns[1].Span != saved {
146		t.Fatal("maximum")
147	}
148	w.setBorder(b, -10000)
149	if w.columnRects()[0].W < 49 {
150		t.Fatal("minimum")
151	}
152}
153
154func TestSetRowBorder(t *testing.T) {
155	w := spanWorkspace(OverflowFixed, 2)
156	w.Columns[0].Windows = []WindowID{1, 3, 4}
157	r := w.columnRects()[0]
158	before := rowRects(r, w.Columns[0], w.gap())
159	b := border{kind: borderRow, col: 0, row: 0}
160	pos := before[0].Y + before[0].H + 5 + 30
161	w.setBorder(b, pos)
162	after := rowRects(r, w.Columns[0], w.gap())
163	if after[0].Y+after[0].H+5 != pos || after[2] != before[2] || sum(w.Columns[0].Shares) != 10000 {
164		t.Fatal(before, after, w.Columns[0].Shares)
165	}
166	pair := w.Columns[0].Shares[0] + w.Columns[0].Shares[1]
167	w.setBorder(b, -10000)
168	if w.Columns[0].Shares[0] != minShare {
169		t.Fatal("minimum")
170	}
171	w.setBorder(b, 10000)
172	if w.Columns[0].Shares[1] != minShare || w.Columns[0].Shares[0]+w.Columns[0].Shares[1] != pair {
173		t.Fatal("pair")
174	}
175	w.resetBorder(b)
176	if w.Columns[0].Shares != nil {
177		t.Fatal("reset")
178	}
179}
180
181func TestBorderPixelRemainders(t *testing.T) {
182	for _, mode := range []Overflow{OverflowFixed, OverflowCascade} {
183		for n := 2; n <= 4; n++ {
184			for width := 997; width <= 1003; width++ {
185				for pair := 0; pair < n-1; pair++ {
186					w := spanWorkspace(mode, n)
187					w.SetMaxColumns(n)
188					for i := range w.Columns {
189						w.Columns[i].BandStart = false
190					}
191					w.SetOutput(width, 800)
192					before := w.columnRects()
193					b := border{kind: borderColumn, col: pair}
194					pos := before[pair].X + before[pair].W + 5
195					w.setBorder(b, pos)
196					if after := w.columnRects(); !slices.Equal(before, after) {
197						t.Fatalf("stamp changed geometry: mode=%v n=%d width=%d pair=%d before=%v after=%v", mode, n, width, pair, before, after)
198					}
199					for delta := -20; delta <= 20; delta++ {
200						w.setBorder(b, pos+delta)
201						after := w.columnRects()
202						for i := range before {
203							if i != pair && i != pair+1 && after[i] != before[i] {
204								t.Fatalf("untouched rect changed: mode=%v n=%d width=%d pair=%d delta=%d before=%v after=%v", mode, n, width, pair, delta, before, after)
205							}
206						}
207						if after[pair].X+after[pair].W+5 != pos+delta {
208							t.Fatal("border missed pointer", before, after)
209						}
210					}
211				}
212			}
213		}
214	}
215}
216
217func TestBorderStampIgnoresMaximizedWidth(t *testing.T) {
218	for _, mode := range []Overflow{OverflowScroll, OverflowFixed, OverflowCascade} {
219		t.Run(string(mode), func(t *testing.T) {
220			w := spanWorkspace(mode, 3)
221			normal := w.columnWidthFor(2, true)
222			w.Columns[2].FullWidth = true
223			rs := w.columnRects()
224			w.setBorder(border{kind: borderColumn, col: 0}, rs[0].X+rs[0].W+15)
225			w.Columns[2].FullWidth = false
226			if got := w.columnWidth(2); got != normal {
227				t.Fatalf("unmaximized width = %d, want %d", got, normal)
228			}
229		})
230	}
231}
232
233func TestSetBorderReportsChange(t *testing.T) {
234	for _, mode := range []Overflow{OverflowScroll, OverflowFixed} {
235		w := spanWorkspace(mode, 2)
236		b := border{kind: borderColumn, col: 0}
237		x := w.columnRects()[0]
238		pos := x.X + x.W + 5 + 40
239		if !w.setBorder(b, pos) {
240			t.Fatal(mode, "first move must report a change")
241		}
242		if w.setBorder(b, pos) {
243			t.Fatal(mode, "same position reported a change")
244		}
245		w.setBorder(b, -10000)
246		if w.setBorder(b, -20000) {
247			t.Fatal(mode, "clamped at the minimum reported a change")
248		}
249	}
250	w := spanWorkspace(OverflowFixed, 2)
251	w.Columns[0].Windows = []WindowID{1, 3}
252	b := border{kind: borderRow, col: 0}
253	r := w.columnRects()[0]
254	pos := rowRects(r, w.Columns[0], w.gap())[0].Y + 200
255	if !w.setBorder(b, pos) || w.setBorder(b, pos) {
256		t.Fatal("row border change reporting")
257	}
258	if w.setBorder(border{kind: borderColumn, col: 9}, 0) {
259		t.Fatal("invalid border reported a change")
260	}
261}
262
263func TestBorderMotionPublishesOnlyChanges(t *testing.T) {
264	c, w := borderCore(t, false)
265	grabButton(t, c, btnRight, true, time.Second)
266	grabCommands(c)
267	grabMove(t, c, c.cursorX+150, c.cursorY)
268	if len(grabCommands(c)) == 0 {
269		t.Fatal("a resizing motion must publish")
270	}
271	spans := []int{w.Columns[0].Span, w.Columns[1].Span}
272	grabMove(t, c, c.cursorX, c.cursorY)
273	if cmds := grabCommands(c); len(cmds) != 0 {
274		t.Fatalf("unchanged motion published %+v", cmds)
275	}
276	if spans[0] != w.Columns[0].Span || spans[1] != w.Columns[1].Span {
277		t.Fatal("spans changed")
278	}
279	grabMove(t, c, -10000, c.cursorY)
280	grabCommands(c)
281	grabMove(t, c, -20000, c.cursorY)
282	if cmds := grabCommands(c); len(cmds) != 0 {
283		t.Fatalf("motion clamped at the minimum published %+v", cmds)
284	}
285}
286
287func TestSetBorderUndersizedPairReportsStampOnce(t *testing.T) {
288	t.Run("row", func(t *testing.T) {
289		w := spanWorkspace(OverflowFixed, 1)
290		w.Columns[0].Windows = []WindowID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
291		// Ten rows is the most that fit; pixel rounding leaves the last pair
292		// just under twice the minimum share.
293		b := border{kind: borderRow, col: 0, row: 8}
294		rows := rowRects(w.columnRects()[0], w.Columns[0], w.gap())
295		pos := rows[8].Y + 1
296		if !w.setBorder(b, pos) {
297			t.Fatal("stamping an undersized row pair must report a change")
298		}
299		if c := w.Columns[0]; c.Shares[8]+c.Shares[9] >= 2*minShare {
300			t.Fatal("row pair not undersized", c.Shares)
301		}
302		if w.setBorder(b, pos) {
303			t.Fatal("repeat on an undersized row pair reported a change")
304		}
305	})
306	for _, mode := range []Overflow{OverflowFixed, OverflowCascade} {
307		t.Run(string(mode), func(t *testing.T) {
308			w := spanWorkspace(mode, 3)
309			w.border = 300
310			b := border{kind: borderColumn, col: 0}
311			pos := w.columnRects()[0].X + 100
312			if !w.setBorder(b, pos) {
313				t.Fatal("stamping an undersized column pair must report a change")
314			}
315			avail := w.Usable.W - w.gap()*4
316			if pair := w.Columns[0].Span + w.Columns[1].Span; pair >= 2*pixelShare(4*w.border, avail) {
317				t.Fatal("column pair not undersized", pair)
318			}
319			if w.setBorder(b, pos) {
320				t.Fatal("repeat on an undersized column pair reported a change")
321			}
322		})
323	}
324}

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 26

Tags 10

↑↓ moveenter openesc close