copy_scroll_render_test.go RawHistoryFolder

1package daemon
2
3import (
4	"fmt"
5	"testing"
6
7	renderer "github.com/bnema/vev-vt"
8	"github.com/bnema/vev/internal/domain"
9	scopy "github.com/bnema/vev/internal/usecase/copy"
10	"github.com/stretchr/testify/require"
11)
12
13func TestCopyRowCompositionMatchesFrameReference(t *testing.T) {
14	for _, target := range []domain.Rect{
15		{X: 2, Y: 2, Width: 12, Height: 4},
16		{X: -3, Y: -1, Width: 12, Height: 4},
17		{X: 15, Y: 5, Width: 12, Height: 4},
18		{X: -20, Y: 2, Width: 12, Height: 4},
19		{X: 25, Y: 2, Width: 12, Height: 4},
20		{X: 2, Y: 20, Width: 12, Height: 4},
21		{X: 2, Y: 2, Width: 3, Height: 1},
22	} {
23		t.Run(fmt.Sprint(target), func(t *testing.T) {
24			// The viewport is taller than the document: blank rows must erase the
25			// old pane rather than leak its contents. Selection/search share the
26			// same row renderer as the owned-frame API.
27			rows := [][]renderer.Cell{testRow("alpha beta"), testRow("second row")}
28			mode := scopy.NewMode(scopy.NewDocument(scopy.NewSnapshotFromRows(rows, 12, 4), ""))
29			mode.Search("alpha")
30			require.True(t, mode.StartCharacterSelection(scopy.Pos{Col: 1}))
31			require.True(t, mode.ExtendCharacterSelection(scopy.Pos{Row: 1, Col: 3}))
32			styles := resolveStyles(nil)
33			base := renderer.NewFrame(20, 7)
34			for y := range base.Height {
35				base.FillRow(y, 0, base.Width, renderer.Cell{Rune: '#'})
36			}
37			want := base.Clone()
38			src := mode.Render(styles.CopyStatus, styles.Selection)
39			// Reference placement deliberately uses independent per-cell clipping.
40			for y := 0; y < src.Height-1 && y < target.Height; y++ {
41				for x := 0; x < src.Width && x < target.Width; x++ {
42					dx, dy := target.X+x, target.Y+y
43					if dx >= 0 && dx < want.Width && dy >= 0 && dy < want.Height-1 {
44						want.Set(dx, dy, src.Cell(x, y))
45					}
46				}
47			}
48			want.FillRow(want.Height-1, 0, want.Width, renderer.Cell{Rune: ' ', Style: styles.SurfaceBar})
49			for x := range min(want.Width, src.Width) {
50				want.Set(x, want.Height-1, src.Cell(x, src.Height-1))
51			}
52			got, _ := composeCopyClientFrame(mode, target, base, styles)
53			require.Equal(t, captureTestFrame(want), captureTestFrame(got))
54			require.NoError(t, got.CheckInvariants())
55		})
56	}
57}
58
59func TestCopyScrollKeepsBaseUnadornedAndExitRefreshesLiveState(t *testing.T) {
60	f := newPerformanceFixture(t, performanceConfig{size: domain.Size{Cols: 80, Rows: 24}, panes: 1, historyRows: 100})
61	ack := func() { f.ac.ackOutputState(f.ac.output.currentEpoch(), f.ac.output.next) }
62	f.d.enterCopyMode(f.sess, f.ac)
63	ack()
64	require.True(t, f.ac.render.cache.valid)
65	committed := f.ac.render.cache.frame
66	before := captureTestFrame(committed)
67	f.d.copyWheel(f.sess, f.ac, -30)
68	ack()
69	require.True(t, f.ac.render.cache.valid)
70	require.Equal(t, before, captureTestFrame(committed), "scroll must not mutate the committed buffer")
71	require.NotContains(t, frameText(f.ac.render.cache.frame), "[SCROLL]")
72
73	// Live damage still updates the base behind the immutable history viewport.
74	f.activePane.mu.Lock()
75	f.activePane.screen.Write([]byte("\x1b[1;1HLIVE-UPDATED"))
76	f.activePane.mu.Unlock()
77	f.d.copyWheel(f.sess, f.ac, -3)
78	ack()
79	require.Contains(t, frameText(f.ac.render.cache.frame), "LIVE-UPDATED")
80	require.NotContains(t, frameText(f.ac.render.cache.frame), "[SCROLL]")
81
82	f.d.copyWheel(f.sess, f.ac, 1000)
83	ack()
84	require.Nil(t, f.ac.overlays.copyMode)
85	require.Contains(t, frameText(f.ac.render.cache.frame), "LIVE-UPDATED")
86	require.NoError(t, f.ac.render.cache.frame.CheckInvariants())
87}

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