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 copy",
35			setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
36				t.Helper()
37				d.enterCopyMode(sess, ac)
38				d.enterPalette(sess, ac)
39				d.enterPrompt(sess, ac, "Rename", "", func(string) error { return nil })
40			},
41			input: []byte("x"),
42			check: func(t *testing.T, ac *attachedClient) {
43				t.Helper()
44				require.Equal(t, "x", ac.overlays.prompt.Value())
45				require.Equal(t, "", ac.overlays.palette.Query())
46				require.True(t, ac.overlays.copyActive())
47			},
48		},
49		{
50			name: "palette before copy",
51			setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
52				t.Helper()
53				d.enterCopyMode(sess, ac)
54				d.enterPalette(sess, ac)
55			},
56			input: []byte("x"),
57			check: func(t *testing.T, ac *attachedClient) {
58				t.Helper()
59				require.Equal(t, "x", ac.overlays.palette.Query())
60				require.True(t, ac.overlays.copyActive())
61			},
62		},
63		{
64			name: "copy",
65			setup: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
66				t.Helper()
67				d.enterCopyMode(sess, ac)
68			},
69			input: []byte("q"),
70			check: func(t *testing.T, ac *attachedClient) {
71				t.Helper()
72				require.False(t, ac.overlays.copyActive())
73			},
74		},
75	}
76
77	for _, tc := range cases {
78		t.Run(tc.name, func(t *testing.T) {
79			p1, releasePTY1 := newBlockingPTY(t)
80			p2, releasePTY2 := newBlockingPTY(t)
81			d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2)
82			defer releasePTY1()
83			defer releasePTY2()
84			tc.setup(t, d, sess, ac)
85
86			require.True(t, ac.overlays.HandleInput(d, tc.input))
87
88			tc.check(t, ac)
89		})
90	}
91}
92
93func TestOverlayRuntimeSnapshotDoesNotAliasCopySearchState(t *testing.T) {
94	ac := &attachedClient{}
95	ac.initOverlays()
96	snap := scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("alpha"), testRow("beta alpha")}, 16, 2)
97	mode := scopy.NewMode(scopy.NewDocument(snap, domain.DefaultWordSeparators))
98	require.True(t, mode.Search("alpha"))
99	search := visualsearch.New(snap)
100	for _, r := range "alpha" {
101		search.Insert(r)
102	}
103
104	ac.overlays.copyMu.Lock()
105	ac.overlays.copyMode = mode
106	ac.overlays.copySearch = search
107	ac.overlays.copyMu.Unlock()
108
109	renderSnap := ac.overlays.SnapshotForRender()
110	defer renderSnap.Unlock()
111
112	ac.overlays.copyMu.Lock()
113	ac.overlays.copyMode.Searches[0].Row = 99
114	ac.overlays.copySearch.Insert('z')
115	ac.overlays.copyMu.Unlock()
116
117	require.Equal(t, 0, renderSnap.copyMode.Searches[0].Row)
118	require.Equal(t, "alpha", renderSnap.copySearchModel.Query())
119}
120
121func TestOverlayRuntimeHandleInputInactive(t *testing.T) {
122	ac := &attachedClient{}
123	ac.initOverlays()
124
125	require.False(t, ac.overlays.HandleInput(&Daemon{}, []byte("x")))
126}
127
128func TestOverlayRuntimeSnapshotCarriesCopyModeDocumentAndClearReleasesIt(t *testing.T) {
129	ac := &attachedClient{}
130	ac.initOverlays()
131	p := newPane("floating", nil, domain.Size{Cols: 4, Rows: 2})
132	document := scopy.NewDocument(scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 2), domain.DefaultWordSeparators)
133
134	ac.overlays.copyMu.Lock()
135	ac.overlays.copyMode = scopy.NewMode(document)
136	ac.overlays.copyPane = p
137	ac.overlays.copyDocument = document
138	ac.overlays.copyMu.Unlock()
139
140	renderSnap := ac.overlays.SnapshotForRender()
141	require.Same(t, p, renderSnap.copyPane)
142	require.NotNil(t, renderSnap.copyMode)
143	require.Same(t, document, renderSnap.copyMode.Document())
144	renderSnap.Unlock()
145
146	ac.overlays.copyMu.Lock()
147	ac.overlays.clearCopyModeLocked()
148	require.Nil(t, ac.overlays.copyPane)
149	require.Nil(t, ac.overlays.copyMode)
150	require.Nil(t, ac.overlays.copyDocument)
151	ac.overlays.copyMu.Unlock()
152}
153
154func TestOverlayRuntimePointerEpochInvalidatesTransferAndPaneClose(t *testing.T) {
155	ac := &attachedClient{}
156	ac.initOverlays()
157	p := newPane("pane", nil, domain.Size{Cols: 4, Rows: 1})
158	document := scopy.NewDocument(scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("row")}, 4, 1), domain.DefaultWordSeparators)
159
160	ac.overlays.copyMu.Lock()
161	ac.overlays.beginCopyPointerLocked(copyPointerState{pane: p, document: document, press: scopy.Pos{}})
162	staleEpoch := ac.overlays.copyPointer.epoch
163	ac.overlays.clearCopyPointerForTransferLocked()
164	ac.overlays.invalidateCopyPointerLocked(true) // release/close while publish revalidates
165	require.NotEqual(t, staleEpoch, ac.overlays.copyPointerEpoch)
166	require.False(t, ac.overlays.copyPointer.valid)
167	ac.overlays.copyCandidate = scopy.NewMode(document)
168	ac.overlays.copyDocument = document
169	ac.overlays.copyPane = p
170	ac.overlays.copyMu.Unlock()
171
172	require.True(t, ac.overlays.clearCopyModeForPane(p))
173	ac.overlays.copyMu.Lock()
174	require.False(t, ac.overlays.copyPointer.valid)
175	ac.overlays.copyMu.Unlock()
176}
177
178func TestOverlayRuntimeCopyModeDocumentSurvivesRingOverwrite(t *testing.T) {
179	ac := &attachedClient{}
180	ac.initOverlays()
181	history := newTestHistory(1)
182	appendHistoryRow(t, history, testRow("before"))
183	document := scopy.NewDocument(scopy.NewSnapshot(history, renderer.NewFrame(6, 1), nil, nil), domain.DefaultWordSeparators)
184
185	ac.overlays.copyMu.Lock()
186	ac.overlays.copyMode = scopy.NewMode(document)
187	ac.overlays.copyDocument = document
188	ac.overlays.copyMu.Unlock()
189
190	appendHistoryRow(t, history, testRow("after"))
191	renderSnap := ac.overlays.SnapshotForRender()
192	defer renderSnap.Unlock()
193	require.NotNil(t, renderSnap.copyMode)
194	require.Same(t, document, renderSnap.copyMode.Document())
195	require.Equal(t, "before", rowText(renderSnap.copyMode.Document().Snapshot().Row(0)))
196}

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