copy_scroll_ownership_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5
6	renderer "github.com/bnema/vev-vt"
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/protocol"
9	scopy "github.com/bnema/vev/internal/usecase/copy"
10	"github.com/stretchr/testify/require"
11)
12
13func TestCopyCacheFailedPublicationAndRetry(t *testing.T) {
14	for _, failure := range []string{"prepare", "send"} {
15		t.Run(failure, func(t *testing.T) {
16			d, sess, ac, sends := newManualSessionWithPTYs(t)
17			healthy := ac.transport()
18			state := cacheState("live", 1)
19			state.floating = capturedFloatingRenderState{}
20			state.overlays = capturedOverlayRenderState{
21				copyActive: true, copyPaneID: "pane",
22				copyMode: scopy.NewMode(scopy.NewDocument(scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("COPY")}, 6, 1), "")),
23			}
24			committed := composeFrame(state, composeCacheInput{})
25			require.True(t, committed.cache.valid)
26			ac.render.cache = committed.cache
27			before := cloneComposeCache(ac.render.cache)
28			state.bars.topRight = "retry"
29			state.attachment = ac
30			state.route.Target = protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
31			state.view.revision = ac.viewSnapshot().revision
32			pending := composeFrame(state, ac.render.cache, ac.render.spare)
33			if failure == "prepare" {
34				pending.frame = renderer.Frame{Width: 1}
35			} else {
36				ac.replaceTransport(cacheFailTransport{})
37			}
38			ac.sendMu.Lock()
39			require.True(t, d.publishCapturedFrameForTest(sess, ac, &state, pending))
40			require.Equal(t, before, cloneComposeCache(ac.render.cache))
41			require.Zero(t, ac.output.next)
42			if failure == "send" {
43				sess.mu.Lock()
44				sess.registerAttachmentLocked(ac)
45				sess.mu.Unlock()
46				ac.setSession(sess)
47				ac.replaceTransport(healthy)
48			}
49			pending = composeFrame(state, ac.render.cache, ac.render.spare)
50			state.view.revision = ac.viewSnapshot().revision
51			ac.sendMu.Lock()
52			require.True(t, d.publishCapturedFrameForTest(sess, ac, &state, pending))
53			require.Equal(t, pending.cache, ac.render.cache)
54			require.NotContains(t, frameText(ac.render.cache.frame), "COPY")
55			output := unmarshalTestOutput(t, (<-sends).Payload)
56			terminal := renderer.NewScreen(pending.frame.Width, pending.frame.Height)
57			terminal.Write(output.Data)
58			require.Contains(t, frameText(captureTestFrame(terminal)), "COPY", "retry must render copy cells despite interleaved ANSI styling")
59			require.Equal(t, uint64(1), ac.output.next)
60		})
61	}
62}
63
64func TestCopyCacheSearchTransition(t *testing.T) {
65	d, sess, ac, sends := newManualSessionWithPTYs(t, nil)
66	sess.tabs[0].focusedPane().screen.Write([]byte("alpha"))
67	ack := func() {
68		mustOutputData(t, sends)
69		ac.ackOutputState(ac.output.currentEpoch(), ac.output.next)
70	}
71	d.enterCopyMode(sess, ac)
72	ack()
73	require.True(t, ac.render.cache.valid)
74	base := captureTestFrame(ac.render.cache.frame)
75	d.handleInput(sess, ac, []byte("/"))
76	ack()
77	require.NotNil(t, ac.overlays.copySearch)
78	require.False(t, ac.render.cache.valid)
79	require.Equal(t, base, captureTestFrame(ac.render.cache.frame), "search modal must not contaminate the live base")
80	d.handleInput(sess, ac, []byte("\x1b"))
81	ack()
82	require.Nil(t, ac.overlays.copySearch)
83	require.NotNil(t, ac.overlays.copyMode)
84	require.True(t, ac.render.cache.valid)
85	require.Equal(t, base, captureTestFrame(ac.render.cache.frame))
86}
87
88func TestCopyExitRefreshesAfterOtherAttachmentConsumesDamage(t *testing.T) {
89	d, sess, ac, sends := newManualSessionWithPTYs(t, nil)
90	p := sess.tabs[0].focusedPane()
91	p.screen.Write([]byte("before"))
92	d.enterCopyMode(sess, ac)
93	mustOutputData(t, sends)
94	ac.ackOutputState(ac.output.currentEpoch(), ac.output.next)
95	copyBefore := captureTestFrame(ac.render.cache.frame)
96
97	tr, peerSends := newCapturingTransport(t)
98	peer := &attachedClient{tr: tr, output: newOutputStateStream(), geometry: domain.Geometry{Size: ac.geometry.Size}}
99	peer.output.attachment = peer
100	peer.initOverlays()
101	peer.setSession(sess)
102	sess.mu.Lock()
103	require.True(t, sess.registerAttachmentLocked(peer))
104	sess.mu.Unlock()
105	p.mu.Lock()
106	p.screen.Write([]byte("\x1b[1;1Hafter-peer"))
107	p.mu.Unlock()
108	// A real peer capture/emission consumes the shared pane damage first.
109	d.paint(sess, peer, true, nil)
110	mustOutputData(t, peerSends)
111	p.mu.Lock()
112	require.Empty(t, p.screen.Damage())
113	p.mu.Unlock()
114	require.Contains(t, frameText(peer.render.cache.frame), "after-peer")
115	require.Equal(t, copyBefore, captureTestFrame(ac.render.cache.frame), "peer publication cannot mutate another attachment's cache")
116
117	// The copy document is still frozen, but leaving it must recapture live
118	// cells even though the other attachment already acknowledged the damage.
119	d.copyWheel(sess, ac, 1)
120	mustOutputData(t, sends)
121	require.Nil(t, ac.overlays.copyMode)
122	require.Contains(t, frameText(ac.render.cache.frame), "after-peer")
123	require.NoError(t, ac.render.cache.frame.CheckInvariants())
124}

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