v0.3.0 99dc266

overlay_runtime_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/require"
7
8	renderer "github.com/bnema/vev-vt"
9	"github.com/bnema/vev/internal/domain"
10	scopy "github.com/bnema/vev/internal/usecase/copy"
11	promptui "github.com/bnema/vev/internal/usecase/prompt"
12	"github.com/bnema/vev/internal/usecase/visualsearch"
13)
14
15func TestOverlayRuntimeActive(t *testing.T) {
16	ac := &attachedClient{}
17	ac.initOverlays()
18	require.False(t, ac.overlays.Active())
19
20	ac.overlays.promptMu.Lock()
21	ac.overlays.prompt = promptui.New(" Test ", "")
22	ac.overlays.promptMu.Unlock()
23	require.True(t, ac.overlays.Active())
24}
25
26func TestOverlayRuntimeHandleInputPrecedence(t *testing.T) {
27	cases := []struct {
28		name  string
29		setup func(t *testing.T, d *Daemon, sess *session, ac *attachedClient)
30		input []byte
31		check func(t *testing.T, ac *attachedClient)
32	}{
33		{
34			name: "prompt before palette picker copy",
35			setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
36				t.Helper()
37				d.enterCopyMode(sess, ac)
38				d.enterPicker(sess, ac)
39				d.enterPalette(sess, ac)
40				d.enterPrompt(sess, ac, "Rename", "", func(string) error { return nil })
41			},
42			input: []byte("x"),
43			check: func(t *testing.T, ac *attachedClient) {
44				t.Helper()
45				require.Equal(t, "x", ac.overlays.prompt.Value())
46				require.Equal(t, "", ac.overlays.palette.Query())
47				require.True(t, ac.overlays.pickerActive())
48				require.True(t, ac.overlays.copyActive())
49			},
50		},
51		{
52			name: "palette before picker copy",
53			setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
54				t.Helper()
55				d.enterCopyMode(sess, ac)
56				d.enterPicker(sess, ac)
57				d.enterPalette(sess, ac)
58			},
59			input: []byte("x"),
60			check: func(t *testing.T, ac *attachedClient) {
61				t.Helper()
62				require.Equal(t, "x", ac.overlays.palette.Query())
63				require.True(t, ac.overlays.pickerActive())
64				require.True(t, ac.overlays.copyActive())
65			},
66		},
67		{
68			name: "picker before copy",
69			setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
70				t.Helper()
71				d.enterCopyMode(sess, ac)
72				d.enterPicker(sess, ac)
73			},
74			input: []byte("j"),
75			check: func(t *testing.T, ac *attachedClient) {
76				t.Helper()
77				target, ok := ac.overlays.picker.Selected()
78				require.True(t, ok)
79				require.Equal(t, 1, target.TabIndex)
80				require.True(t, ac.overlays.copyActive())
81			},
82		},
83		{
84			name: "copy",
85			setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
86				t.Helper()
87				d.enterCopyMode(sess, ac)
88			},
89			input: []byte("q"),
90			check: func(t *testing.T, ac *attachedClient) {
91				t.Helper()
92				require.False(t, ac.overlays.copyActive())
93			},
94		},
95	}
96
97	for _, tc := range cases {
98		t.Run(tc.name, func(t *testing.T) {
99			p1, releasePTY1 := newBlockingPTY(t)
100			p2, releasePTY2 := newBlockingPTY(t)
101			d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2)
102			defer releasePTY1()
103			defer releasePTY2()
104			tc.setup(t, d, sess, ac)
105
106			require.True(t, ac.overlays.HandleInput(d, tc.input))
107
108			tc.check(t, ac)
109		})
110	}
111}
112
113func TestOverlayRuntimeSnapshotDoesNotAliasCopySearchState(t *testing.T) {
114	ac := &attachedClient{}
115	ac.initOverlays()
116	snap := scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("alpha"), testRow("beta alpha")}, 16, 2)
117	mode := scopy.NewMode(scopy.NewDocument(snap, domain.DefaultWordSeparators))
118	require.True(t, mode.Search("alpha"))
119	search := visualsearch.New(snap)
120	for _, r := range "alpha" {
121		search.Insert(r)
122	}
123
124	ac.overlays.copyMu.Lock()
125	ac.overlays.copyMode = mode
126	ac.overlays.copySearch = search
127	ac.overlays.copyMu.Unlock()
128
129	renderSnap := ac.overlays.SnapshotForRender()
130	defer renderSnap.Unlock()
131
132	ac.overlays.copyMu.Lock()
133	ac.overlays.copyMode.Searches[0].Row = 99
134	ac.overlays.copySearch.Insert('z')
135	ac.overlays.copyMu.Unlock()
136
137	require.Equal(t, 0, renderSnap.copyMode.Searches[0].Row)
138	require.Equal(t, "alpha", renderSnap.copySearchModel.Query())
139}
140
141func TestOverlayRuntimeHandleInputInactive(t *testing.T) {
142	ac := &attachedClient{}
143	ac.initOverlays()
144
145	require.False(t, ac.overlays.HandleInput(&Daemon{}, []byte("x")))
146}
147
148func TestOverlayRuntimeSnapshotCarriesCopyModeDocumentAndClearReleasesIt(t *testing.T) {
149	ac := &attachedClient{}
150	ac.initOverlays()
151	p := newPane("floating", nil, domain.Size{Cols: 4, Rows: 2})
152	document := scopy.NewDocument(scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 2), domain.DefaultWordSeparators)
153
154	ac.overlays.copyMu.Lock()
155	ac.overlays.copyMode = scopy.NewMode(document)
156	ac.overlays.copyPane = p
157	ac.overlays.copyDocument = document
158	ac.overlays.copyMu.Unlock()
159
160	renderSnap := ac.overlays.SnapshotForRender()
161	require.Same(t, p, renderSnap.copyPane)
162	require.NotNil(t, renderSnap.copyMode)
163	require.Same(t, document, renderSnap.copyMode.Document())
164	renderSnap.Unlock()
165
166	ac.overlays.copyMu.Lock()
167	ac.overlays.clearCopyModeLocked()
168	require.Nil(t, ac.overlays.copyPane)
169	require.Nil(t, ac.overlays.copyMode)
170	require.Nil(t, ac.overlays.copyDocument)
171	ac.overlays.copyMu.Unlock()
172}
173
174func TestOverlayRuntimePointerEpochInvalidatesTransferAndPaneClose(t *testing.T) {
175	ac := &attachedClient{}
176	ac.initOverlays()
177	p := newPane("pane", nil, domain.Size{Cols: 4, Rows: 1})
178	document := scopy.NewDocument(scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 1), domain.DefaultWordSeparators)
179
180	ac.overlays.copyMu.Lock()
181	ac.overlays.beginCopyPointerLocked(copyPointerState{pane: p, document: document, press: scopy.Pos{}})
182	staleEpoch := ac.overlays.copyPointer.epoch
183	ac.overlays.clearCopyPointerForTransferLocked()
184	ac.overlays.invalidateCopyPointerLocked(true) // release/close while publish revalidates
185	require.NotEqual(t, staleEpoch, ac.overlays.copyPointerEpoch)
186	require.False(t, ac.overlays.copyPointer.valid)
187	ac.overlays.copyCandidate = scopy.NewMode(document)
188	ac.overlays.copyDocument = document
189	ac.overlays.copyPane = p
190	ac.overlays.copyMu.Unlock()
191
192	require.True(t, ac.overlays.clearCopyModeForPane(p))
193	ac.overlays.copyMu.Lock()
194	require.False(t, ac.overlays.copyPointer.valid)
195	ac.overlays.copyMu.Unlock()
196}
197
198func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) {
199	ac := &attachedClient{}
200	ac.initOverlays()
201	history := newTestHistory(1)
202	appendHistoryRow(t, history, testRow("before"))
203	document := scopy.NewDocument(scopy.NewSnapshot(history, renderer.NewFrame(6, 1), nil, nil), domain.DefaultWordSeparators)
204
205	ac.overlays.copyMu.Lock()
206	ac.overlays.copyMode = scopy.NewMode(document)
207	ac.overlays.copyDocument = document
208	ac.overlays.copyMu.Unlock()
209
210	appendHistoryRow(t, history, testRow("after"))
211	renderSnap := ac.overlays.SnapshotForRender()
212	defer renderSnap.Unlock()
213	require.NotNil(t, renderSnap.copyMode)
214	require.Same(t, document, renderSnap.copyMode.Document())
215	require.Equal(t, "before", rowText(renderSnap.copyMode.Document().Snapshot().Row(0)))
216}

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