ops.go RawHistoryFolder

1package layout
2
3import (
4	"errors"
5	"math"
6	"sort"
7
8	"github.com/bnema/vev/internal/domain"
9)
10
11var (
12	ErrNotFound      = errors.New("pane not found")
13	ErrTooSmall      = errors.New("layout too small")
14	ErrNoPane        = errors.New("no pane in direction")
15	ErrNotInSplit    = errors.New("pane is not in a split")
16	ErrNotToggleable = errors.New("layout node is not toggleable")
17)
18
19// ResizeFocus changes the focused pane subtree's share in the nearest split on
20// axis. It commits only after the candidate layout solves successfully.
21func (t *Tree) ResizeFocus(axis Axis, delta int, area domain.Rect) error {
22	candidate := t.clone()
23	if candidate == nil || candidate.Root == nil {
24		return ErrNotInSplit
25	}
26	dir, ok := splitDirForAxis(axis)
27	if !ok || !hasMatchingSplit(candidate.Root, candidate.Focus, dir) {
28		return ErrNotInSplit
29	}
30	if _, solved := Solve(candidate.Root, area); !solved {
31		return ErrTooSmall
32	}
33
34	splitNode, splitArea, targetIndex, found := nearestMatchingSplit(candidate.Root, candidate.Focus, dir, area)
35	if !found {
36		return ErrTooSmall
37	}
38	rects, solved := splitChildRects(splitNode, splitArea)
39	if !solved {
40		return ErrTooSmall
41	}
42	extents := make([]int, len(rects))
43	minimums := make([]int, len(rects))
44	for i, rect := range rects {
45		extents[i] = rect.Width
46		if dir == Vertical {
47			extents[i] = rect.Height
48		}
49		minimum, valid := minimumExtent(splitNode.Children[i], dir)
50		if !valid {
51			return ErrTooSmall
52		}
53		minimums[i] = minimum
54		splitNode.Children[i].Weight = float64(extents[i])
55	}
56
57	requested := delta
58	if requested < 0 {
59		if requested == math.MinInt {
60			requested = math.MaxInt
61		} else {
62			requested = -requested
63		}
64	}
65	if requested == 0 {
66		return ErrTooSmall
67	}
68
69	neighbor := -1
70	transfer := 0
71	if delta > 0 {
72		for _, index := range preferredNeighbors(targetIndex, len(extents)) {
73			available := extents[index] - minimums[index]
74			if available <= 0 {
75				continue
76			}
77			neighbor = index
78			transfer = min(requested, available)
79			break
80		}
81	} else {
82		neighbors := preferredNeighbors(targetIndex, len(extents))
83		available := extents[targetIndex] - minimums[targetIndex]
84		if len(neighbors) > 0 && available > 0 {
85			neighbor = neighbors[0]
86			transfer = min(requested, available)
87		}
88	}
89	if neighbor < 0 || transfer == 0 {
90		return ErrTooSmall
91	}
92
93	if delta > 0 {
94		extents[targetIndex] += transfer
95		extents[neighbor] -= transfer
96	} else {
97		extents[targetIndex] -= transfer
98		extents[neighbor] += transfer
99	}
100	splitNode.Children[targetIndex].Weight = float64(extents[targetIndex])
101	splitNode.Children[neighbor].Weight = float64(extents[neighbor])
102	if _, solved := Solve(candidate.Root, area); !solved {
103		return ErrTooSmall
104	}
105	*t = *candidate
106	return nil
107}
108
109// Equalize resets every split's direct child shares to their defaults. The
110// candidate must remain solvable before it is committed.
111func (t *Tree) Equalize(area domain.Rect) error {
112	candidate := t.clone()
113	if candidate == nil || candidate.Root == nil {
114		return ErrTooSmall
115	}
116	clearSplitChildWeights(candidate.Root)
117	if _, ok := Solve(candidate.Root, area); !ok {
118		return ErrTooSmall
119	}
120	*t = *candidate
121	return nil
122}
123
124// CanResize reports whether the focused pane is contained by any split.
125func (t *Tree) CanResize() bool {
126	if t == nil || t.Root == nil {
127		return false
128	}
129	return hasMatchingSplit(t.Root, t.Focus, Horizontal) || hasMatchingSplit(t.Root, t.Focus, Vertical)
130}
131
132func splitDirForAxis(axis Axis) (SplitDir, bool) {
133	switch axis {
134	case Width:
135		return Horizontal, true
136	case Height:
137		return Vertical, true
138	default:
139		return Horizontal, false
140	}
141}
142
143func hasMatchingSplit(n *Node, focus PaneID, dir SplitDir) bool {
144	if n == nil || !containsLeaf(n, focus) {
145		return false
146	}
147	for _, child := range n.Children {
148		if containsLeaf(child, focus) && hasMatchingSplit(child, focus, dir) {
149			return true
150		}
151	}
152	return n.Kind == Split && n.Dir == dir
153}
154
155func nearestMatchingSplit(n *Node, focus PaneID, dir SplitDir, area domain.Rect) (*Node, domain.Rect, int, bool) {
156	if n == nil || !containsLeaf(n, focus) {
157		return nil, domain.Rect{}, 0, false
158	}
159	childAreas := nodeChildAreas(n, area)
160	for i, child := range n.Children {
161		if !containsLeaf(child, focus) {
162			continue
163		}
164		childArea := area
165		if i < len(childAreas) {
166			childArea = childAreas[i]
167		}
168		if match, matchArea, index, ok := nearestMatchingSplit(child, focus, dir, childArea); ok {
169			return match, matchArea, index, true
170		}
171		if n.Kind == Split && n.Dir == dir {
172			return n, area, i, true
173		}
174		return nil, domain.Rect{}, 0, false
175	}
176	return nil, domain.Rect{}, 0, false
177}
178
179func preferredNeighbors(target, count int) []int {
180	neighbors := make([]int, 0, 2)
181	if target+1 < count {
182		neighbors = append(neighbors, target+1)
183	}
184	if target > 0 {
185		neighbors = append(neighbors, target-1)
186	}
187	return neighbors
188}
189
190func clearSplitChildWeights(n *Node) {
191	if n == nil {
192		return
193	}
194	if n.Kind == Split {
195		clearChildWeights(n)
196	}
197	for _, child := range n.Children {
198		clearSplitChildWeights(child)
199	}
200}
201
202// Split adds newID next to target. The after flag controls whether the new pane
203// is placed after target on the chosen axis.
204func (t *Tree) Split(target PaneID, dir Direction, after bool, newID PaneID, area domain.Rect) error {
205	candidate := t.clone()
206	if candidate == nil || candidate.Root == nil || !insertSplit(candidate.Root, target, axisFor(dir), after, newID, area) {
207		return ErrNotFound
208	}
209	candidate.Focus = newID
210	if _, ok := Solve(candidate.Root, area); !ok {
211		return ErrTooSmall
212	}
213	*t = *candidate
214	return nil
215}
216
217func insertSplit(n *Node, target PaneID, axis SplitDir, after bool, newID PaneID, area domain.Rect) bool {
218	if n == nil {
219		return false
220	}
221	childAreas := nodeChildAreas(n, area)
222	for i, child := range n.Children {
223		if child.Kind == Leaf && child.Leaf == target {
224			newLeaf := NewLeaf(newID)
225			if n.Kind == Split && n.Dir == axis {
226				normalizeChildWeightsFromArea(n, area)
227				targetExtent := childAreas[i].Width
228				if axis == Vertical {
229					targetExtent = childAreas[i].Height
230				}
231				firstExtent := (targetExtent - 1) / 2
232				child.Weight = float64(firstExtent)
233				newLeaf.Weight = float64(targetExtent - 1 - firstExtent)
234				at := i
235				if after {
236					at++
237				}
238				n.Children = append(n.Children, nil)
239				copy(n.Children[at+1:], n.Children[at:])
240				n.Children[at] = newLeaf
241				return true
242			}
243			if n.Kind == Stack {
244				weight := n.Weight
245				stack := n.clone()
246				stack.Weight = 0
247				clearChildWeights(stack)
248				children := []*Node{stack, newLeaf}
249				if !after {
250					children = []*Node{newLeaf, stack}
251				}
252				*n = Node{Kind: Split, Dir: axis, Children: children, Weight: weight}
253				return true
254			}
255			weight := child.Weight
256			child.Weight = 0
257			children := []*Node{child, newLeaf}
258			if !after {
259				children = []*Node{newLeaf, child}
260			}
261			n.Children[i] = &Node{Kind: Split, Dir: axis, Children: children, Weight: weight}
262			return true
263		}
264		childArea := area
265		if i < len(childAreas) {
266			childArea = childAreas[i]
267		}
268		if insertSplit(child, target, axis, after, newID, childArea) {
269			return true
270		}
271	}
272	if n.Kind == Leaf && n.Leaf == target {
273		weight := n.Weight
274		existing := n.clone()
275		existing.Weight = 0
276		children := []*Node{existing, NewLeaf(newID)}
277		if !after {
278			children = []*Node{NewLeaf(newID), existing}
279		}
280		*n = Node{Kind: Split, Dir: axis, Children: children, Weight: weight}
281		return true
282	}
283	return false
284}
285
286func nodeChildAreas(n *Node, area domain.Rect) []domain.Rect {
287	if n.Kind == Split {
288		rects, ok := splitChildRects(n, area)
289		if ok {
290			return rects
291		}
292	}
293	areas := make([]domain.Rect, len(n.Children))
294	for i := range areas {
295		areas[i] = area
296	}
297	return areas
298}
299
300func normalizeChildWeightsFromArea(n *Node, area domain.Rect) {
301	rects, ok := splitChildRects(n, area)
302	if !ok || len(rects) == 0 {
303		return
304	}
305	for i, rect := range rects {
306		extent := rect.Width
307		if n.Dir == Vertical {
308			extent = rect.Height
309		}
310		n.Children[i].Weight = float64(extent)
311	}
312}
313
314func clearChildWeights(n *Node) {
315	for _, child := range n.Children {
316		child.Weight = 0
317	}
318}
319
320// StackNew puts newID in target's stack, creating one if target is not stacked.
321func (t *Tree) StackNew(target, newID PaneID, area domain.Rect) error {
322	candidate := t.clone()
323	if candidate == nil || !stackNew(candidate.Root, target, newID) {
324		return ErrNotFound
325	}
326	candidate.Focus = newID
327	if _, ok := Solve(candidate.Root, area); !ok {
328		return ErrTooSmall
329	}
330	*t = *candidate
331	return nil
332}
333
334func stackNew(n *Node, target, newID PaneID) bool {
335	if n == nil {
336		return false
337	}
338	if n.Kind == Stack {
339		for _, child := range n.Children {
340			if child.Kind == Leaf && child.Leaf == target {
341				n.Children = append(n.Children, NewLeaf(newID))
342				n.Expanded = newID
343				clearChildWeights(n)
344				return true
345			}
346		}
347	}
348	for i, child := range n.Children {
349		if child.Kind == Leaf && child.Leaf == target {
350			weight := child.Weight
351			child.Weight = 0
352			n.Children[i] = &Node{Kind: Stack, Children: []*Node{child, NewLeaf(newID)}, Expanded: newID, Weight: weight}
353			return true
354		}
355		if stackNew(child, target, newID) {
356			return true
357		}
358	}
359	if n.Kind == Leaf && n.Leaf == target {
360		weight := n.Weight
361		*n = Node{Kind: Stack, Children: []*Node{NewLeaf(target), NewLeaf(newID)}, Expanded: newID, Weight: weight}
362		return true
363	}
364	return false
365}
366
367// ToggleStack converts a stack back to a vertical split. For an unstacked leaf in
368// a two-child split, it converts that split to a stack.
369func (t *Tree) ToggleStack(target PaneID, area domain.Rect) error {
370	candidate := t.clone()
371	result := toggleStack(candidate.Root, target)
372	switch result {
373	case toggleNotFound:
374		return ErrNotFound
375	case toggleNotToggleable:
376		return ErrNotToggleable
377	}
378	if _, ok := Solve(candidate.Root, area); !ok {
379		return ErrTooSmall
380	}
381	*t = *candidate
382	return nil
383}
384
385type toggleResult int
386
387const (
388	toggleNotFound toggleResult = iota
389	toggleToggled
390	toggleNotToggleable
391)
392
393func toggleStack(n *Node, target PaneID) toggleResult {
394	if n == nil {
395		return toggleNotFound
396	}
397	for _, child := range n.Children {
398		if child.Kind == Leaf || !containsLeaf(child, target) {
399			continue
400		}
401		result := toggleStack(child, target)
402		if result != toggleNotFound {
403			return result
404		}
405	}
406	if !containsLeaf(n, target) {
407		return toggleNotFound
408	}
409	if n.Kind == Stack {
410		if stackDirectLeafIndex(n, target) < 0 {
411			return toggleNotToggleable
412		}
413		n.Kind = Split
414		n.Dir = Vertical
415		n.Expanded = ""
416		clearChildWeights(n)
417		return toggleToggled
418	}
419	if n.Kind == Split && len(n.Children) == 2 && splitDirectLeavesContain(n, target) {
420		n.Kind = Stack
421		n.Expanded = target
422		clearChildWeights(n)
423		return toggleToggled
424	}
425	return toggleNotToggleable
426}
427
428func stackDirectLeafIndex(n *Node, target PaneID) int {
429	for i, child := range n.Children {
430		if child.Kind == Leaf && child.Leaf == target {
431			return i
432		}
433	}
434	return -1
435}
436
437func splitDirectLeavesContain(n *Node, target PaneID) bool {
438	for _, child := range n.Children {
439		if child.Kind != Leaf {
440			return false
441		}
442	}
443	return n.Children[0].Leaf == target || n.Children[1].Leaf == target
444}
445
446// Close removes target and dissolves single-child containers.
447func (t *Tree) Close(target PaneID) error {
448	candidate := t.clone()
449	if candidate == nil || candidate.Root == nil || !containsLeaf(candidate.Root, target) {
450		return ErrNotFound
451	}
452	if candidate.Root.Kind == Leaf && candidate.Root.Leaf == target {
453		candidate.Root = nil
454		candidate.Focus = ""
455		*t = *candidate
456		return nil
457	}
458	refocus := closeRefocusCandidate(candidate.Root, target)
459	root, removed := closeNode(candidate.Root, target)
460	if !removed {
461		return ErrNotFound
462	}
463	candidate.Root = root
464	if candidate.Focus == target {
465		if refocus != "" && containsLeaf(candidate.Root, refocus) {
466			candidate.Focus = refocus
467		} else {
468			ids := leafIDs(candidate.Root)
469			if len(ids) > 0 {
470				candidate.Focus = ids[0]
471			} else {
472				candidate.Focus = ""
473			}
474		}
475		setExpanded(candidate.Root, candidate.Focus)
476	}
477	*t = *candidate
478	return nil
479}
480
481func closeNode(n *Node, target PaneID) (*Node, bool) {
482	if n.Kind == Leaf {
483		if n.Leaf == target {
484			return nil, true
485		}
486		return n, false
487	}
488	removed := false
489	children := n.Children[:0]
490	for _, child := range n.Children {
491		newChild, didRemove := closeNode(child, target)
492		removed = removed || didRemove
493		if newChild != nil {
494			children = append(children, newChild)
495		}
496	}
497	n.Children = children
498	if !removed {
499		return n, false
500	}
501	if n.Kind == Stack && n.Expanded == target && len(n.Children) > 0 {
502		n.Expanded = firstLeaf(n.Children[0])
503	}
504	if (n.Kind == Stack || n.Kind == Split) && len(n.Children) == 1 {
505		promoted := n.Children[0]
506		promoted.Weight = n.Weight
507		return promoted, true
508	}
509	return n, true
510}
511
512func closeRefocusCandidate(n *Node, target PaneID) PaneID {
513	if n == nil || n.Kind == Leaf {
514		return ""
515	}
516	for i, child := range n.Children {
517		if !containsLeaf(child, target) {
518			continue
519		}
520		if child.Kind == Leaf && child.Leaf == target {
521			if i > 0 {
522				return lastLeaf(n.Children[i-1])
523			}
524			if i+1 < len(n.Children) {
525				return firstLeaf(n.Children[i+1])
526			}
527			return ""
528		}
529		if id := closeRefocusCandidate(child, target); id != "" {
530			return id
531		}
532		if i > 0 {
533			return lastLeaf(n.Children[i-1])
534		}
535		if i+1 < len(n.Children) {
536			return firstLeaf(n.Children[i+1])
537		}
538		return ""
539	}
540	return ""
541}
542
543// ContainsLeaf reports whether n contains a leaf with id.
544func ContainsLeaf(n *Node, id PaneID) bool { return containsLeaf(n, id) }
545
546func containsLeaf(n *Node, id PaneID) bool {
547	if n == nil {
548		return false
549	}
550	if n.Kind == Leaf {
551		return n.Leaf == id
552	}
553	for _, child := range n.Children {
554		if containsLeaf(child, id) {
555			return true
556		}
557	}
558	return false
559}
560
561func firstLeaf(n *Node) PaneID {
562	if n.Kind == Leaf {
563		return n.Leaf
564	}
565	for _, child := range n.Children {
566		if id := firstLeaf(child); id != "" {
567			return id
568		}
569	}
570	return ""
571}
572
573func lastLeaf(n *Node) PaneID {
574	if n.Kind == Leaf {
575		return n.Leaf
576	}
577	for i := len(n.Children) - 1; i >= 0; i-- {
578		if id := lastLeaf(n.Children[i]); id != "" {
579			return id
580		}
581	}
582	return ""
583}
584
585func setExpanded(n *Node, id PaneID) bool {
586	if n == nil {
587		return false
588	}
589	if n.Kind == Stack && stackDirectLeafIndex(n, id) >= 0 {
590		n.Expanded = id
591		return true
592	}
593	for _, child := range n.Children {
594		if setExpanded(child, id) {
595			return true
596		}
597	}
598	return false
599}
600
601// LeafIDs returns the leaf ids under n in layout order.
602func LeafIDs(n *Node) []PaneID { return leafIDs(n) }
603
604func leafIDs(n *Node) []PaneID {
605	if n == nil {
606		return nil
607	}
608	if n.Kind == Leaf {
609		return []PaneID{n.Leaf}
610	}
611	var ids []PaneID
612	for _, child := range n.Children {
613		ids = append(ids, leafIDs(child)...)
614	}
615	return ids
616}
617
618// FocusSpan returns the solved focus rectangle of the focused pane.
619func (t *Tree) FocusSpan(area domain.Rect) (domain.Rect, error) {
620	if t == nil || t.Root == nil {
621		return domain.Rect{}, ErrNotFound
622	}
623	placements, ok := Solve(t.Root, area)
624	if !ok {
625		return domain.Rect{}, ErrTooSmall
626	}
627	for _, placement := range placements {
628		if placement.ID == t.Focus {
629			return focusRect(placement), nil
630		}
631	}
632	return domain.Rect{}, ErrNotFound
633}
634
635// EntryPane returns the pane on the facing edge that best preserves span on
636// the perpendicular axis. It does not mutate the tree.
637func (t *Tree) EntryPane(dir Direction, span, area domain.Rect) (PaneID, error) {
638	if t == nil || t.Root == nil {
639		return "", ErrNotFound
640	}
641	placements, ok := Solve(t.Root, area)
642	if !ok {
643		return "", ErrTooSmall
644	}
645
646	outside := span
647	switch dir {
648	case Left:
649		outside.X = area.X + area.Width
650	case Right:
651		outside.X = area.X - span.Width
652	case Up:
653		outside.Y = area.Y + area.Height
654	case Down:
655		outside.Y = area.Y - span.Height
656	default:
657		return "", ErrNoPane
658	}
659
660	return selectFacingCandidate(placements, outside, dir, facingGapFirst)
661}
662
663// FocusEnter focuses the best pane on the facing edge and expands it when it
664// is a collapsed stack member.
665func (t *Tree) FocusEnter(dir Direction, span, area domain.Rect) error {
666	id, err := t.EntryPane(dir, span, area)
667	if err != nil {
668		return err
669	}
670	t.Focus = id
671	setExpanded(t.Root, id)
672	return nil
673}
674
675// FocusDir moves focus in dir. Vertical movement within a stack walks stack
676// members before falling back to geometric focus.
677func (t *Tree) FocusDir(dir Direction, area domain.Rect) error {
678	if t.Root == nil {
679		return ErrNotFound
680	}
681	if (dir == Up || dir == Down) && focusStackLocal(t.Root, t.Focus, dir, &t.Focus) {
682		return nil
683	}
684	placements, ok := Solve(t.Root, area)
685	if !ok {
686		return ErrTooSmall
687	}
688	currentIdx := -1
689	for i, p := range placements {
690		if p.ID == t.Focus {
691			currentIdx = i
692			break
693		}
694	}
695	if currentIdx < 0 {
696		return ErrNotFound
697	}
698	current := focusRect(placements[currentIdx])
699	candidate, err := selectFacingCandidate(placements, current, dir, facingOverlapFirst)
700	if err != nil {
701		return err
702	}
703	t.Focus = candidate
704	setExpanded(t.Root, t.Focus)
705	return nil
706}
707
708type facingMetricPriority uint8
709
710const (
711	facingGapFirst facingMetricPriority = iota
712	facingOverlapFirst
713)
714
715type facingCandidate struct {
716	id      PaneID
717	gap     int
718	overlap int
719}
720
721func selectFacingCandidate(placements []Placement, from domain.Rect, dir Direction, priority facingMetricPriority) (PaneID, error) {
722	candidates := make([]facingCandidate, 0, len(placements))
723	for _, placement := range placements {
724		if facing, gap, perpendicularOverlap := sideGapOverlap(from, focusRect(placement), dir); facing {
725			candidates = append(candidates, facingCandidate{id: placement.ID, gap: gap, overlap: perpendicularOverlap})
726		}
727	}
728	if len(candidates) == 0 {
729		return "", ErrNoPane
730	}
731	sort.Slice(candidates, func(i, j int) bool {
732		if priority == facingGapFirst {
733			if candidates[i].gap != candidates[j].gap {
734				return candidates[i].gap < candidates[j].gap
735			}
736			if candidates[i].overlap != candidates[j].overlap {
737				return candidates[i].overlap > candidates[j].overlap
738			}
739		} else {
740			if candidates[i].overlap != candidates[j].overlap {
741				return candidates[i].overlap > candidates[j].overlap
742			}
743			if candidates[i].gap != candidates[j].gap {
744				return candidates[i].gap < candidates[j].gap
745			}
746		}
747		return candidates[i].id < candidates[j].id
748	})
749	return candidates[0].id, nil
750}
751
752func focusStackLocal(n *Node, id PaneID, dir Direction, focus *PaneID) bool {
753	if n.Kind == Stack {
754		idx := stackDirectLeafIndex(n, id)
755		if idx >= 0 {
756			if next := adjacentStackLeaf(n, idx, dir); next != "" {
757				n.Expanded = next
758				*focus = next
759				return true
760			}
761			return false
762		}
763	}
764	for _, child := range n.Children {
765		if focusStackLocal(child, id, dir, focus) {
766			return true
767		}
768	}
769	return false
770}
771
772func adjacentStackLeaf(n *Node, idx int, dir Direction) PaneID {
773	step := 0
774	switch dir {
775	case Up:
776		step = -1
777	case Down:
778		step = 1
779	default:
780		return ""
781	}
782	for i := idx + step; i >= 0 && i < len(n.Children); i += step {
783		if n.Children[i].Kind == Leaf {
784			return n.Children[i].Leaf
785		}
786	}
787	return ""
788}
789
790func focusRect(p Placement) domain.Rect {
791	if p.TitleBar.Height == 0 {
792		return p.Content
793	}
794	if p.Content.Height == 0 {
795		return p.TitleBar
796	}
797	y := p.TitleBar.Y
798	height := p.Content.Y + p.Content.Height - y
799	return domain.Rect{X: p.TitleBar.X, Y: y, Width: p.TitleBar.Width, Height: height}
800}
801
802func sideGapOverlap(a, b domain.Rect, dir Direction) (bool, int, int) {
803	ar, ab := a.X+a.Width, a.Y+a.Height
804	br, bb := b.X+b.Width, b.Y+b.Height
805	switch dir {
806	case Left:
807		if br > a.X {
808			return false, 0, 0
809		}
810		return true, a.X - br, overlap(a.Y, ab, b.Y, bb)
811	case Right:
812		if b.X < ar {
813			return false, 0, 0
814		}
815		return true, b.X - ar, overlap(a.Y, ab, b.Y, bb)
816	case Up:
817		if bb > a.Y {
818			return false, 0, 0
819		}
820		return true, a.Y - bb, overlap(a.X, ar, b.X, br)
821	case Down:
822		if b.Y < ab {
823			return false, 0, 0
824		}
825		return true, b.Y - ab, overlap(a.X, ar, b.X, br)
826	default:
827		return false, 0, 0
828	}
829}
830
831func overlap(a0, a1, b0, b1 int) int {
832	lo := max(a0, b0)
833	hi := min(a1, b1)
834	if hi <= lo {
835		return 0
836	}
837	return hi - lo
838}

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 12

Tags 11

↑↓ moveenter openesc close