move_resize_fences.go RawHistoryFolder

1package daemon
2
3import (
4	"cmp"
5	"slices"
6)
7
8// moveResizeFences excludes concurrent layout publishers while a move
9// revalidates and publishes ownership. It is not an architecture lock. Fences
10// are always acquired session -> tab -> pane by immutable stable identity and
11// released in exact reverse order.
12type moveResizeFences struct {
13	sessions []*session
14	tabs     []*tab
15	panes    []*pane
16	held     bool
17}
18
19// newMovePaneResizeFences covers both sessions, both affected layouts, and
20// only the pane whose owner will change. Stable-identity deduplication also
21// handles moves between tabs in the same session.
22func newMovePaneResizeFences(source, destination *session, sourceTab, destinationTab *tab, moved *pane) *moveResizeFences {
23	return newMoveResizeFences(
24		[]*session{source, destination},
25		[]*tab{sourceTab, destinationTab},
26		[]*pane{moved},
27	)
28}
29
30// newMoveTabResizeFences snapshots every process-bearing pane transferred with
31// a tab. The acquire callback must revalidate this exact tiled membership and
32// installed floating slot after all fences are held, because this snapshot is
33// intentionally taken before waiting on any resize transaction.
34func newMoveTabResizeFences(source, destination *session, moved *tab) *moveResizeFences {
35	if moved == nil {
36		return newMoveResizeFences([]*session{source, destination}, nil, nil)
37	}
38	moved.mu.Lock()
39	panes := make([]*pane, 0, len(moved.panes)+1)
40	for _, p := range moved.panes {
41		panes = append(panes, p)
42	}
43	if moved.floating.pane != nil && (moved.floating.state == floatingHidden || moved.floating.state == floatingVisible) {
44		panes = append(panes, moved.floating.pane)
45	}
46	moved.mu.Unlock()
47	return newMoveResizeFences([]*session{source, destination}, []*tab{moved}, panes)
48}
49
50func newMoveResizeFences(sessions []*session, tabs []*tab, panes []*pane) *moveResizeFences {
51	byID := make(map[string]*session, len(sessions))
52	for _, sess := range sessions {
53		if sess == nil {
54			continue
55		}
56		if _, exists := byID[string(sess.id)]; !exists {
57			byID[string(sess.id)] = sess
58		}
59	}
60	ordered := make([]*session, 0, len(byID))
61	for _, sess := range byID {
62		ordered = append(ordered, sess)
63	}
64	slices.SortFunc(ordered, func(left, right *session) int {
65		return cmp.Compare(left.id, right.id)
66	})
67
68	tabsByID := make(map[string]*tab, len(tabs))
69	for _, tb := range tabs {
70		if tb == nil {
71			continue
72		}
73		if _, exists := tabsByID[tb.stableID]; !exists {
74			tabsByID[tb.stableID] = tb
75		}
76	}
77	orderedTabs := make([]*tab, 0, len(tabsByID))
78	for _, tb := range tabsByID {
79		orderedTabs = append(orderedTabs, tb)
80	}
81	slices.SortFunc(orderedTabs, func(left, right *tab) int {
82		return cmp.Compare(left.stableID, right.stableID)
83	})
84
85	panesByID := make(map[string]*pane, len(panes))
86	for _, p := range panes {
87		if p == nil {
88			continue
89		}
90		if _, exists := panesByID[p.stableID]; !exists {
91			panesByID[p.stableID] = p
92		}
93	}
94	orderedPanes := make([]*pane, 0, len(panesByID))
95	for _, p := range panesByID {
96		orderedPanes = append(orderedPanes, p)
97	}
98	slices.SortFunc(orderedPanes, func(left, right *pane) int {
99		return cmp.Compare(left.stableID, right.stableID)
100	})
101	return &moveResizeFences{sessions: ordered, tabs: orderedTabs, panes: orderedPanes}
102}
103
104// acquire waits for every resize fence before invoking revalidateAndPublish.
105// Callers must enter with no daemon, session, tab, or pane architecture lock
106// held. The callback is the only architecture-lock window: under the canonical
107// architecture lock order it must revalidate the sessions, tabs, tiled pane
108// memberships, and installed floating slot captured before this wait; publish
109// every changed pane owner; and bump each affected tab's layoutGeneration. A
110// false result (or panic) releases every acquired fence in reverse order. A true
111// result retains all fences so no resize can observe half-published ownership;
112// the caller must Release after architecture locks are dropped and before PTY
113// I/O, applyTabLayout, or applySessionLayout.
114func (f *moveResizeFences) acquire(revalidateAndPublish func() bool) (accepted bool) {
115	if f == nil || f.held {
116		return false
117	}
118	for _, sess := range f.sessions {
119		sess.layoutApplyMu.Lock()
120	}
121	for _, tb := range f.tabs {
122		tb.layoutApplyMu.Lock()
123	}
124	for _, p := range f.panes {
125		p.resizeMu.Lock()
126	}
127	f.held = true
128	defer func() {
129		if !accepted {
130			f.Release()
131		}
132	}()
133	if revalidateAndPublish == nil {
134		return false
135	}
136	accepted = revalidateAndPublish()
137	return accepted
138}
139
140func (d *Daemon) observeBeforeResizeOwnerPostEffect(effect resizeOwnerPostEffect) {
141	if d != nil && d.beforeResizeOwnerPostEffect != nil {
142		d.beforeResizeOwnerPostEffect(effect)
143	}
144}
145
146// acquireResizeOwnerPostEffectFences acquires and validates the complete
147// canonical session/tab/pane fence set. It performs no callbacks or external
148// operations and returns the fences held on success.
149func acquireResizeOwnerPostEffectFences(members []resizeMember) *moveResizeFences {
150	if len(members) == 0 {
151		return nil
152	}
153	sessions := make([]*session, 0, len(members))
154	tabs := make([]*tab, 0, len(members))
155	panes := make([]*pane, 0, len(members))
156	for i := range members {
157		member := &members[i]
158		sessions = append(sessions, member.session)
159		tabs = append(tabs, member.tab)
160		panes = append(panes, member.pane)
161	}
162	fences := newMoveResizeFences(sessions, tabs, panes)
163	if !fences.acquire(func() bool { return resizeMembersOwnerCurrent(members) }) {
164		return nil
165	}
166	return fences
167}
168
169// publishResizeOwnerPostEffect linearizes an in-memory resize effect with pane
170// ownership publication. The deterministic seam runs before fence acquisition,
171// then the complete canonical session/tab/pane fence set is acquired and every
172// immutable owner lease is revalidated. The callback must publish in-memory
173// state only; timer, PTY, transport, repository, and renderer work belongs
174// after this method returns.
175func (d *Daemon) publishResizeOwnerPostEffect(members []resizeMember, effect resizeOwnerPostEffect, publish func()) bool {
176	if publish == nil {
177		return false
178	}
179	d.observeBeforeResizeOwnerPostEffect(effect)
180	if len(members) == 0 {
181		publish()
182		return true
183	}
184	fences := acquireResizeOwnerPostEffectFences(members)
185	if fences == nil {
186		return false
187	}
188	defer fences.Release()
189	publish()
190	return true
191}
192
193func (f *moveResizeFences) Release() {
194	if f == nil || !f.held {
195		return
196	}
197	for i := len(f.panes) - 1; i >= 0; i-- {
198		f.panes[i].resizeMu.Unlock()
199	}
200	for i := len(f.tabs) - 1; i >= 0; i-- {
201		f.tabs[i].layoutApplyMu.Unlock()
202	}
203	for i := len(f.sessions) - 1; i >= 0; i-- {
204		f.sessions[i].layoutApplyMu.Unlock()
205	}
206	f.held = false
207}

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