detached 306f37e

refactor(copy): centralize close client recovery

commit
306f37ec41528b566ca8a245b806f6c42412f6ed
author
brice · · 2026-07-11 07:14 UTC
parent
36fe4a5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +22 -12
M internal/usecase/daemon/copymode_lifecycle_test.go +17 -0
@@ -65,6 +65,23 @@ func TestCopyModeLifecycleClosePaneClearsRecoveredClientState(t *testing.T) {
6565 require.Nil(t, copyTargetPane(ac.overlays))
6666 }
6767
68+func TestCopyModeLifecycleCloseOnlyPaneInTabClearsRecoveredClientState(t *testing.T) {
69+ d, sess, ac, _, releases := newManualTabSession(t, 2)
70+ defer func() {
71+ for _, release := range releases {
72+ release()
73+ }
74+ }()
75+ tb := sess.activeTab()
76+ p := tb.focusedPane()
77+ d.enterCopyMode(sess, ac)
78+ require.True(t, ac.overlays.copyActive())
79+
80+ require.NoError(t, d.closePane(sess, tb, p.id, nil, false))
81+ require.False(t, ac.overlays.copyActive())
82+ require.Nil(t, copyTargetPane(ac.overlays))
83+}
84+
6885 func TestCopyModeLifecycleRejectsPublicationForInactiveOrRemovedPane(t *testing.T) {
6986 for _, tc := range []struct {
7087 name string
@@ -65,6 +65,23 @@ func TestCopyModeLifecycleClosePaneClearsRecoveredClientState(t *testing.T) {
65 require.Nil(t, copyTargetPane(ac.overlays))65 require.Nil(t, copyTargetPane(ac.overlays))
66 }66 }
67 67
68+func TestCopyModeLifecycleCloseOnlyPaneInTabClearsRecoveredClientState(t *testing.T) {
69+ d, sess, ac, _, releases := newManualTabSession(t, 2)
70+ defer func() {
71+ for _, release := range releases {
72+ release()
73+ }
74+ }()
75+ tb := sess.activeTab()
76+ p := tb.focusedPane()
77+ d.enterCopyMode(sess, ac)
78+ require.True(t, ac.overlays.copyActive())
79+
80+ require.NoError(t, d.closePane(sess, tb, p.id, nil, false))
81+ require.False(t, ac.overlays.copyActive())
82+ require.Nil(t, copyTargetPane(ac.overlays))
83+}
84+
68 func TestCopyModeLifecycleRejectsPublicationForInactiveOrRemovedPane(t *testing.T) {85 func TestCopyModeLifecycleRejectsPublicationForInactiveOrRemovedPane(t *testing.T) {
69 for _, tc := range []struct {86 for _, tc := range []struct {
70 name string87 name string
M internal/usecase/daemon/pane_actions.go +5 -12
@@ -226,6 +226,11 @@ func (d *Daemon) closePane(sess *session, tb *tab, id layout.PaneID, ac *attache
226226 if tb == nil {
227227 return layout.ErrNotFound
228228 }
229+ if ac == nil {
230+ sess.mu.Lock()
231+ ac = sess.client
232+ sess.mu.Unlock()
233+ }
229234 tb.mu.Lock()
230235 p := tb.panes[id]
231236 if p == nil || tb.tree == nil || !layout.ContainsLeaf(tb.tree.Root, id) {
@@ -234,11 +239,6 @@ func (d *Daemon) closePane(sess *session, tb *tab, id layout.PaneID, ac *attache
234239 }
235240 if len(layout.LeafIDs(tb.tree.Root)) <= 1 {
236241 tb.mu.Unlock()
237- if ac == nil {
238- sess.mu.Lock()
239- ac = sess.client
240- sess.mu.Unlock()
241- }
242242 if ac != nil {
243243 ac.overlays.clearCopyModeForPane(p)
244244 }
@@ -253,13 +253,6 @@ func (d *Daemon) closePane(sess *session, tb *tab, id layout.PaneID, ac *attache
253253 d.applyLayoutLocked(tb)
254254 tb.mu.Unlock()
255255
256- // A reap/normal close can be called without the client argument. Recover
257- // the attached client after releasing tab.mu so copyMu never overlaps it.
258- if ac == nil {
259- sess.mu.Lock()
260- ac = sess.client
261- sess.mu.Unlock()
262- }
263256 if ac != nil {
264257 ac.overlays.clearCopyModeForPane(p)
265258 }
@@ -226,6 +226,11 @@ func (d *Daemon) closePane(sess *session, tb *tab, id layout.PaneID, ac *attache
226 if tb == nil {226 if tb == nil {
227 return layout.ErrNotFound227 return layout.ErrNotFound
228 }228 }
229+ if ac == nil {
230+ sess.mu.Lock()
231+ ac = sess.client
232+ sess.mu.Unlock()
233+ }
229 tb.mu.Lock()234 tb.mu.Lock()
230 p := tb.panes[id]235 p := tb.panes[id]
231 if p == nil || tb.tree == nil || !layout.ContainsLeaf(tb.tree.Root, id) {236 if p == nil || tb.tree == nil || !layout.ContainsLeaf(tb.tree.Root, id) {
@@ -234,11 +239,6 @@ func (d *Daemon) closePane(sess *session, tb *tab, id layout.PaneID, ac *attache
234 }239 }
235 if len(layout.LeafIDs(tb.tree.Root)) <= 1 {240 if len(layout.LeafIDs(tb.tree.Root)) <= 1 {
236 tb.mu.Unlock()241 tb.mu.Unlock()
237- if ac == nil {
238- sess.mu.Lock()
239- ac = sess.client
240- sess.mu.Unlock()
241- }
242 if ac != nil {242 if ac != nil {
243 ac.overlays.clearCopyModeForPane(p)243 ac.overlays.clearCopyModeForPane(p)
244 }244 }
@@ -253,13 +253,6 @@ func (d *Daemon) closePane(sess *session, tb *tab, id layout.PaneID, ac *attache
253 d.applyLayoutLocked(tb)253 d.applyLayoutLocked(tb)
254 tb.mu.Unlock()254 tb.mu.Unlock()
255 255
256- // A reap/normal close can be called without the client argument. Recover
257- // the attached client after releasing tab.mu so copyMu never overlaps it.
258- if ac == nil {
259- sess.mu.Lock()
260- ac = sess.client
261- sess.mu.Unlock()
262- }
263 if ac != nil {256 if ac != nil {
264 ac.overlays.clearCopyModeForPane(p)257 ac.overlays.clearCopyModeForPane(p)
265 }258 }

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