v0.2.0 80c61e3

move_tab.go RawHistoryFolder

1package daemon
2
3import (
4	"context"
5
6	"github.com/bnema/vev/internal/domain"
7	"github.com/bnema/vev/internal/usecase/layout"
8)
9
10type moveTabAdmission struct {
11	tab                *tab
12	sourceIndex        int
13	sourceAttachments  []*attachedClient
14	sourceTransports   map[*attachedClient]transportSnapshot
15	layoutGeneration   uint64
16	floatingState      floatingState
17	floatingPane       *pane
18	floatingGeneration uint64
19	panes              []*pane
20	destinationActive  *tab
21	destinationSize    domain.Size
22	finalSource        bool
23}
24
25type moveTabCommit struct {
26	req                 moveTabRequest
27	source              *session
28	destination         *session
29	admission           moveTabAdmission
30	sourceEffectsFrozen bool
31	frozenEffects       frozenAttachmentEffectGates
32	syncCleanup         syncTimerCleanup
33	oldTabCancel        context.CancelFunc
34	sourceEmpty         bool
35	retiredParked       []parkedAttachmentRetirement
36	retiredAttachments  []detachedAttachmentSnapshot
37	sourceMetadata      domain.CatalogueMetadataUpdate
38	sourceMetadataValid bool
39	destMetadata        domain.CatalogueMetadataUpdate
40	destMetadataValid   bool
41	err                 error
42}
43
44func (d *Daemon) moveTab(req moveTabRequest) (result error) {
45	defer func() { result = normalizeMoveRejection(result) }()
46	if d == nil || req.Source.ID == "" || req.Destination.ID == "" || req.SourceTabID == "" || req.Source.ID == req.Destination.ID {
47		return errMovePaneInvalid
48	}
49	d.mu.Lock()
50	source := moveSessionForLocatorLocked(d, req.Source)
51	destination := moveSessionForLocatorLocked(d, req.Destination)
52	d.mu.Unlock()
53	if source == nil || destination == nil {
54		return errMoveStaleTarget
55	}
56	if source == destination {
57		return errMovePaneInvalid
58	}
59	// Dispatch admission must precede lifecycle reservation. A final close holds
60	// dispatchMu while it waits for teardown ownership; reserving first would let
61	// that close wait on a move which is itself blocked on dispatchMu.
62	if d.beforeMoveDispatch != nil {
63		d.beforeMoveDispatch()
64	}
65	unlockDispatch := lockMoveDispatch(source, destination)
66	dispatchHeld := true
67	defer func() {
68		if dispatchHeld {
69			unlockDispatch()
70		}
71	}()
72
73	reservation, err := d.reserveMoveLifecycles(source, destination)
74	if err != nil {
75		return errMoveStaleTarget
76	}
77	reservationHeld := true
78	defer func() {
79		if reservationHeld {
80			reservation.Release()
81		}
82	}()
83	if d.afterMoveLifecycleReserved != nil {
84		d.afterMoveLifecycleReserved()
85	}
86
87	admission, err := d.snapshotMoveTabAdmission(req, source, destination)
88	if err != nil {
89		return err
90	}
91	if d.afterMoveTabSourceSnapshot != nil {
92		d.afterMoveTabSourceSnapshot()
93	}
94	commit := moveTabCommit{req: req, source: source, destination: destination, admission: *admission}
95	var frozen frozenAttachmentEffectGates
96	if admission.finalSource && len(admission.sourceAttachments) != 0 {
97		interrupts := make([]attachmentTransportInterrupt, 0, len(admission.sourceAttachments))
98		for _, ac := range admission.sourceAttachments {
99			if transport := admission.sourceTransports[ac]; transport.transport != nil {
100				interrupts = append(interrupts, attachmentTransportInterrupt{ac: ac, transport: transport})
101			}
102		}
103		frozen = freezeAttachmentEffectGatesWith(attachmentEffectFreezeOptions{interrupts: interrupts, nonblocking: true, afterFrozen: func(ac *attachedClient) {
104			if d.afterAttachmentEffectGateFrozen != nil {
105				d.afterAttachmentEffectGateFrozen("move-tab", ac)
106			}
107		}}, admission.sourceAttachments...)
108		if !frozen.acquired || !frozen.drained {
109			frozen.unfreeze()
110			return errMovePaneInvalid
111		}
112		commit.sourceEffectsFrozen = true
113	}
114	commit.frozenEffects = frozen
115	effectsFrozen := commit.sourceEffectsFrozen
116	defer func() {
117		if effectsFrozen {
118			frozen.unfreeze()
119		}
120	}()
121
122	fences := newMoveTabResizeFences(source, destination, admission.tab)
123	if !fences.acquire(func() bool { return commit.publishLocked(d, fences.panes) }) {
124		if commit.err != nil {
125			return commit.err
126		}
127		return errMoveStaleTarget
128	}
129	fences.Release()
130	postcommit := movePanePostcommitPlan{
131		source:                   source,
132		destination:              destination,
133		sourceTab:                admission.tab,
134		destinationTab:           admission.tab,
135		movedPane:                firstMovePane(admission.panes),
136		movedPanes:               admission.panes,
137		sourceAttachments:        admission.sourceAttachments,
138		syncCleanup:              commit.syncCleanup,
139		frozenEffects:            frozen,
140		effectsFrozen:            effectsFrozen,
141		unlockDispatch:           unlockDispatch,
142		reservation:              reservation,
143		oldTabCancel:             commit.oldTabCancel,
144		sourceTabRemoved:         true,
145		sourceEmpty:              commit.sourceEmpty,
146		retiredParked:            commit.retiredParked,
147		retiredAttachments:       commit.retiredAttachments,
148		sourceMetadata:           commit.sourceMetadata,
149		sourceMetadataValid:      commit.sourceMetadataValid,
150		destinationMetadata:      commit.destMetadata,
151		destinationMetadataValid: commit.destMetadataValid,
152	}
153	effectsFrozen = false
154	dispatchHeld = false
155	reservationHeld = false
156	postcommit.execute(d)
157	return nil
158}
159
160func (d *Daemon) snapshotMoveTabAdmission(req moveTabRequest, source, destination *session) (*moveTabAdmission, error) {
161	d.mu.Lock()
162	defer d.mu.Unlock()
163	if d.closing || d.sessions[source.id] != source || d.sessions[destination.id] != destination {
164		return nil, errMoveStaleTarget
165	}
166	unlockSessions := lockAttachmentSessions(source, destination)
167	defer unlockSessions()
168	if !moveSessionLocatorCurrentLocked(source, req.Source) || !moveSessionLocatorCurrentLocked(destination, req.Destination) {
169		return nil, errMoveStaleTarget
170	}
171	if req.Attachment != nil && !attachmentRegisteredLocked(source, req.Attachment) {
172		return nil, errMoveStaleTarget
173	}
174	if req.AttachmentToken.ac != nil && (req.AttachmentToken.ac != req.Attachment || !moveAttachmentTokenCurrentLocked(req.AttachmentToken, source)) {
175		return nil, errMoveStaleTarget
176	}
177	moved := findMoveTabLocked(source, req.SourceTabID)
178	if moved == nil || len(destination.tabs) == 0 {
179		return nil, errMoveStaleTarget
180	}
181	destinationActive := destination.tabs[0]
182	destinationActive.mu.Lock()
183	destinationSize := destinationActive.size
184	destinationActive.mu.Unlock()
185	moved.mu.Lock()
186	defer moved.mu.Unlock()
187	if !moved.floatingTransferableLocked() {
188		return nil, errMoveFloatingWarming
189	}
190	if moved.tree == nil || moved.tree.Root == nil {
191		return nil, errMovePaneInvalid
192	}
193	if _, ok := layout.Solve(moved.tree.Root, domain.Rect{Width: destinationSize.Cols, Height: destinationSize.Rows}); !ok {
194		return nil, errMoveTooSmall
195	}
196	panes := make([]*pane, 0, len(moved.panes)+1)
197	for _, p := range moved.panes {
198		panes = append(panes, p)
199	}
200	if moved.floating.pane != nil && (moved.floating.state == floatingHidden || moved.floating.state == floatingVisible) {
201		panes = append(panes, moved.floating.pane)
202	}
203	idx := indexMoveTabLocked(source, moved)
204	attachments := source.snapshotAttachmentsLocked()
205	transports := make(map[*attachedClient]transportSnapshot, len(attachments))
206	for _, ac := range attachments {
207		transports[ac] = ac.transportSnapshot()
208	}
209	return &moveTabAdmission{
210		tab: moved, sourceIndex: idx, sourceAttachments: attachments, sourceTransports: transports,
211		layoutGeneration: moved.layoutGeneration, floatingState: moved.floating.state,
212		floatingPane: moved.floating.pane, floatingGeneration: moved.floating.generation,
213		panes: panes, destinationActive: destinationActive, destinationSize: destinationSize,
214		finalSource: len(source.tabs) == 1,
215	}, nil
216}
217
218func (c *moveTabCommit) publishLocked(d *Daemon, fencedPanes []*pane) bool {
219	d.mu.Lock()
220	if d.closing || d.sessions[c.source.id] != c.source || d.sessions[c.destination.id] != c.destination {
221		d.mu.Unlock()
222		return false
223	}
224	d.notices.routingMu.Lock()
225	unlockSessions := lockAttachmentSessions(c.source, c.destination)
226	defer unlockSessions()
227	defer d.notices.routingMu.Unlock()
228	defer d.mu.Unlock()
229	if !moveSessionLocatorCurrentLocked(c.source, c.req.Source) || !moveSessionLocatorCurrentLocked(c.destination, c.req.Destination) {
230		return false
231	}
232	if c.req.Attachment != nil && !attachmentRegisteredLocked(c.source, c.req.Attachment) {
233		return false
234	}
235	if c.req.AttachmentToken.ac != nil && (c.req.AttachmentToken.ac != c.req.Attachment || !moveAttachmentTokenCurrentLocked(c.req.AttachmentToken, c.source)) {
236		return false
237	}
238	moved := c.admission.tab
239	if indexMoveTabLocked(c.source, moved) != c.admission.sourceIndex || findMoveTabLocked(c.destination, c.req.SourceTabID) != nil ||
240		len(c.source.tabs) == 1 != c.admission.finalSource {
241		return false
242	}
243	if c.admission.finalSource && !sameMoveAttachmentsLocked(c.source, c.admission.sourceAttachments) {
244		return false
245	}
246	if len(c.destination.tabs) == 0 || c.destination.tabs[0] != c.admission.destinationActive {
247		return false
248	}
249	c.admission.destinationActive.mu.Lock()
250	destinationSizeCurrent := c.admission.destinationActive.size == c.admission.destinationSize
251	c.admission.destinationActive.mu.Unlock()
252	if !destinationSizeCurrent {
253		return false
254	}
255	moved.mu.Lock()
256	defer moved.mu.Unlock()
257	if moved.layoutGeneration != c.admission.layoutGeneration || !moved.floatingTransferableLocked() ||
258		moved.floating.state != c.admission.floatingState || moved.floating.pane != c.admission.floatingPane ||
259		moved.floating.generation != c.admission.floatingGeneration || !sameMoveTabPanesLocked(moved, c.admission.panes, fencedPanes) {
260		return false
261	}
262	for _, p := range fencedPanes {
263		p.mu.Lock()
264		defer p.mu.Unlock()
265		owner := p.ownerSnapshot()
266		if owner == nil || owner.session != c.source || owner.tab != moved {
267			return false
268		}
269	}
270
271	idx := c.admission.sourceIndex
272	if len(c.source.tabs) > 1 {
273		c.source.prepareAttachmentViewsForRemovedTabLocked(moved, idx)
274	}
275	c.source.tabs = append(c.source.tabs[:idx], c.source.tabs[idx+1:]...)
276	c.destination.tabs = append(c.destination.tabs, moved)
277	moved.size = c.admission.destinationSize
278	moved.bumpLayoutGenerationLocked()
279	c.oldTabCancel = moved.cancel
280	parent := c.destination.ctx
281	if parent == nil {
282		parent = d.serveCtx
283	}
284	moved.ctx, moved.cancel = context.WithCancel(parent)
285	type ownerChange struct {
286		pane     *pane
287		oldOwner paneEffectLease
288		newOwner paneEffectLease
289	}
290	ownerChanges := make([]ownerChange, 0, len(fencedPanes))
291	for _, p := range fencedPanes {
292		floatingGeneration := uint64(0)
293		if p == moved.floating.pane {
294			floatingGeneration = moved.floating.generation
295		}
296		oldOwner, newOwner := p.publishOwnerLocked(c.destination, moved, floatingGeneration)
297		ownerChanges = append(ownerChanges, ownerChange{pane: p, oldOwner: oldOwner, newOwner: newOwner})
298	}
299	if d.beforeMoveTabCommit != nil {
300		d.beforeMoveTabCommit()
301	}
302	for _, change := range ownerChanges {
303		c.syncCleanup.append(d.migratePaneSyncOwnerLocked(change.pane, change.oldOwner, change.newOwner))
304	}
305	if len(c.source.tabs) == 0 {
306		d.unregisterSessionLocked(c.source)
307		c.retiredAttachments = detachMoveAttachmentsLocked(c.source, c.admission.sourceTransports)
308		c.source.tabs = nil
309		d.purgeParkingForSessionLocked(c.source)
310		c.retiredParked = d.purgeParkedForSessionLocked(c.source)
311	}
312	c.sourceEmpty = len(c.source.tabs) == 0
313	if !c.source.ephemeral {
314		c.sourceMetadata = c.source.persistRecordLocked(max(d.nowUnixNano(), c.source.createdAt, int64(1))).MetadataUpdate()
315		c.sourceMetadataValid = true
316	}
317	if !c.destination.ephemeral {
318		c.destMetadata = c.destination.persistRecordLocked(max(d.nowUnixNano(), c.destination.createdAt, int64(1))).MetadataUpdate()
319		c.destMetadataValid = true
320	}
321	return true
322}
323
324func sameMoveTabPanesLocked(tb *tab, admitted, fenced []*pane) bool {
325	current := make(map[*pane]struct{}, len(tb.panes)+1)
326	for _, p := range tb.panes {
327		current[p] = struct{}{}
328	}
329	if tb.floating.pane != nil && (tb.floating.state == floatingHidden || tb.floating.state == floatingVisible) {
330		current[tb.floating.pane] = struct{}{}
331	}
332	if len(current) != len(admitted) || len(current) != len(fenced) {
333		return false
334	}
335	for _, p := range admitted {
336		if _, ok := current[p]; !ok {
337			return false
338		}
339	}
340	for _, p := range fenced {
341		if _, ok := current[p]; !ok {
342			return false
343		}
344	}
345	return true
346}

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