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}