1use crate::{Pos, TextBuffer};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum Motion {
28 Left,
30 Right,
32 Up,
34 Down,
36
37 FileStart,
39
40 FileEnd,
42
43 LineStart,
45
46 LineEnd,
48
49 WordStartBefore,
51
52 WordStartAfter,
54
55 WordEndAfter,
57}
58
59#[inline]
64pub fn apply_motion(buffer: &TextBuffer, cursor: Pos, motion: Motion) -> Pos {
65 let cursor = buffer.clamp_pos(cursor);
66
67 match motion {
68 Motion::Left => buffer.move_left(cursor),
69 Motion::Right => buffer.move_right(cursor),
70 Motion::Up => buffer.move_up(cursor),
71 Motion::Down => buffer.move_down(cursor),
72
73 Motion::FileStart => {
74 let target_col = if cursor.line == 0 { 0 } else { cursor.col };
75 buffer.clamp_pos(Pos::new(0, target_col))
76 }
77
78 Motion::FileEnd => {
79 let last = buffer.len_lines().saturating_sub(1);
80 buffer.clamp_pos(Pos::new(last, cursor.col))
81 }
82
83 Motion::LineStart => Pos::new(cursor.line, 0),
84
85 Motion::LineEnd => {
86 let line = buffer.clamp_line(cursor.line);
87 let end_col = buffer.line_len_chars(line);
88 Pos::new(line, end_col)
89 }
90
91 Motion::WordStartBefore => buffer.word_start_before(cursor),
92
93 Motion::WordStartAfter => buffer.word_start_after(cursor),
94
95 Motion::WordEndAfter => buffer.word_end_after(cursor),
96 }
97}
98
99pub fn apply_motion_n(buffer: &TextBuffer, cursor: Pos, motion: Motion, count: usize) -> Pos {
104 let mut cur = buffer.clamp_pos(cursor);
105 for _ in 0..count {
106 let next = apply_motion(buffer, cur, motion);
107 if next == cur {
109 break;
110 }
111 cur = next;
112 }
113 cur
114}
115
116pub mod helpers {
118 use super::{Motion, apply_motion_n};
119 use crate::{Pos, TextBuffer};
120
121 #[inline]
123 pub fn word_forward(buffer: &TextBuffer, cursor: Pos, count: usize) -> Pos {
124 apply_motion_n(buffer, cursor, Motion::WordStartAfter, count)
125 }
126
127 #[inline]
129 pub fn word_backward(buffer: &TextBuffer, cursor: Pos, count: usize) -> Pos {
130 apply_motion_n(buffer, cursor, Motion::WordStartBefore, count)
131 }
132
133 #[inline]
135 pub fn gg(buffer: &TextBuffer, cursor: Pos) -> Pos {
136 super::apply_motion(buffer, cursor, Motion::FileStart)
137 }
138
139 #[inline]
141 pub fn file_end(buffer: &TextBuffer, cursor: Pos) -> Pos {
142 super::apply_motion(buffer, cursor, Motion::FileEnd)
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn motion_count_zero_is_noop() {
152 let b = TextBuffer::from_str("abc\n");
153 let p = Pos::new(0, 2);
154 assert_eq!(apply_motion_n(&b, p, Motion::Left, 0), p);
155 assert_eq!(apply_motion_n(&b, p, Motion::WordEndAfter, 0), p);
156 }
157
158 #[test]
159 fn gg_goes_to_first_line_and_clamps_column() {
160 let b = TextBuffer::from_str("a\nbb\nccc\n");
161 let p = Pos::new(2, 2);
162 let p2 = apply_motion(&b, p, Motion::FileStart);
163 assert_eq!(p2.line, 0);
164 assert_eq!(p2.col, 1);
166 }
167
168 #[test]
169 fn file_end_goes_to_last_line_and_clamps_column() {
170 let b = TextBuffer::from_str("a\nbb\nccc\n");
173 let p = Pos::new(0, 10);
174 let p2 = apply_motion(&b, p, Motion::FileEnd);
175
176 assert_eq!(p2.line, 3);
178 assert_eq!(p2.col, 0);
179 }
180
181 #[test]
182 fn line_start_and_line_end_work() {
183 let b = TextBuffer::from_str("hello\nworld!\n");
184 let p = Pos::new(1, 2);
185
186 let start = apply_motion(&b, p, Motion::LineStart);
187 assert_eq!(start, Pos::new(1, 0));
188
189 let end = apply_motion(&b, p, Motion::LineEnd);
190 assert_eq!(end, Pos::new(1, 6));
191 }
192
193 #[test]
194 fn left_right_clamp_at_bounds() {
195 let b = TextBuffer::from_str("ab\n");
198 let p0 = Pos::new(0, 0);
199 assert_eq!(apply_motion(&b, p0, Motion::Left), Pos::new(0, 0));
200
201 let p_end = Pos::new(0, 2);
203 assert_eq!(apply_motion(&b, p_end, Motion::Right), Pos::new(1, 0));
204 }
205
206 #[test]
207 fn up_down_preserve_column_when_possible() {
208 let b = TextBuffer::from_str("aaaa\nb\ncccccc\n");
209 let p = Pos::new(0, 3);
210
211 let down = apply_motion(&b, p, Motion::Down);
212 assert_eq!(down, Pos::new(1, 1));
214
215 let down2 = apply_motion(&b, down, Motion::Down);
216 assert_eq!(down2, Pos::new(2, 1));
218
219 let up = apply_motion(&b, down2, Motion::Up);
220 assert_eq!(up, Pos::new(1, 1));
221 }
222
223 #[test]
224 fn word_motions_ascii_smoke() {
225 let b = TextBuffer::from_str("abc def_12!\n");
226 let p = Pos::new(0, 6); let start = apply_motion(&b, p, Motion::WordStartBefore);
229 assert_eq!(start, Pos::new(0, 5));
230
231 let end = apply_motion(&b, start, Motion::WordEndAfter);
232 assert_eq!(end, Pos::new(0, 10));
233 }
234
235 #[test]
236 fn repeated_word_forward_stops_at_eof() {
237 let b = TextBuffer::from_str("a b c\n");
238 let p = Pos::new(0, 0);
239 let p2 = apply_motion_n(&b, p, Motion::WordEndAfter, 100);
240
241 assert_eq!(p2, Pos::new(0, 5));
243 }
244
245 #[test]
246 fn word_start_after_visits_symbol_tokens() {
247 let b = TextBuffer::from_str("(normal/insert/command)\n");
248 let mut p = Pos::new(0, 0);
249
250 p = apply_motion(&b, p, Motion::WordStartAfter);
251 assert_eq!(p, Pos::new(0, 1)); p = apply_motion(&b, p, Motion::WordStartAfter);
254 assert_eq!(p, Pos::new(0, 7)); p = apply_motion(&b, p, Motion::WordStartAfter);
257 assert_eq!(p, Pos::new(0, 8)); p = apply_motion(&b, p, Motion::WordStartAfter);
260 assert_eq!(p, Pos::new(0, 14)); p = apply_motion(&b, p, Motion::WordStartAfter);
263 assert_eq!(p, Pos::new(0, 15)); p = apply_motion(&b, p, Motion::WordStartAfter);
266 assert_eq!(p, Pos::new(0, 22)); }
268
269 #[test]
270 fn word_start_before_stops_on_symbol_token() {
271 let b = TextBuffer::from_str("(normal/insert)\n");
272 let p = Pos::new(0, 15); let p2 = apply_motion(&b, p, Motion::WordStartBefore);
274 assert_eq!(p2, Pos::new(0, 14)); }
276
277 #[test]
278 fn word_end_after_can_land_on_symbol_token() {
279 let b = TextBuffer::from_str("alpha / beta\n");
280
281 let p = Pos::new(0, 0);
282 let p = apply_motion(&b, p, Motion::WordEndAfter);
283 assert_eq!(p, Pos::new(0, 4)); let p = apply_motion(&b, p, Motion::WordEndAfter);
286 assert_eq!(p, Pos::new(0, 6)); }
288}