selection_test.go RawHistoryFolder

1package copy
2
3import (
4	"testing"
5
6	renderer "github.com/bnema/vev-vt"
7	"github.com/stretchr/testify/require"
8)
9
10func TestSelectionRanges(t *testing.T) {
11	doc := documentFromStrings([]string{"alpha", "bravo", "charlie"}, " -_@")
12
13	tests := []struct {
14		name      string
15		selection Selection
16		want      []CellRange
17	}{
18		{
19			name:      "line forward",
20			selection: Selection{Anchor: Pos{0, 2}, Active: Pos{1, 1}, Granularity: Line, Enabled: true},
21			want:      []CellRange{{Row: 0, Start: 0, End: 4}, {Row: 1, Start: 0, End: 4}},
22		},
23		{
24			name:      "line reverse",
25			selection: Selection{Anchor: Pos{1, 1}, Active: Pos{0, 2}, Granularity: Line, Enabled: true},
26			want:      []CellRange{{Row: 0, Start: 0, End: 4}, {Row: 1, Start: 0, End: 4}},
27		},
28		{
29			name:      "character forward",
30			selection: Selection{Anchor: Pos{0, 2}, Active: Pos{1, 1}, Granularity: Character, Enabled: true},
31			want:      []CellRange{{Row: 0, Start: 2, End: 4}, {Row: 1, Start: 0, End: 1}},
32		},
33		{
34			name:      "character reverse",
35			selection: Selection{Anchor: Pos{1, 1}, Active: Pos{0, 2}, Granularity: Character, Enabled: true},
36			want:      []CellRange{{Row: 0, Start: 2, End: 4}, {Row: 1, Start: 0, End: 1}},
37		},
38		{
39			name:      "word forward",
40			selection: Selection{Anchor: Pos{0, 2}, Active: Pos{1, 1}, Granularity: Word, Enabled: true},
41			want:      []CellRange{{Row: 0, Start: 0, End: 4}, {Row: 1, Start: 0, End: 4}},
42		},
43		{
44			name:      "word reverse",
45			selection: Selection{Anchor: Pos{1, 1}, Active: Pos{0, 2}, Granularity: Word, Enabled: true},
46			want:      []CellRange{{Row: 0, Start: 0, End: 4}, {Row: 1, Start: 0, End: 4}},
47		},
48		{name: "disabled", selection: Selection{}, want: nil},
49	}
50	for _, tt := range tests {
51		t.Run(tt.name, func(t *testing.T) {
52			require.Equal(t, tt.want, tt.selection.Ranges(doc))
53		})
54	}
55}
56
57func TestSelectionRangeForRowMatchesCanonicalRanges(t *testing.T) {
58	wide := []renderer.Cell{{Rune: 'a'}, {Rune: '界'}, {Continuation: true}, {Rune: 'b'}}
59	doc := NewDocument(NewSnapshotFromRows([][]renderer.Cell{row("alpha"), wide, row("bravo")}, 5, 3), " -_@")
60	tests := []struct {
61		name      string
62		selection Selection
63		row       int
64		want      CellRange
65		ok        bool
66	}{
67		{"line reverse", Selection{Anchor: Pos{2, 1}, Active: Pos{0, 2}, Granularity: Line, Enabled: true}, 1, CellRange{Row: 1, End: 3}, true},
68		{"character wide", Selection{Anchor: Pos{0, 2}, Active: Pos{1, 1}, Granularity: Character, Enabled: true}, 1, CellRange{Row: 1, End: 2}, true},
69		{"word", Selection{Anchor: Pos{0, 2}, Active: Pos{2, 1}, Granularity: Word, Enabled: true}, 1, CellRange{Row: 1, End: 3}, true},
70		{"outside selection", Selection{Anchor: Pos{0, 2}, Active: Pos{1, 1}, Granularity: Character, Enabled: true}, 2, CellRange{}, false},
71	}
72	for _, tt := range tests {
73		t.Run(tt.name, func(t *testing.T) {
74			got, ok := tt.selection.RangeForRow(doc, tt.row)
75			require.Equal(t, tt.ok, ok)
76			if ok {
77				require.Equal(t, tt.want, got)
78				require.Contains(t, tt.selection.Ranges(doc), got)
79			}
80		})
81	}
82}
83
84func TestSelectionOrdered(t *testing.T) {
85	tests := []struct {
86		name      string
87		selection Selection
88		start     Pos
89		end       Pos
90	}{
91		{"forward", Selection{Anchor: Pos{0, 2}, Active: Pos{1, 1}}, Pos{0, 2}, Pos{1, 1}},
92		{"reverse", Selection{Anchor: Pos{1, 1}, Active: Pos{0, 2}}, Pos{0, 2}, Pos{1, 1}},
93	}
94	for _, tt := range tests {
95		t.Run(tt.name, func(t *testing.T) {
96			start, end := tt.selection.Ordered()
97			require.Equal(t, tt.start, start)
98			require.Equal(t, tt.end, end)
99		})
100	}
101}
102
103func TestSelectionRangesEmptyRowsAndWideGlyphs(t *testing.T) {
104	wide := []renderer.Cell{{Rune: 'a'}, {Rune: '界'}, {Continuation: true}, {Rune: 'b'}}
105	doc := NewDocument(NewSnapshotFromRows([][]renderer.Cell{{}, wide, {}}, 4, 3), " -_@")
106
107	tests := []struct {
108		name      string
109		selection Selection
110		want      []CellRange
111	}{
112		{
113			name:      "line includes empty rows",
114			selection: Selection{Anchor: Pos{0, 0}, Active: Pos{2, 0}, Granularity: Line, Enabled: true},
115			want:      []CellRange{{Row: 0, Start: 0, End: 0}, {Row: 1, Start: 0, End: 3}, {Row: 2, Start: 0, End: 0}},
116		},
117		{
118			name:      "character expands wide glyph continuation",
119			selection: Selection{Anchor: Pos{1, 1}, Active: Pos{1, 2}, Granularity: Character, Enabled: true},
120			want:      []CellRange{{Row: 1, Start: 1, End: 2}},
121		},
122		{
123			name:      "character crosses empty row",
124			selection: Selection{Anchor: Pos{0, 0}, Active: Pos{1, 1}, Granularity: Character, Enabled: true},
125			want:      []CellRange{{Row: 0, Start: 0, End: 0}, {Row: 1, Start: 0, End: 2}},
126		},
127	}
128	for _, tt := range tests {
129		t.Run(tt.name, func(t *testing.T) {
130			require.Equal(t, tt.want, tt.selection.Ranges(doc))
131		})
132	}
133}
134
135func TestWordSelectionRejectsSeparator(t *testing.T) {
136	doc := documentFromStrings([]string{"alpha/beta"}, "/")
137
138	selection, ok := NewWordSelection(doc, Pos{0, 5})
139	require.False(t, ok)
140	require.False(t, selection.Enabled)
141}
142
143func TestSelectionConstructorsAndMutators(t *testing.T) {
144	doc := documentFromStrings([]string{"alpha", "bravo"}, " -_@")
145
146	selection := NewLineSelection(Pos{0, 2})
147	require.True(t, selection.Enabled)
148	require.Equal(t, Line, selection.Granularity)
149	require.Equal(t, Pos{0, 2}, selection.Anchor)
150
151	selection.Extend(Pos{1, 1})
152	selection.AsCharacter()
153	require.Equal(t, Character, selection.Granularity)
154	require.Equal(t, "pha\nbr", selection.Text(doc))
155
156	word, ok := NewWordSelection(doc, Pos{1, 2})
157	require.True(t, ok)
158	require.Equal(t, "bravo", word.Text(doc))
159	word.Extend(Pos{0, 2})
160	require.Equal(t, "alpha\nbravo", word.Text(doc))
161}

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