v0.7.0 4c16dd8

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

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