1use crate::{Buffer, Position};
12
13fn line_chars(line: &str) -> usize {
16 line.chars().count()
17}
18
19fn last_col(line: &str) -> usize {
22 line_chars(line).saturating_sub(1)
23}
24
25fn clamp_to_segment(start: usize, end: usize, visual_col: usize, line: &str) -> usize {
30 let line_max = last_col(line);
31 let seg_max = if end > start { end - 1 } else { start };
32 let want = start.saturating_add(visual_col);
33 want.min(seg_max).min(line_max).max(start.min(line_max))
34}
35
36impl Buffer {
37 pub fn move_left(&mut self, count: usize) {
41 let cursor = self.cursor();
42 let new_col = cursor.col.saturating_sub(count.max(1));
43 self.set_cursor(Position::new(cursor.row, new_col));
44 self.refresh_sticky_col_from_cursor();
45 }
46
47 pub fn move_right_in_line(&mut self, count: usize) {
50 let cursor = self.cursor();
51 let line = self.line(cursor.row).unwrap_or("");
52 let limit = last_col(line);
53 let new_col = (cursor.col + count.max(1)).min(limit);
54 self.set_cursor(Position::new(cursor.row, new_col));
55 self.refresh_sticky_col_from_cursor();
56 }
57
58 pub fn move_right_to_end(&mut self, count: usize) {
61 let cursor = self.cursor();
62 let line = self.line(cursor.row).unwrap_or("");
63 let limit = line_chars(line);
64 let new_col = (cursor.col + count.max(1)).min(limit);
65 self.set_cursor(Position::new(cursor.row, new_col));
66 self.refresh_sticky_col_from_cursor();
67 }
68
69 pub fn move_line_start(&mut self) {
71 let row = self.cursor().row;
72 self.set_cursor(Position::new(row, 0));
73 self.refresh_sticky_col_from_cursor();
74 }
75
76 pub fn move_first_non_blank(&mut self) {
78 let row = self.cursor().row;
79 let col = self
80 .line(row)
81 .unwrap_or("")
82 .chars()
83 .position(|c| !c.is_whitespace())
84 .unwrap_or(0);
85 self.set_cursor(Position::new(row, col));
86 self.refresh_sticky_col_from_cursor();
87 }
88
89 pub fn move_line_end(&mut self) {
91 let row = self.cursor().row;
92 let col = last_col(self.line(row).unwrap_or(""));
93 self.set_cursor(Position::new(row, col));
94 self.refresh_sticky_col_from_cursor();
95 }
96
97 pub fn move_last_non_blank(&mut self) {
100 let row = self.cursor().row;
101 let line = self.line(row).unwrap_or("");
102 let col = line
103 .char_indices()
104 .rev()
105 .find(|(_, c)| !c.is_whitespace())
106 .map(|(byte, _)| line[..byte].chars().count())
107 .unwrap_or(0);
108 self.set_cursor(Position::new(row, col));
109 self.refresh_sticky_col_from_cursor();
110 }
111
112 pub fn move_paragraph_prev(&mut self, count: usize) {
114 let mut row = self.cursor().row;
115 for _ in 0..count.max(1) {
116 if row == 0 {
117 break;
118 }
119 let mut r = row.saturating_sub(1);
122 while r > 0 && self.line(r).is_some_and(|l| l.is_empty()) {
123 r -= 1;
124 }
125 while r > 0 && self.line(r).is_some_and(|l| !l.is_empty()) {
126 r -= 1;
127 }
128 row = r;
129 }
130 self.set_cursor(Position::new(row, 0));
131 self.refresh_sticky_col_from_cursor();
132 }
133
134 pub fn move_paragraph_next(&mut self, count: usize) {
136 let last = self.row_count().saturating_sub(1);
137 let mut row = self.cursor().row;
138 for _ in 0..count.max(1) {
139 if row >= last {
140 break;
141 }
142 let mut r = row.saturating_add(1);
143 while r < last && self.line(r).is_some_and(|l| l.is_empty()) {
144 r += 1;
145 }
146 while r < last && self.line(r).is_some_and(|l| !l.is_empty()) {
147 r += 1;
148 }
149 row = r;
150 }
151 self.set_cursor(Position::new(row, 0));
152 self.refresh_sticky_col_from_cursor();
153 }
154
155 pub fn move_up(&mut self, count: usize) {
159 self.move_vertical(-(count.max(1) as isize));
160 }
161
162 pub fn move_down(&mut self, count: usize) {
164 self.move_vertical(count.max(1) as isize);
165 }
166
167 pub fn move_screen_up(&mut self, count: usize) {
174 self.move_screen_vertical(-(count.max(1) as isize));
175 }
176
177 pub fn move_screen_down(&mut self, count: usize) {
179 self.move_screen_vertical(count.max(1) as isize);
180 }
181
182 pub fn move_top(&mut self) {
184 self.set_cursor(Position::new(0, 0));
185 self.move_first_non_blank();
186 }
187
188 pub fn move_bottom(&mut self, count: usize) {
191 let last = self.row_count().saturating_sub(1);
192 let target = if count == 0 {
193 last
194 } else {
195 (count - 1).min(last)
196 };
197 self.set_cursor(Position::new(target, 0));
198 self.move_first_non_blank();
199 }
200
201 pub fn move_word_fwd(&mut self, big: bool, count: usize) {
206 for _ in 0..count.max(1) {
207 let from = self.cursor();
208 if let Some(next) = next_word_start(self, from, big) {
209 self.set_cursor(next);
210 } else {
211 break;
212 }
213 }
214 self.refresh_sticky_col_from_cursor();
215 }
216
217 pub fn move_word_back(&mut self, big: bool, count: usize) {
219 for _ in 0..count.max(1) {
220 let from = self.cursor();
221 if let Some(prev) = prev_word_start(self, from, big) {
222 self.set_cursor(prev);
223 } else {
224 break;
225 }
226 }
227 self.refresh_sticky_col_from_cursor();
228 }
229
230 pub fn match_bracket(&mut self) -> bool {
234 let cursor = self.cursor();
235 let line = match self.line(cursor.row) {
236 Some(l) => l,
237 None => return false,
238 };
239 let ch = match line.chars().nth(cursor.col) {
240 Some(c) => c,
241 None => return false,
242 };
243 let (open, close, forward) = match ch {
244 '(' => ('(', ')', true),
245 ')' => ('(', ')', false),
246 '[' => ('[', ']', true),
247 ']' => ('[', ']', false),
248 '{' => ('{', '}', true),
249 '}' => ('{', '}', false),
250 '<' => ('<', '>', true),
251 '>' => ('<', '>', false),
252 _ => return false,
253 };
254 let mut depth: i32 = 0;
255 if forward {
256 let mut r = cursor.row;
257 let mut c = cursor.col;
258 loop {
259 let chars: Vec<char> = self.line(r).unwrap_or("").chars().collect();
260 while c < chars.len() {
261 let here = chars[c];
262 if here == open {
263 depth += 1;
264 } else if here == close {
265 depth -= 1;
266 if depth == 0 {
267 self.set_cursor(Position::new(r, c));
268 self.refresh_sticky_col_from_cursor();
269 return true;
270 }
271 }
272 c += 1;
273 }
274 if r + 1 >= self.row_count() {
275 return false;
276 }
277 r += 1;
278 c = 0;
279 }
280 } else {
281 let mut r = cursor.row;
282 let mut c = cursor.col as isize;
283 loop {
284 let chars: Vec<char> = self.line(r).unwrap_or("").chars().collect();
285 while c >= 0 {
286 let here = chars[c as usize];
287 if here == close {
288 depth += 1;
289 } else if here == open {
290 depth -= 1;
291 if depth == 0 {
292 self.set_cursor(Position::new(r, c as usize));
293 self.refresh_sticky_col_from_cursor();
294 return true;
295 }
296 }
297 c -= 1;
298 }
299 if r == 0 {
300 return false;
301 }
302 r -= 1;
303 c = self.line(r).unwrap_or("").chars().count() as isize - 1;
304 }
305 }
306 }
307
308 pub fn find_char_on_line(&mut self, ch: char, forward: bool, till: bool) -> bool {
313 let cursor = self.cursor();
314 let line = match self.line(cursor.row) {
315 Some(l) => l,
316 None => return false,
317 };
318 let chars: Vec<char> = line.chars().collect();
319 if chars.is_empty() {
320 return false;
321 }
322 let target_col = if forward {
323 chars
324 .iter()
325 .enumerate()
326 .skip(cursor.col + 1)
327 .find(|(_, c)| **c == ch)
328 .map(|(i, _)| if till { i.saturating_sub(1) } else { i })
329 } else {
330 (0..cursor.col)
331 .rev()
332 .find(|&i| chars[i] == ch)
333 .map(|i| if till { i + 1 } else { i })
334 };
335 match target_col {
336 Some(col) => {
337 self.set_cursor(Position::new(cursor.row, col));
338 self.refresh_sticky_col_from_cursor();
339 true
340 }
341 None => false,
342 }
343 }
344
345 pub fn move_word_end(&mut self, big: bool, count: usize) {
347 for _ in 0..count.max(1) {
348 let from = self.cursor();
349 if let Some(end) = next_word_end(self, from, big) {
350 self.set_cursor(end);
351 } else {
352 break;
353 }
354 }
355 self.refresh_sticky_col_from_cursor();
356 }
357
358 pub fn move_viewport_top(&mut self, offset: usize) {
362 let v = self.viewport();
363 let last = self.row_count().saturating_sub(1);
364 let target = v.top_row.saturating_add(offset).min(last);
365 self.set_cursor(Position::new(target, 0));
366 self.move_first_non_blank();
367 }
368
369 pub fn move_viewport_middle(&mut self) {
371 let v = self.viewport();
372 let last = self.row_count().saturating_sub(1);
373 let height = v.height as usize;
374 let visible_bot = v.top_row.saturating_add(height.saturating_sub(1)).min(last);
375 let mid = v.top_row + (visible_bot - v.top_row) / 2;
376 self.set_cursor(Position::new(mid, 0));
377 self.move_first_non_blank();
378 }
379
380 pub fn move_viewport_bottom(&mut self, offset: usize) {
382 let v = self.viewport();
383 let last = self.row_count().saturating_sub(1);
384 let height = v.height as usize;
385 let visible_bot = v.top_row.saturating_add(height.saturating_sub(1)).min(last);
386 let target = visible_bot.saturating_sub(offset).max(v.top_row);
387 self.set_cursor(Position::new(target, 0));
388 self.move_first_non_blank();
389 }
390
391 pub fn move_word_end_back(&mut self, big: bool, count: usize) {
395 for _ in 0..count.max(1) {
396 let from = self.cursor();
397 match prev_word_end(self, from, big) {
398 Some(p) => self.set_cursor(p),
399 None => break,
400 }
401 }
402 self.refresh_sticky_col_from_cursor();
403 }
404
405 fn move_screen_vertical(&mut self, delta: isize) {
408 let v = self.viewport();
409 if matches!(v.wrap, crate::Wrap::None) || v.text_width == 0 {
410 self.move_vertical(delta);
411 return;
412 }
413 let cursor = self.cursor();
417 let line = self.line(cursor.row).unwrap_or("");
418 let segs = crate::wrap::wrap_segments(line, v.text_width, v.wrap);
419 let seg_idx = crate::wrap::segment_for_col(&segs, cursor.col);
420 let visual_col = cursor.col.saturating_sub(segs[seg_idx].0);
421 let down = delta > 0;
422 for _ in 0..delta.unsigned_abs() {
423 if !self.step_screen(down, visual_col) {
424 break;
425 }
426 }
427 self.set_sticky_col(Some(self.cursor().col));
428 }
429
430 fn step_screen(&mut self, down: bool, visual_col: usize) -> bool {
434 let v = self.viewport();
435 let cursor = self.cursor();
436 let line = self.line(cursor.row).unwrap_or("");
437 let segs = crate::wrap::wrap_segments(line, v.text_width, v.wrap);
438 let seg_idx = crate::wrap::segment_for_col(&segs, cursor.col);
439 if down {
440 if seg_idx + 1 < segs.len() {
441 let (s, e) = segs[seg_idx + 1];
442 let target = clamp_to_segment(s, e, visual_col, line);
443 self.set_cursor(Position::new(cursor.row, target));
444 return true;
445 }
446 let Some(next_row) = self.next_visible_row(cursor.row) else {
447 return false;
448 };
449 let next_line = self.line(next_row).unwrap_or("");
450 let next_segs = crate::wrap::wrap_segments(next_line, v.text_width, v.wrap);
451 let (s, e) = next_segs[0];
452 let target = clamp_to_segment(s, e, visual_col, next_line);
453 self.set_cursor(Position::new(next_row, target));
454 true
455 } else {
456 if seg_idx > 0 {
457 let (s, e) = segs[seg_idx - 1];
458 let target = clamp_to_segment(s, e, visual_col, line);
459 self.set_cursor(Position::new(cursor.row, target));
460 return true;
461 }
462 let Some(prev_row) = self.prev_visible_row(cursor.row) else {
463 return false;
464 };
465 let prev_line = self.line(prev_row).unwrap_or("");
466 let prev_segs = crate::wrap::wrap_segments(prev_line, v.text_width, v.wrap);
467 let (s, e) = *prev_segs.last().unwrap_or(&(0, 0));
468 let target = clamp_to_segment(s, e, visual_col, prev_line);
469 self.set_cursor(Position::new(prev_row, target));
470 true
471 }
472 }
473
474 fn move_vertical(&mut self, delta: isize) {
475 let cursor = self.cursor();
476 let want = self.sticky_col().unwrap_or(cursor.col);
477 self.set_sticky_col(Some(want));
481 let mut target_row = cursor.row;
484 if delta < 0 {
485 for _ in 0..(-delta) as usize {
486 match self.prev_visible_row(target_row) {
487 Some(r) => target_row = r,
488 None => break,
489 }
490 }
491 } else {
492 for _ in 0..delta as usize {
493 match self.next_visible_row(target_row) {
494 Some(r) => target_row = r,
495 None => break,
496 }
497 }
498 }
499 let line = self.line(target_row).unwrap_or("");
500 let max_col = last_col(line);
501 let target_col = want.min(max_col);
502 self.set_cursor(Position::new(target_row, target_col));
503 }
504
505 fn refresh_sticky_col_from_cursor(&mut self) {
508 let col = self.cursor().col;
509 self.set_sticky_col(Some(col));
510 }
511}
512
513fn is_word(c: char) -> bool {
515 c.is_alphanumeric() || c == '_'
516}
517
518#[derive(Debug, Clone, Copy, PartialEq, Eq)]
522enum CharKind {
523 Word,
524 Punct,
525 Space,
526}
527
528fn char_kind(c: char, big: bool) -> CharKind {
529 if c.is_whitespace() {
530 CharKind::Space
531 } else if big || is_word(c) {
532 CharKind::Word
535 } else {
536 CharKind::Punct
537 }
538}
539
540fn step_forward(buf: &Buffer, pos: Position) -> Option<Position> {
542 let line = buf.line(pos.row)?;
543 let len = line_chars(line);
544 if pos.col + 1 < len {
545 return Some(Position::new(pos.row, pos.col + 1));
546 }
547 if pos.row + 1 < buf.row_count() {
548 return Some(Position::new(pos.row + 1, 0));
549 }
550 None
551}
552
553fn step_back(buf: &Buffer, pos: Position) -> Option<Position> {
555 if pos.col > 0 {
556 return Some(Position::new(pos.row, pos.col - 1));
557 }
558 if pos.row == 0 {
559 return None;
560 }
561 let prev_row = pos.row - 1;
562 let prev_len = line_chars(buf.line(prev_row).unwrap_or(""));
563 Some(Position::new(prev_row, prev_len.saturating_sub(1)))
564}
565
566fn char_at(buf: &Buffer, pos: Position) -> Option<char> {
567 buf.line(pos.row)?.chars().nth(pos.col)
568}
569
570fn next_word_start(buf: &Buffer, from: Position, big: bool) -> Option<Position> {
571 let start_kind = char_at(buf, from).map(|c| char_kind(c, big));
572 let mut cur = from;
573 if let Some(kind) = start_kind {
578 while char_at(buf, cur).map(|c| char_kind(c, big)) == Some(kind) {
579 let prev_row = cur.row;
580 match step_forward(buf, cur) {
581 Some(next) => {
582 cur = next;
583 if next.row != prev_row {
584 break;
585 }
586 }
587 None => return Some(end_of_buffer(buf)),
588 }
589 }
590 }
591 while char_at(buf, cur).map(|c| char_kind(c, big)) == Some(CharKind::Space) {
594 match step_forward(buf, cur) {
595 Some(next) => cur = next,
596 None => return Some(end_of_buffer(buf)),
597 }
598 }
599 Some(cur)
600}
601
602fn end_of_buffer(buf: &Buffer) -> Position {
606 let last_row = buf.row_count().saturating_sub(1);
607 let last_line = buf.line(last_row).unwrap_or("");
608 Position::new(last_row, line_chars(last_line))
609}
610
611fn prev_word_start(buf: &Buffer, from: Position, big: bool) -> Option<Position> {
612 let mut cur = step_back(buf, from)?;
613 while char_at(buf, cur).map(|c| char_kind(c, big)) == Some(CharKind::Space) {
615 cur = step_back(buf, cur)?;
616 }
617 let target_kind = char_at(buf, cur).map(|c| char_kind(c, big))?;
618 loop {
620 let Some(prev) = step_back(buf, cur) else {
621 return Some(cur);
622 };
623 if char_at(buf, prev).map(|c| char_kind(c, big)) == Some(target_kind) {
624 cur = prev;
625 } else {
626 return Some(cur);
627 }
628 }
629}
630
631fn prev_word_end(buf: &Buffer, from: Position, big: bool) -> Option<Position> {
636 let mut cur = step_back(buf, from)?;
637 loop {
638 if char_at(buf, cur).map(|c| char_kind(c, big)) == Some(CharKind::Space) {
641 cur = step_back(buf, cur)?;
642 continue;
643 }
644 let here = char_kind_or_space(buf, cur, big);
645 let next = next_char_kind_in_row(buf, cur, big);
646 let same = if big {
647 here != CharKind::Space && next != CharKind::Space
648 } else {
649 here == next
650 };
651 if !same {
652 return Some(cur);
653 }
654 cur = step_back(buf, cur)?;
655 }
656}
657
658fn char_kind_or_space(buf: &Buffer, pos: Position, big: bool) -> CharKind {
663 char_at(buf, pos)
664 .map(|c| char_kind(c, big))
665 .unwrap_or(CharKind::Space)
666}
667
668fn next_char_kind_in_row(buf: &Buffer, pos: Position, big: bool) -> CharKind {
672 let line = buf.line(pos.row).unwrap_or("");
673 let len = line_chars(line);
674 if pos.col + 1 < len {
675 char_kind_or_space(buf, Position::new(pos.row, pos.col + 1), big)
676 } else {
677 CharKind::Space
678 }
679}
680
681fn next_word_end(buf: &Buffer, from: Position, big: bool) -> Option<Position> {
682 let mut cur = step_forward(buf, from)?;
685 while char_at(buf, cur).map(|c| char_kind(c, big)) == Some(CharKind::Space) {
686 cur = step_forward(buf, cur)?;
687 }
688 let kind = char_at(buf, cur).map(|c| char_kind(c, big))?;
689 loop {
690 let Some(next) = step_forward(buf, cur) else {
691 return Some(cur);
692 };
693 if char_at(buf, next).map(|c| char_kind(c, big)) == Some(kind) {
694 cur = next;
695 } else {
696 return Some(cur);
697 }
698 }
699}
700
701#[cfg(test)]
702mod tests {
703 use super::*;
704
705 fn at(b: &Buffer) -> Position {
706 b.cursor()
707 }
708
709 #[test]
710 fn move_left_clamps_at_zero() {
711 let mut b = Buffer::from_str("abcd");
712 b.move_right_in_line(3);
713 assert_eq!(at(&b), Position::new(0, 3));
714 b.move_left(10);
715 assert_eq!(at(&b), Position::new(0, 0));
716 }
717
718 #[test]
719 fn move_left_does_not_wrap_to_prev_row() {
720 let mut b = Buffer::from_str("abc\ndef");
721 b.move_down(1);
722 assert_eq!(at(&b).row, 1);
723 b.move_left(99);
724 assert_eq!(at(&b), Position::new(1, 0));
725 }
726
727 #[test]
728 fn move_right_in_line_stops_at_last_char() {
729 let mut b = Buffer::from_str("abcd");
730 b.move_right_in_line(99);
731 assert_eq!(at(&b), Position::new(0, 3));
732 }
733
734 #[test]
735 fn move_right_to_end_allows_one_past() {
736 let mut b = Buffer::from_str("abcd");
737 b.move_right_to_end(99);
738 assert_eq!(at(&b), Position::new(0, 4));
739 }
740
741 #[test]
742 fn move_line_start_end() {
743 let mut b = Buffer::from_str(" hello");
744 b.move_line_end();
745 assert_eq!(at(&b), Position::new(0, 6));
746 b.move_line_start();
747 assert_eq!(at(&b), Position::new(0, 0));
748 b.move_first_non_blank();
749 assert_eq!(at(&b), Position::new(0, 2));
750 }
751
752 #[test]
753 fn move_line_end_on_empty_row_stays_at_zero() {
754 let mut b = Buffer::from_str("");
755 b.move_line_end();
756 assert_eq!(at(&b), Position::new(0, 0));
757 }
758
759 #[test]
760 fn move_down_preserves_sticky_col_across_short_row() {
761 let mut b = Buffer::from_str("hello world\nhi\nlong line again");
762 b.move_right_in_line(7);
763 assert_eq!(at(&b), Position::new(0, 7));
764 b.move_down(1);
765 assert_eq!(at(&b).row, 1);
766 assert_eq!(at(&b).col, 1);
768 b.move_down(1);
769 assert_eq!(at(&b), Position::new(2, 7));
771 }
772
773 #[test]
774 fn move_down_skips_closed_fold() {
775 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
776 b.add_fold(1, 3, true);
777 b.move_down(1);
780 assert_eq!(at(&b).row, 1);
781 b.move_down(1);
782 assert_eq!(at(&b).row, 4);
783 }
784
785 #[test]
786 fn move_up_skips_closed_fold() {
787 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
788 b.add_fold(1, 3, true);
789 b.set_cursor(Position::new(4, 0));
790 b.move_up(1);
791 assert_eq!(at(&b).row, 1);
792 b.move_up(1);
793 assert_eq!(at(&b).row, 0);
794 }
795
796 #[test]
797 fn open_fold_is_walked_normally() {
798 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
799 b.add_fold(1, 3, false);
800 b.move_down(2);
802 assert_eq!(at(&b).row, 2);
803 }
804
805 #[test]
806 fn move_top_lands_on_first_non_blank() {
807 let mut b = Buffer::from_str(" indented\nrow2");
808 b.move_down(1);
809 b.move_top();
810 assert_eq!(at(&b), Position::new(0, 4));
811 }
812
813 #[test]
814 fn move_bottom_with_count_jumps_to_line() {
815 let mut b = Buffer::from_str("a\n b\nc\nd");
816 b.move_bottom(2);
817 assert_eq!(at(&b), Position::new(1, 2));
818 }
819
820 #[test]
821 fn move_bottom_zero_jumps_to_last_row() {
822 let mut b = Buffer::from_str("a\nb\nc");
823 b.move_bottom(0);
824 assert_eq!(at(&b), Position::new(2, 0));
825 }
826
827 #[test]
828 fn move_word_fwd_skips_whitespace_runs() {
829 let mut b = Buffer::from_str("foo bar baz");
830 b.move_word_fwd(false, 1);
831 assert_eq!(at(&b), Position::new(0, 4));
832 b.move_word_fwd(false, 1);
833 assert_eq!(at(&b), Position::new(0, 9));
834 }
835
836 #[test]
837 fn move_word_fwd_separates_word_from_punct_in_small_w() {
838 let mut b = Buffer::from_str("foo.bar");
839 b.move_word_fwd(false, 1);
840 assert_eq!(at(&b), Position::new(0, 3));
841 b.move_word_fwd(false, 1);
842 assert_eq!(at(&b), Position::new(0, 4));
843 }
844
845 #[test]
846 fn move_word_fwd_big_collapses_word_and_punct() {
847 let mut b = Buffer::from_str("foo.bar baz");
848 b.move_word_fwd(true, 1);
849 assert_eq!(at(&b), Position::new(0, 8));
850 }
851
852 #[test]
853 fn move_word_back_lands_on_word_start() {
854 let mut b = Buffer::from_str("foo bar baz");
855 b.move_line_end();
856 assert_eq!(at(&b), Position::new(0, 10));
857 b.move_word_back(false, 1);
858 assert_eq!(at(&b), Position::new(0, 8));
859 b.move_word_back(false, 2);
860 assert_eq!(at(&b), Position::new(0, 0));
861 }
862
863 #[test]
864 fn move_word_end_lands_on_last_char() {
865 let mut b = Buffer::from_str("foo bar");
866 b.move_word_end(false, 1);
867 assert_eq!(at(&b), Position::new(0, 2));
868 b.move_word_end(false, 1);
869 assert_eq!(at(&b), Position::new(0, 6));
870 }
871
872 #[test]
873 fn find_char_forward_lands_on_match() {
874 let mut b = Buffer::from_str("foo,bar,baz");
875 assert!(b.find_char_on_line(',', true, false));
876 assert_eq!(at(&b), Position::new(0, 3));
877 assert!(b.find_char_on_line(',', true, false));
878 assert_eq!(at(&b), Position::new(0, 7));
879 }
880
881 #[test]
882 fn find_char_till_stops_one_short() {
883 let mut b = Buffer::from_str("foo,bar");
884 assert!(b.find_char_on_line(',', true, true));
885 assert_eq!(at(&b), Position::new(0, 2));
886 }
887
888 #[test]
889 fn find_char_backward_lands_on_match() {
890 let mut b = Buffer::from_str("foo,bar,baz");
891 b.set_cursor(Position::new(0, 10));
892 assert!(b.find_char_on_line(',', false, false));
893 assert_eq!(at(&b), Position::new(0, 7));
894 }
895
896 #[test]
897 fn find_char_no_match_returns_false() {
898 let mut b = Buffer::from_str("hello");
899 assert!(!b.find_char_on_line('z', true, false));
900 assert_eq!(at(&b), Position::new(0, 0));
901 }
902
903 #[test]
904 fn move_viewport_top_with_offset() {
905 let mut b = Buffer::from_str("a\nb\nc\nd\ne\nf");
906 b.viewport_mut().top_row = 1;
907 b.viewport_mut().height = 4;
908 b.move_viewport_top(2);
909 assert_eq!(at(&b), Position::new(3, 0));
910 }
911
912 #[test]
913 fn move_viewport_middle_picks_center_of_visible() {
914 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
915 b.viewport_mut().top_row = 0;
916 b.viewport_mut().height = 5;
917 b.move_viewport_middle();
918 assert_eq!(at(&b), Position::new(2, 0));
919 }
920
921 #[test]
922 fn move_viewport_bottom_with_offset() {
923 let mut b = Buffer::from_str("a\nb\nc\nd\ne");
924 b.viewport_mut().top_row = 0;
925 b.viewport_mut().height = 5;
926 b.move_viewport_bottom(1);
927 assert_eq!(at(&b), Position::new(3, 0));
928 }
929
930 #[test]
931 fn move_word_end_back_lands_on_prev_word_end() {
932 let mut b = Buffer::from_str("foo bar baz");
933 b.set_cursor(Position::new(0, 9));
934 b.move_word_end_back(false, 1);
935 assert_eq!(at(&b), Position::new(0, 6));
936 b.move_word_end_back(false, 1);
937 assert_eq!(at(&b), Position::new(0, 2));
938 }
939
940 #[test]
941 fn move_word_end_back_big_skips_punct() {
942 let mut b = Buffer::from_str("foo-bar qux");
943 b.set_cursor(Position::new(0, 10));
944 b.move_word_end_back(true, 1);
945 assert_eq!(at(&b), Position::new(0, 6));
946 }
947
948 #[test]
949 fn move_word_end_back_crosses_lines() {
950 let mut b = Buffer::from_str("abc\ndef");
951 b.set_cursor(Position::new(1, 2));
952 b.move_word_end_back(false, 1);
953 assert_eq!(at(&b), Position::new(0, 2));
954 }
955
956 #[test]
957 fn match_bracket_pairs_within_line() {
958 let mut b = Buffer::from_str("if (x + y) {");
959 b.set_cursor(Position::new(0, 3));
960 assert!(b.match_bracket());
961 assert_eq!(at(&b), Position::new(0, 9));
962 assert!(b.match_bracket());
963 assert_eq!(at(&b), Position::new(0, 3));
964 }
965
966 #[test]
967 fn match_bracket_handles_nesting() {
968 let mut b = Buffer::from_str("((x))");
969 b.set_cursor(Position::new(0, 0));
970 assert!(b.match_bracket());
971 assert_eq!(at(&b), Position::new(0, 4));
972 }
973
974 #[test]
975 fn match_bracket_crosses_lines() {
976 let mut b = Buffer::from_str("{\n x\n}");
977 b.set_cursor(Position::new(0, 0));
978 assert!(b.match_bracket());
979 assert_eq!(at(&b), Position::new(2, 0));
980 }
981
982 #[test]
983 fn match_bracket_returns_false_off_bracket() {
984 let mut b = Buffer::from_str("hello");
985 assert!(!b.match_bracket());
986 }
987
988 #[test]
989 fn motion_count_zero_treated_as_one() {
990 let mut b = Buffer::from_str("abcd");
991 b.move_right_in_line(0);
992 assert_eq!(at(&b), Position::new(0, 1));
993 }
994
995 fn enable_wrap(b: &mut Buffer, mode: crate::Wrap, text_width: u16) {
996 let v = b.viewport_mut();
997 v.wrap = mode;
998 v.text_width = text_width;
999 v.height = 10;
1000 }
1001
1002 #[test]
1003 fn screen_down_falls_back_to_move_down_when_wrap_off() {
1004 let mut b = Buffer::from_str("a\nb\nc");
1005 b.move_screen_down(1);
1006 assert_eq!(at(&b), Position::new(1, 0));
1007 b.move_screen_down(1);
1008 assert_eq!(at(&b), Position::new(2, 0));
1009 }
1010
1011 #[test]
1012 fn screen_down_walks_within_wrapped_row() {
1013 let mut b = Buffer::from_str("aaaabbbbcccc\nx");
1015 enable_wrap(&mut b, crate::Wrap::Char, 4);
1016 b.set_cursor(Position::new(0, 1));
1017 b.move_screen_down(1);
1018 assert_eq!(at(&b), Position::new(0, 5));
1020 b.move_screen_down(1);
1021 assert_eq!(at(&b), Position::new(0, 9));
1022 b.move_screen_down(1);
1024 assert_eq!(at(&b), Position::new(1, 0));
1025 }
1026
1027 #[test]
1028 fn screen_up_walks_within_wrapped_row() {
1029 let mut b = Buffer::from_str("aaaabbbbcccc");
1030 enable_wrap(&mut b, crate::Wrap::Char, 4);
1031 b.set_cursor(Position::new(0, 9));
1032 b.move_screen_up(1);
1033 assert_eq!(at(&b), Position::new(0, 5));
1035 b.move_screen_up(1);
1036 assert_eq!(at(&b), Position::new(0, 1));
1037 b.move_screen_up(1);
1039 assert_eq!(at(&b), Position::new(0, 1));
1040 }
1041
1042 #[test]
1043 fn screen_down_clamps_to_short_segment() {
1044 let mut b = Buffer::from_str("aaaaaabb\nx");
1048 enable_wrap(&mut b, crate::Wrap::Char, 6);
1049 b.set_cursor(Position::new(0, 4));
1050 b.move_screen_down(1);
1051 assert_eq!(at(&b), Position::new(0, 7));
1053 b.move_screen_down(1);
1054 assert_eq!(at(&b), Position::new(1, 0));
1056 }
1057
1058 #[test]
1059 fn screen_down_count_compounds() {
1060 let mut b = Buffer::from_str("aaaabbbbcccc");
1061 enable_wrap(&mut b, crate::Wrap::Char, 4);
1062 b.set_cursor(Position::new(0, 0));
1063 b.move_screen_down(2);
1064 assert_eq!(at(&b), Position::new(0, 8));
1065 }
1066}