1use crate::{Pos, TextBuffer};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum Motion {
30 Left,
32 Right,
34 Up,
36 Down,
38
39 FileStart,
41
42 FileEnd,
44
45 LineStart,
47
48 LineEnd,
50
51 WordStartBefore,
53
54 WordStartAfter,
56
57 WordEndAfter,
59}
60
61#[inline]
66pub fn apply_motion(buffer: &TextBuffer, cursor: Pos, motion: Motion) -> Pos {
67 let cursor = buffer.clamp_pos(cursor);
68
69 match motion {
70 Motion::Left => buffer.move_left(cursor),
71 Motion::Right => buffer.move_right(cursor),
72 Motion::Up => buffer.move_up(cursor),
73 Motion::Down => buffer.move_down(cursor),
74
75 Motion::FileStart => {
76 let target_col = if cursor.line == 0 { 0 } else { cursor.col };
77 buffer.clamp_pos(Pos::new(0, target_col))
78 }
79
80 Motion::FileEnd => {
81 let last = buffer.len_lines().saturating_sub(1);
82 buffer.clamp_pos(Pos::new(last, cursor.col))
83 }
84
85 Motion::LineStart => Pos::new(cursor.line, 0),
86
87 Motion::LineEnd => {
88 let line = buffer.clamp_line(cursor.line);
89 let end_col = buffer.line_len_chars(line);
90 Pos::new(line, end_col)
91 }
92
93 Motion::WordStartBefore => buffer.word_start_before(cursor),
94
95 Motion::WordStartAfter => buffer.word_start_after(cursor),
96
97 Motion::WordEndAfter => buffer.word_end_after(cursor),
98 }
99}
100
101pub fn apply_motion_n(buffer: &TextBuffer, cursor: Pos, motion: Motion, count: usize) -> Pos {
106 let mut cur = buffer.clamp_pos(cursor);
107 for _ in 0..count {
108 let next = apply_motion(buffer, cur, motion);
109 if next == cur {
111 break;
112 }
113 cur = next;
114 }
115 cur
116}
117
118pub mod helpers {
120 use super::{apply_motion_n, Motion};
121 use crate::{Pos, TextBuffer};
122
123 #[inline]
125 pub fn word_forward(buffer: &TextBuffer, cursor: Pos, count: usize) -> Pos {
126 apply_motion_n(buffer, cursor, Motion::WordStartAfter, count)
127 }
128
129 #[inline]
131 pub fn word_backward(buffer: &TextBuffer, cursor: Pos, count: usize) -> Pos {
132 apply_motion_n(buffer, cursor, Motion::WordStartBefore, count)
133 }
134
135 #[inline]
137 pub fn gg(buffer: &TextBuffer, cursor: Pos) -> Pos {
138 super::apply_motion(buffer, cursor, Motion::FileStart)
139 }
140
141 #[inline]
143 pub fn file_end(buffer: &TextBuffer, cursor: Pos) -> Pos {
144 super::apply_motion(buffer, cursor, Motion::FileEnd)
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn motion_count_zero_is_noop() {
154 let b = TextBuffer::from_str("abc\n");
155 let p = Pos::new(0, 2);
156 assert_eq!(apply_motion_n(&b, p, Motion::Left, 0), p);
157 assert_eq!(apply_motion_n(&b, p, Motion::WordEndAfter, 0), p);
158 }
159
160 #[test]
161 fn gg_goes_to_first_line_and_clamps_column() {
162 let b = TextBuffer::from_str("a\nbb\nccc\n");
163 let p = Pos::new(2, 2);
164 let p2 = apply_motion(&b, p, Motion::FileStart);
165 assert_eq!(p2.line, 0);
166 assert_eq!(p2.col, 1);
168 }
169
170 #[test]
171 fn file_end_goes_to_last_line_and_clamps_column() {
172 let b = TextBuffer::from_str("a\nbb\nccc\n");
175 let p = Pos::new(0, 10);
176 let p2 = apply_motion(&b, p, Motion::FileEnd);
177
178 assert_eq!(p2.line, 3);
180 assert_eq!(p2.col, 0);
181 }
182
183 #[test]
184 fn line_start_and_line_end_work() {
185 let b = TextBuffer::from_str("hello\nworld!\n");
186 let p = Pos::new(1, 2);
187
188 let start = apply_motion(&b, p, Motion::LineStart);
189 assert_eq!(start, Pos::new(1, 0));
190
191 let end = apply_motion(&b, p, Motion::LineEnd);
192 assert_eq!(end, Pos::new(1, 6));
193 }
194
195 #[test]
196 fn left_right_clamp_at_bounds() {
197 let b = TextBuffer::from_str("ab\n");
200 let p0 = Pos::new(0, 0);
201 assert_eq!(apply_motion(&b, p0, Motion::Left), Pos::new(0, 0));
202
203 let p_end = Pos::new(0, 2);
205 assert_eq!(apply_motion(&b, p_end, Motion::Right), Pos::new(1, 0));
206 }
207
208 #[test]
209 fn up_down_preserve_column_when_possible() {
210 let b = TextBuffer::from_str("aaaa\nb\ncccccc\n");
211 let p = Pos::new(0, 3);
212
213 let down = apply_motion(&b, p, Motion::Down);
214 assert_eq!(down, Pos::new(1, 1));
216
217 let down2 = apply_motion(&b, down, Motion::Down);
218 assert_eq!(down2, Pos::new(2, 1));
220
221 let up = apply_motion(&b, down2, Motion::Up);
222 assert_eq!(up, Pos::new(1, 1));
223 }
224
225 #[test]
226 fn word_motions_ascii_smoke() {
227 let b = TextBuffer::from_str("abc def_12!\n");
228 let p = Pos::new(0, 6); let start = apply_motion(&b, p, Motion::WordStartBefore);
231 assert_eq!(start, Pos::new(0, 5));
232
233 let end = apply_motion(&b, start, Motion::WordEndAfter);
234 assert_eq!(end, Pos::new(0, 10));
235 }
236
237 #[test]
238 fn repeated_word_forward_stops_at_eof() {
239 let b = TextBuffer::from_str("a b c\n");
240 let p = Pos::new(0, 0);
241 let p2 = apply_motion_n(&b, p, Motion::WordEndAfter, 100);
242
243 assert_eq!(p2, Pos::new(0, 5));
245 }
246
247 #[test]
248 fn word_start_after_visits_symbol_tokens() {
249 let b = TextBuffer::from_str("(normal/insert/command)\n");
250 let mut p = Pos::new(0, 0);
251
252 p = apply_motion(&b, p, Motion::WordStartAfter);
253 assert_eq!(p, Pos::new(0, 1)); p = apply_motion(&b, p, Motion::WordStartAfter);
256 assert_eq!(p, Pos::new(0, 7)); p = apply_motion(&b, p, Motion::WordStartAfter);
259 assert_eq!(p, Pos::new(0, 8)); p = apply_motion(&b, p, Motion::WordStartAfter);
262 assert_eq!(p, Pos::new(0, 14)); p = apply_motion(&b, p, Motion::WordStartAfter);
265 assert_eq!(p, Pos::new(0, 15)); p = apply_motion(&b, p, Motion::WordStartAfter);
268 assert_eq!(p, Pos::new(0, 22)); }
270
271 #[test]
272 fn word_start_before_stops_on_symbol_token() {
273 let b = TextBuffer::from_str("(normal/insert)\n");
274 let p = Pos::new(0, 15); let p2 = apply_motion(&b, p, Motion::WordStartBefore);
276 assert_eq!(p2, Pos::new(0, 14)); }
278
279 #[test]
280 fn word_end_after_can_land_on_symbol_token() {
281 let b = TextBuffer::from_str("alpha / beta\n");
282
283 let p = Pos::new(0, 0);
284 let p = apply_motion(&b, p, Motion::WordEndAfter);
285 assert_eq!(p, Pos::new(0, 4)); let p = apply_motion(&b, p, Motion::WordEndAfter);
288 assert_eq!(p, Pos::new(0, 6)); }
290}