main ece7682

consume_expel.go RawHistoryFolder

1package layout
2
3import (
4	"errors"
5
6	"github.com/bnema/vev/internal/domain"
7)
8
9var (
10	ErrUnsupportedColumnLayout = errors.New("unsupported column layout")
11	ErrInvalidDirection        = errors.New("invalid direction")
12)
13
14// ConsumeOrExpelPane moves target horizontally between canonical root columns.
15// A singleton column is consumed into its immediate neighbor; a member of a
16// vertical or stack column is expelled into a new singleton column.
17func (t *Tree) ConsumeOrExpelPane(target PaneID, dir Direction, area domain.Rect) (bool, error) {
18	if dir != Left && dir != Right {
19		return false, ErrInvalidDirection
20	}
21	if !validCanonicalColumnTree(t) {
22		return false, ErrUnsupportedColumnLayout
23	}
24
25	columns := canonicalColumns(t.Root)
26	columnIndex, _, found := findColumnMember(columns, target)
27	if !found {
28		return false, ErrNotFound
29	}
30
31	if columns[columnIndex].Kind == Leaf {
32		destinationIndex := columnIndex - 1
33		if dir == Right {
34			destinationIndex = columnIndex + 1
35		}
36		if destinationIndex < 0 || destinationIndex >= len(columns) {
37			return false, nil
38		}
39	}
40
41	candidate := t.clone()
42	if _, solved := Solve(candidate.Root, area); !solved {
43		return false, ErrTooSmall
44	}
45
46	columns = canonicalColumns(candidate.Root)
47	columnIndex, memberIndex, _ := findColumnMember(columns, target)
48	if candidate.Root.Kind == Split && candidate.Root.Dir == Horizontal {
49		normalizeChildWeightsFromArea(candidate.Root, area)
50	}
51
52	if columns[columnIndex].Kind == Leaf {
53		consumeSingletonColumn(candidate, columnIndex, dir, area)
54	} else {
55		expelColumnMember(candidate, columnIndex, memberIndex, dir)
56	}
57	candidate.Focus = target
58
59	if _, solved := Solve(candidate.Root, area); !solved {
60		return false, ErrTooSmall
61	}
62	*t = *candidate
63	return true, nil
64}
65
66func validCanonicalColumnTree(t *Tree) bool {
67	if t == nil || t.Root == nil {
68		return false
69	}
70
71	columns := canonicalColumns(t.Root)
72	if len(columns) == 0 {
73		return false
74	}
75	if t.Root.Kind == Split && t.Root.Dir == Horizontal && len(columns) < 2 {
76		return false
77	}
78
79	ids := make(map[PaneID]struct{})
80	for _, column := range columns {
81		if !validCanonicalColumn(column, ids) {
82			return false
83		}
84	}
85	if t.Focus == "" {
86		return false
87	}
88	_, focused := ids[t.Focus]
89	return focused
90}
91
92func canonicalColumns(root *Node) []*Node {
93	if root == nil {
94		return nil
95	}
96	if root.Kind == Split && root.Dir == Horizontal {
97		return root.Children
98	}
99	return []*Node{root}
100}
101
102func validCanonicalColumn(column *Node, ids map[PaneID]struct{}) bool {
103	if column == nil {
104		return false
105	}
106
107	switch column.Kind {
108	case Leaf:
109		if len(column.Children) != 0 {
110			return false
111		}
112		return addUniqueLeafID(ids, column.Leaf)
113	case Split:
114		if column.Dir != Vertical || len(column.Children) < 2 {
115			return false
116		}
117	case Stack:
118		if len(column.Children) < 2 {
119			return false
120		}
121	default:
122		return false
123	}
124
125	expanded := column.Kind != Stack
126	for _, member := range column.Children {
127		if member == nil || member.Kind != Leaf || len(member.Children) != 0 || !addUniqueLeafID(ids, member.Leaf) {
128			return false
129		}
130		if column.Kind == Stack && member.Leaf == column.Expanded {
131			expanded = true
132		}
133	}
134	return expanded
135}
136
137func addUniqueLeafID(ids map[PaneID]struct{}, id PaneID) bool {
138	if id == "" {
139		return false
140	}
141	if _, exists := ids[id]; exists {
142		return false
143	}
144	ids[id] = struct{}{}
145	return true
146}
147
148func findColumnMember(columns []*Node, target PaneID) (int, int, bool) {
149	for columnIndex, column := range columns {
150		if column.Kind == Leaf {
151			if column.Leaf == target {
152				return columnIndex, -1, true
153			}
154			continue
155		}
156		for memberIndex, member := range column.Children {
157			if member.Leaf == target {
158				return columnIndex, memberIndex, true
159			}
160		}
161	}
162	return 0, 0, false
163}
164
165// fairShareWeight returns the mean effective sibling weight so an inserted
166// node lands with an equal share while siblings keep their proportions.
167func fairShareWeight(siblings []*Node) float64 {
168	if len(siblings) == 0 {
169		return 0
170	}
171	total := 0.0
172	for _, sibling := range siblings {
173		total += effectiveWeight(sibling.Weight)
174	}
175	return total / float64(len(siblings))
176}
177
178func consumeSingletonColumn(t *Tree, sourceIndex int, dir Direction, area domain.Rect) {
179	root := t.Root
180	destinationIndex := sourceIndex - 1
181	if dir == Right {
182		destinationIndex = sourceIndex + 1
183	}
184
185	columnAreas, _ := splitChildRects(root, area)
186	moved := root.Children[sourceIndex]
187	moved.Weight = 0
188	destination := root.Children[destinationIndex]
189
190	switch destination.Kind {
191	case Leaf:
192		weight := destination.Weight
193		destination.Weight = 0
194		destination = &Node{
195			Kind:     Split,
196			Dir:      Vertical,
197			Children: []*Node{destination, moved},
198			Weight:   weight,
199		}
200	case Split:
201		normalizeChildWeightsFromArea(destination, columnAreas[destinationIndex])
202		moved.Weight = fairShareWeight(destination.Children)
203		destination.Children = append(destination.Children, moved)
204	case Stack:
205		destination.Children = append(destination.Children, moved)
206		destination.Expanded = moved.Leaf
207		clearChildWeights(destination)
208	}
209	root.Children[destinationIndex] = destination
210	root.Children = removeNodeAt(root.Children, sourceIndex)
211
212	if len(root.Children) == 1 {
213		promoted := root.Children[0]
214		promoted.Weight = root.Weight
215		t.Root = promoted
216	}
217}
218
219func expelColumnMember(t *Tree, columnIndex, memberIndex int, dir Direction) {
220	root := t.Root
221	rootIsHorizontal := root.Kind == Split && root.Dir == Horizontal
222	columns := canonicalColumns(root)
223	source := columns[columnIndex]
224	wrapperWeight := source.Weight
225	if !rootIsHorizontal {
226		wrapperWeight = root.Weight
227	}
228
229	moved := source.Children[memberIndex]
230	moved.Weight = 0
231	source.Children = removeNodeAt(source.Children, memberIndex)
232	if source.Kind == Stack {
233		if source.Expanded == moved.Leaf {
234			source.Expanded = source.Children[0].Leaf
235		}
236		clearChildWeights(source)
237	}
238
239	remaining := source
240	if len(source.Children) == 1 {
241		remaining = source.Children[0]
242		remaining.Weight = source.Weight
243	}
244
245	if !rootIsHorizontal {
246		remaining.Weight = 0
247		children := []*Node{remaining, moved}
248		if dir == Left {
249			children[0], children[1] = children[1], children[0]
250		}
251		t.Root = &Node{Kind: Split, Dir: Horizontal, Children: children, Weight: wrapperWeight}
252		return
253	}
254
255	root.Children[columnIndex] = remaining
256	moved.Weight = fairShareWeight(root.Children)
257	insertionIndex := columnIndex
258	if dir == Right {
259		insertionIndex++
260	}
261	root.Children = insertNodeAt(root.Children, insertionIndex, moved)
262}
263
264func removeNodeAt(nodes []*Node, index int) []*Node {
265	copy(nodes[index:], nodes[index+1:])
266	nodes[len(nodes)-1] = nil
267	return nodes[:len(nodes)-1]
268}
269
270func insertNodeAt(nodes []*Node, index int, node *Node) []*Node {
271	nodes = append(nodes, nil)
272	copy(nodes[index+1:], nodes[index:])
273	nodes[index] = node
274	return nodes
275}

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