move_tab.go RawHistoryFolder

1package daemon
2
3import (
4	"context"
5
6	"github.com/bnema/vev/internal/domain"
7	"github.com/bnema/vev/internal/domain/layout"
8)
9
10// moveTabTopology is the private whole-Tab topology Implementation consumed by
11// the shared moveTransaction Module.
12type moveTabTopology struct {
13	req         moveTabRequest
14	admission   moveTabAdmission
15	fencedPanes []*pane
16}
17
18type moveTabAdmission struct {
19	tab                *tab
20	sourceIndex        int
21	layoutGeneration   uint64
22	floatingState      floatingState
23	floatingPane       *pane
24	floatingGeneration uint64
25	panes              []*pane
26	destinationActive  *tab
27	destinationSize    domain.Size
28	finalSource        bool
29}
30
31type moveTabCommit struct {
32	req         moveTabRequest
33	source      *session
34	destination *session
35	admission   moveTabAdmission
36}
37
38func (d *Daemon) moveTab(req moveTabRequest) error {
39	return d.executeMove(&moveTabTopology{req: req})
40}
41
42func (p *moveTabTopology) transactionRequest() moveTransactionRequest {
43	req := p.req
44	return moveTransactionRequest{
45		follow:               req.Follow,
46		beforeFollow:         req.BeforeFollow,
47		afterFollow:          req.AfterFollow,
48		operation:            "tab",
49		attachment:           req.Attachment,
50		attachmentCapability: req.AttachmentCapability,
51		source:               req.Source,
52		destination:          req.Destination,
53		logAttrs: []any{
54			"source_session_id", req.Source.ID,
55			"source_tab_id", req.SourceTabID,
56			"destination_session_id", req.Destination.ID,
57		},
58	}
59}
60
61func (p *moveTabTopology) validRequest() bool {
62	return p != nil && p.req.SourceTabID != "" && p.req.Source.ID != p.req.Destination.ID
63}
64
65// admitLocked runs with daemon and ordered Session locks held. It snapshots
66// whole-Tab policy without publishing topology or Pane ownership.
67func (p *moveTabTopology) admitLocked(_ *Daemon, source, destination *session) error {
68	if source == destination {
69		return errMovePaneInvalid
70	}
71	moved := findMoveTabLocked(source, p.req.SourceTabID)
72	if moved == nil || len(destination.tabs) == 0 {
73		return errMoveStaleTarget
74	}
75	destinationActive := destination.tabs[0]
76	destinationActive.mu.Lock()
77	destinationSize := destinationActive.size
78	destinationActive.mu.Unlock()
79
80	moved.mu.Lock()
81	defer moved.mu.Unlock()
82	if !moved.floatingTransferableLocked() {
83		return errMoveFloatingWarming
84	}
85	if moved.tree == nil || moved.tree.Root == nil {
86		return errMovePaneInvalid
87	}
88	if _, ok := layout.Solve(moved.tree.Root, domain.Rect{Width: destinationSize.Cols, Height: destinationSize.Rows}); !ok {
89		return errMoveTooSmall
90	}
91	panes := make([]*pane, 0, len(moved.panes)+1)
92	for _, pane := range moved.panes {
93		panes = append(panes, pane)
94	}
95	if moved.floating.pane != nil && (moved.floating.state == floatingHidden || moved.floating.state == floatingVisible) {
96		panes = append(panes, moved.floating.pane)
97	}
98	p.admission = moveTabAdmission{
99		tab: moved, sourceIndex: indexMoveTabLocked(source, moved),
100		layoutGeneration: moved.layoutGeneration, floatingState: moved.floating.state,
101		floatingPane: moved.floating.pane, floatingGeneration: moved.floating.generation,
102		panes: panes, destinationActive: destinationActive, destinationSize: destinationSize,
103		finalSource: len(source.tabs) == 1,
104	}
105	return nil
106}
107
108func (p *moveTabTopology) afterAdmission(d *Daemon) {
109	if d.afterMoveTabSourceSnapshot != nil {
110		d.afterMoveTabSourceSnapshot()
111	}
112}
113
114func (p *moveTabTopology) willRetireSource() bool {
115	return p != nil && p.admission.finalSource
116}
117
118func (p *moveTabTopology) resizeFences(source, destination *session) *moveResizeFences {
119	fences := newMoveTabResizeFences(source, destination, p.admission.tab)
120	p.fencedPanes = fences.panes
121	return fences
122}
123
124func (p *moveTabTopology) publishLocked(d *Daemon, source, destination *session) (moveTopologyPublication, error) {
125	commit := moveTabCommit{req: p.req, source: source, destination: destination, admission: p.admission}
126	return commit.publishTopologyLocked(d, p.fencedPanes)
127}
128
129// publishTopologyLocked performs every whole-Tab validation before changing
130// membership, Tab context, or Pane owners. The shared transaction owns common
131// Session retirement and persistence capture after this method succeeds.
132func (c *moveTabCommit) publishTopologyLocked(d *Daemon, fencedPanes []*pane) (moveTopologyPublication, error) {
133	moved := c.admission.tab
134	if indexMoveTabLocked(c.source, moved) != c.admission.sourceIndex || findMoveTabLocked(c.destination, c.req.SourceTabID) != nil ||
135		(len(c.source.tabs) == 1) != c.admission.finalSource {
136		return moveTopologyPublication{}, errMoveStaleTarget
137	}
138	if len(c.destination.tabs) == 0 || c.destination.tabs[0] != c.admission.destinationActive {
139		return moveTopologyPublication{}, errMoveStaleTarget
140	}
141	c.admission.destinationActive.mu.Lock()
142	destinationSizeCurrent := c.admission.destinationActive.size == c.admission.destinationSize
143	c.admission.destinationActive.mu.Unlock()
144	if !destinationSizeCurrent {
145		return moveTopologyPublication{}, errMoveStaleTarget
146	}
147
148	moved.mu.Lock()
149	defer moved.mu.Unlock()
150	if moved.layoutGeneration != c.admission.layoutGeneration || !moved.floatingTransferableLocked() ||
151		moved.floating.state != c.admission.floatingState || moved.floating.pane != c.admission.floatingPane ||
152		moved.floating.generation != c.admission.floatingGeneration || !sameMoveTabPanesLocked(moved, c.admission.panes, fencedPanes) {
153		return moveTopologyPublication{}, errMoveStaleTarget
154	}
155	for _, pane := range fencedPanes {
156		pane.mu.Lock()
157		defer pane.mu.Unlock()
158		owner := pane.ownerSnapshot()
159		if owner == nil || owner.session != c.source || owner.tab != moved {
160			return moveTopologyPublication{}, errMoveStaleTarget
161		}
162	}
163
164	// Everything above is fallible. Membership, context, and Pane owner writes
165	// below form one in-memory publication and cannot become a Move rejection.
166	idx := c.admission.sourceIndex
167	if len(c.source.tabs) > 1 {
168		c.source.prepareAttachmentViewsForRemovedTabLocked(moved, idx)
169	}
170	c.source.tabs = append(c.source.tabs[:idx], c.source.tabs[idx+1:]...)
171	c.destination.tabs = append(c.destination.tabs, moved)
172	moved.size = c.admission.destinationSize
173	moved.bumpLayoutGenerationLocked()
174	oldTabCancel := moved.cancel
175	parent := c.destination.ctx
176	if parent == nil {
177		parent = d.serveCtx
178	}
179	moved.ctx, moved.cancel = context.WithCancel(parent)
180
181	type ownerChange struct {
182		pane     *pane
183		oldOwner paneEffectLease
184		newOwner paneEffectLease
185	}
186	ownerChanges := make([]ownerChange, 0, len(fencedPanes))
187	for _, pane := range fencedPanes {
188		floatingGeneration := uint64(0)
189		if pane == moved.floating.pane {
190			floatingGeneration = moved.floating.generation
191		}
192		oldOwner, newOwner := pane.publishOwnerLocked(c.destination, moved, floatingGeneration)
193		ownerChanges = append(ownerChanges, ownerChange{pane: pane, oldOwner: oldOwner, newOwner: newOwner})
194	}
195	if d.beforeMoveTabCommit != nil {
196		d.beforeMoveTabCommit()
197	}
198	var syncCleanup syncTimerCleanup
199	for _, change := range ownerChanges {
200		syncCleanup.append(d.migratePaneSyncOwnerLocked(change.pane, change.oldOwner, change.newOwner))
201	}
202
203	return moveTopologyPublication{
204		sourceTab:        moved,
205		destinationTab:   moved,
206		movedPane:        firstMovePane(c.admission.panes),
207		movedPanes:       c.admission.panes,
208		syncCleanup:      syncCleanup,
209		oldTabCancel:     oldTabCancel,
210		sourceTabRemoved: true,
211		sourceEmpty:      len(c.source.tabs) == 0,
212	}, nil
213}
214
215func sameMoveTabPanesLocked(tb *tab, admitted, fenced []*pane) bool {
216	current := make(map[*pane]struct{}, len(tb.panes)+1)
217	for _, pane := range tb.panes {
218		current[pane] = struct{}{}
219	}
220	if tb.floating.pane != nil && (tb.floating.state == floatingHidden || tb.floating.state == floatingVisible) {
221		current[tb.floating.pane] = struct{}{}
222	}
223	if len(current) != len(admitted) || len(current) != len(fenced) {
224		return false
225	}
226	for _, pane := range admitted {
227		if _, ok := current[pane]; !ok {
228			return false
229		}
230	}
231	for _, pane := range fenced {
232		if _, ok := current[pane]; !ok {
233			return false
234		}
235	}
236	return true
237}

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