navigator_test.go RawHistoryFolder

1package copy
2
3import (
4	"testing"
5
6	vt "github.com/bnema/vev-vt"
7	renderer "github.com/bnema/vev-vt/ansi"
8	"github.com/stretchr/testify/require"
9)
10
11func navigatorDocument() *Document {
12	return NewDocument(NewSnapshotFromRows([][]renderer.Cell{
13		documentCells("abcdef"),
14		documentCells("xy"),
15		{{Rune: '界'}, {Continuation: true}, {Rune: 'z'}},
16		{},
17		documentCells("omega"),
18	}, 6, 2), " -_@")
19}
20
21func TestNavigatorHorizontalSkipsContinuations(t *testing.T) {
22	doc := navigatorDocument()
23
24	tests := []struct {
25		name string
26		move func(*Navigator, *Document) bool
27		from Pos
28		want Pos
29	}{
30		{"right skips wide continuation", (*Navigator).Right, Pos{Row: 2, Col: 0}, Pos{Row: 2, Col: 2}},
31		{"left skips wide continuation", (*Navigator).Left, Pos{Row: 2, Col: 2}, Pos{Row: 2, Col: 0}},
32	}
33	for _, tt := range tests {
34		t.Run(tt.name, func(t *testing.T) {
35			navigator := NewNavigator(tt.from)
36
37			require.True(t, tt.move(&navigator, doc))
38			require.Equal(t, tt.want, navigator.Pos)
39			require.Equal(t, tt.want.Col, navigator.PreferredCol)
40		})
41	}
42}
43
44func TestNavigatorVerticalRetainsPreferredColumn(t *testing.T) {
45	doc := navigatorDocument()
46	navigator := NewNavigator(Pos{Row: 0, Col: 5})
47
48	require.True(t, navigator.Down(doc))
49	require.Equal(t, Pos{Row: 1, Col: 1}, navigator.Pos)
50	require.Equal(t, 5, navigator.PreferredCol)
51
52	require.True(t, navigator.Down(doc))
53	require.Equal(t, Pos{Row: 2, Col: 2}, navigator.Pos)
54	require.Equal(t, 5, navigator.PreferredCol)
55
56	require.True(t, navigator.Down(doc))
57	require.Equal(t, Pos{Row: 3, Col: 0}, navigator.Pos)
58	require.Equal(t, 5, navigator.PreferredCol)
59
60	require.True(t, navigator.Down(doc))
61	require.Equal(t, Pos{Row: 4, Col: 4}, navigator.Pos)
62	require.Equal(t, 5, navigator.PreferredCol)
63
64	require.True(t, navigator.Up(doc))
65	require.Equal(t, Pos{Row: 3, Col: 0}, navigator.Pos)
66	require.Equal(t, 5, navigator.PreferredCol)
67
68	require.True(t, navigator.Up(doc))
69	require.Equal(t, Pos{Row: 2, Col: 2}, navigator.Pos)
70	require.Equal(t, 5, navigator.PreferredCol)
71}
72
73func TestNavigatorWordMotionsCrossRows(t *testing.T) {
74	doc := navigatorDocument()
75
76	tests := []struct {
77		name string
78		move func(*Navigator, *Document) bool
79		from Pos
80		want Pos
81	}{
82		{"word next", (*Navigator).WordNext, Pos{Row: 0, Col: 5}, Pos{Row: 1, Col: 0}},
83		{"word backward", (*Navigator).WordBackward, Pos{Row: 1, Col: 0}, Pos{Row: 0, Col: 0}},
84		{"word end", (*Navigator).WordEnd, Pos{Row: 1, Col: 1}, Pos{Row: 2, Col: 2}},
85	}
86	for _, tt := range tests {
87		t.Run(tt.name, func(t *testing.T) {
88			navigator := NewNavigator(tt.from)
89
90			require.True(t, tt.move(&navigator, doc))
91			require.Equal(t, tt.want, navigator.Pos)
92			require.Equal(t, tt.want.Col, navigator.PreferredCol)
93		})
94	}
95}
96
97func TestNavigatorEmptyTopBottomPageAndSet(t *testing.T) {
98	doc := navigatorDocument()
99
100	t.Run("empty row is stable", func(t *testing.T) {
101		navigator := NewNavigator(Pos{Row: 2, Col: 2})
102		require.True(t, navigator.Down(doc))
103		require.Equal(t, Pos{Row: 3, Col: 0}, navigator.Pos)
104		require.False(t, navigator.Right(doc))
105		require.Equal(t, Pos{Row: 3, Col: 0}, navigator.Pos)
106	})
107
108	t.Run("top and bottom reset the preferred column", func(t *testing.T) {
109		navigator := NewNavigator(Pos{Row: 0, Col: 5})
110		require.True(t, navigator.Down(doc))
111		require.Equal(t, Pos{Row: 1, Col: 1}, navigator.Pos)
112		require.Equal(t, 5, navigator.PreferredCol)
113		require.True(t, navigator.Top(doc))
114		require.Equal(t, Pos{Row: 0, Col: 1}, navigator.Pos)
115		require.Equal(t, 1, navigator.PreferredCol)
116		require.True(t, navigator.Bottom(doc))
117		require.Equal(t, Pos{Row: 4, Col: 1}, navigator.Pos)
118		require.Equal(t, 1, navigator.PreferredCol)
119	})
120
121	t.Run("page keeps preferred column", func(t *testing.T) {
122		navigator := NewNavigator(Pos{Row: 0, Col: 5})
123		require.True(t, navigator.Page(doc, 2))
124		require.Equal(t, Pos{Row: 2, Col: 2}, navigator.Pos)
125		require.Equal(t, 5, navigator.PreferredCol)
126		require.True(t, navigator.Page(doc, 2))
127		require.Equal(t, Pos{Row: 4, Col: 4}, navigator.Pos)
128		require.Equal(t, 5, navigator.PreferredCol)
129	})
130
131	t.Run("set normalizes continuation and updates preferred column", func(t *testing.T) {
132		navigator := NewNavigator(Pos{Row: 0, Col: 0})
133		require.True(t, navigator.Set(doc, Pos{Row: 2, Col: 1}))
134		require.Equal(t, Pos{Row: 2, Col: 0}, navigator.Pos)
135		require.Equal(t, 0, navigator.PreferredCol)
136		require.False(t, navigator.Set(doc, Pos{Row: 9, Col: 0}))
137		require.Equal(t, Pos{Row: 2, Col: 0}, navigator.Pos)
138	})
139}
140
141func TestNavigatorVerticalMovesDoNotAllocate(t *testing.T) {
142	history := vt.NewHistory(vt.HistoryConfig{MaxRows: 8, ChunkRows: 4})
143	for _, text := range []string{"abcdef", "xy", "界z", "", "omega"} {
144		cells := documentCells(text)
145		if text == "界z" {
146			cells = []renderer.Cell{{Rune: '界'}, {Continuation: true}, {Rune: 'z'}}
147		}
148		require.NoError(t, history.Append(cells, vt.LineBound{End: len(cells)}))
149	}
150	// Rows 0-4 come from history and rows 5-6 from the live screen.
151	doc := NewDocument(NewSnapshot(history, renderer.NewFrame(6, 2), nil, nil), " -_@")
152	require.Equal(t, 7, doc.Len())
153
154	tests := []struct {
155		name string
156		move func(*Navigator, *Document) bool
157		from Pos
158		pref int
159		want Pos
160	}{
161		{"down clamps to short row", (*Navigator).Down, Pos{Row: 0, Col: 5}, 5, Pos{Row: 1, Col: 1}},
162		{"down lands on wide head", (*Navigator).Down, Pos{Row: 1, Col: 1}, 1, Pos{Row: 2, Col: 0}},
163		{"down onto empty row", (*Navigator).Down, Pos{Row: 2, Col: 2}, 2, Pos{Row: 3, Col: 0}},
164		{"up from empty row", (*Navigator).Up, Pos{Row: 3, Col: 0}, 4, Pos{Row: 2, Col: 2}},
165		{"down into history tail", (*Navigator).Down, Pos{Row: 3, Col: 0}, 9, Pos{Row: 4, Col: 4}},
166		{"down into live screen", (*Navigator).Down, Pos{Row: 4, Col: 4}, 4, Pos{Row: 5, Col: 4}},
167		{"up out of live screen", (*Navigator).Up, Pos{Row: 5, Col: 4}, 4, Pos{Row: 4, Col: 4}},
168		{"page down clamps to last row", func(n *Navigator, d *Document) bool { return n.Page(d, 100) }, Pos{Row: 0, Col: 2}, 2, Pos{Row: 6, Col: 2}},
169		{"page up clamps to first row", func(n *Navigator, d *Document) bool { return n.Page(d, -100) }, Pos{Row: 6, Col: 2}, 2, Pos{Row: 0, Col: 2}},
170	}
171	for _, tt := range tests {
172		t.Run(tt.name, func(t *testing.T) {
173			navigator := &Navigator{}
174			allocs := testing.AllocsPerRun(100, func() {
175				*navigator = Navigator{Pos: tt.from, PreferredCol: tt.pref}
176				if !tt.move(navigator, doc) || navigator.Pos != tt.want {
177					t.Fatalf("move() = %v, want %v", navigator.Pos, tt.want)
178				}
179			})
180			require.Zero(t, allocs, "vertical navigation must not decode rows")
181		})
182	}
183}

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