1use crate::{
4 core::direction::{Direction, LinePosition, WordBoundary},
5 mm::{Buffer, Cursor, Position},
6};
7
8use super::types::Motion;
9
10const BRACKET_PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
12
13pub struct MotionEngine;
36
37impl MotionEngine {
38 #[must_use]
49 pub fn calculate(
50 buffer: &Buffer,
51 cursor: &Cursor,
52 motion: Motion,
53 count: usize,
54 ) -> Option<Position> {
55 let count = count.max(1);
56
57 match motion {
58 Motion::Char(dir) => Self::char_motion(buffer, cursor.position, dir, count),
59 Motion::Line(dir) => Self::line_motion(buffer, cursor, dir, count),
60 Motion::Word {
61 direction,
62 boundary,
63 end,
64 } => Self::word_motion(buffer, cursor.position, direction, boundary, end, count),
65 Motion::LinePosition(pos) => Self::line_position(buffer, cursor.position, pos),
66 Motion::Paragraph(dir) => Self::paragraph_motion(buffer, cursor.position, dir, count),
67 Motion::FindChar {
68 char,
69 direction,
70 till,
71 } => Self::find_char(buffer, cursor.position, char, direction, till, count),
72 Motion::JumpLine(line) => Self::jump_line(buffer, cursor, line),
73 Motion::MatchBracket => Self::match_bracket(buffer, cursor.position),
74 }
75 }
76
77 #[must_use]
84 pub fn calculate_with_desired_col(
85 buffer: &Buffer,
86 cursor: &Cursor,
87 motion: Motion,
88 count: usize,
89 ) -> (Option<Position>, Option<usize>) {
90 let count = count.max(1);
91
92 if matches!(motion, Motion::Line(_)) {
93 let desired_col = cursor.preferred_column.unwrap_or(cursor.position.column);
95 let new_pos = Self::calculate(buffer, cursor, motion, count);
96 (new_pos, Some(desired_col))
97 } else {
98 let new_pos = Self::calculate(buffer, cursor, motion, count);
100 (new_pos, None)
101 }
102 }
103
104 fn char_motion(
107 buffer: &Buffer,
108 pos: Position,
109 direction: Direction,
110 count: usize,
111 ) -> Option<Position> {
112 let line_len = buffer.line_len(pos.line)?;
113
114 match direction {
115 Direction::Forward => {
116 let max_col = line_len.saturating_sub(1);
118 let new_col = pos.column.saturating_add(count).min(max_col);
119 Some(Position::new(pos.line, new_col))
120 }
121 Direction::Backward => {
122 let new_col = pos.column.saturating_sub(count);
124 Some(Position::new(pos.line, new_col))
125 }
126 }
127 }
128
129 fn line_motion(
130 buffer: &Buffer,
131 cursor: &Cursor,
132 direction: Direction,
133 count: usize,
134 ) -> Option<Position> {
135 let line_count = buffer.line_count();
136 if line_count == 0 {
137 return None;
138 }
139
140 let new_line = match direction {
141 Direction::Forward => {
142 let target = cursor.position.line.saturating_add(count);
144 target.min(line_count.saturating_sub(1))
145 }
146 Direction::Backward => {
147 cursor.position.line.saturating_sub(count)
149 }
150 };
151
152 let target_col = cursor.effective_column();
154 let line_len = buffer.line_len(new_line)?;
155 let max_col = if line_len == 0 {
156 0
157 } else {
158 line_len.saturating_sub(1)
159 };
160 let new_col = target_col.min(max_col);
161
162 Some(Position::new(new_line, new_col))
163 }
164
165 fn word_motion(
166 buffer: &Buffer,
167 pos: Position,
168 direction: Direction,
169 boundary: WordBoundary,
170 end: bool,
171 count: usize,
172 ) -> Option<Position> {
173 let mut current = pos;
174
175 for _ in 0..count {
176 current = match (direction, end) {
177 (Direction::Forward, false) => Self::word_forward(buffer, current, boundary)?,
178 (Direction::Forward, true) => Self::word_end(buffer, current, boundary)?,
179 (Direction::Backward, false) => Self::word_backward(buffer, current, boundary)?,
180 (Direction::Backward, true) => Self::word_end_backward(buffer, current, boundary)?,
181 };
182 }
183
184 Some(current)
185 }
186
187 #[cfg_attr(coverage_nightly, coverage(off))]
188 fn word_forward(
189 buffer: &Buffer,
190 mut pos: Position,
191 boundary: WordBoundary,
192 ) -> Option<Position> {
193 let line_count = buffer.line_count();
194 if line_count == 0 {
195 return Some(pos);
196 }
197
198 loop {
199 if pos.line >= line_count {
200 pos.line = line_count.saturating_sub(1);
201 pos.column = buffer.line_len(pos.line).unwrap_or(0).saturating_sub(1);
202 break;
203 }
204
205 let line = buffer.line(pos.line)?;
206 let chars: Vec<char> = line.chars().collect();
207 let mut x = pos.column;
208
209 if boundary == WordBoundary::Word {
211 if let Some(&c) = chars.get(x) {
213 if c.is_alphanumeric() || c == '_' {
214 while x < chars.len() && (chars[x].is_alphanumeric() || chars[x] == '_') {
216 x += 1;
217 }
218 } else if !c.is_whitespace() {
219 while x < chars.len()
221 && !chars[x].is_whitespace()
222 && !chars[x].is_alphanumeric()
223 && chars[x] != '_'
224 {
225 x += 1;
226 }
227 }
228 }
229 } else {
230 while x < chars.len() && !chars[x].is_whitespace() {
232 x += 1;
233 }
234 }
235
236 while x < chars.len() && chars[x].is_whitespace() {
238 x += 1;
239 }
240
241 if x < chars.len() {
242 pos.column = x;
243 break;
244 }
245
246 if pos.line + 1 < line_count {
248 pos.line += 1;
249 pos.column = 0;
250
251 if let Some(next_line) = buffer.line(pos.line) {
253 let next_chars: Vec<char> = next_line.chars().collect();
254 let mut nx = 0;
255 while nx < next_chars.len() && next_chars[nx].is_whitespace() {
256 nx += 1;
257 }
258 if nx < next_chars.len() {
259 pos.column = nx;
260 break;
261 }
262 }
263 } else {
264 pos.column = chars.len().saturating_sub(1);
265 break;
266 }
267 }
268
269 Some(pos)
270 }
271
272 #[cfg_attr(coverage_nightly, coverage(off))]
273 fn word_end(buffer: &Buffer, mut pos: Position, boundary: WordBoundary) -> Option<Position> {
274 let line_count = buffer.line_count();
275 if line_count == 0 {
276 return Some(pos);
277 }
278
279 let line_len = buffer.line_len(pos.line)?;
281 if pos.column + 1 < line_len {
282 pos.column += 1;
283 } else if pos.line + 1 < line_count {
284 pos.line += 1;
285 pos.column = 0;
286 }
287
288 loop {
289 if pos.line >= line_count {
290 pos.line = line_count.saturating_sub(1);
291 pos.column = buffer.line_len(pos.line).unwrap_or(0).saturating_sub(1);
292 break;
293 }
294
295 let line = buffer.line(pos.line)?;
296 let chars: Vec<char> = line.chars().collect();
297 let mut x = pos.column;
298
299 while x < chars.len() && chars[x].is_whitespace() {
301 x += 1;
302 }
303
304 if x >= chars.len() {
305 if pos.line + 1 < line_count {
307 pos.line += 1;
308 pos.column = 0;
309 continue;
310 }
311 pos.column = chars.len().saturating_sub(1);
312 break;
313 }
314
315 if boundary == WordBoundary::Word {
317 let is_word_char = chars[x].is_alphanumeric() || chars[x] == '_';
318 if is_word_char {
319 while x + 1 < chars.len()
320 && (chars[x + 1].is_alphanumeric() || chars[x + 1] == '_')
321 {
322 x += 1;
323 }
324 } else {
325 while x + 1 < chars.len()
326 && !chars[x + 1].is_whitespace()
327 && !chars[x + 1].is_alphanumeric()
328 && chars[x + 1] != '_'
329 {
330 x += 1;
331 }
332 }
333 } else {
334 while x + 1 < chars.len() && !chars[x + 1].is_whitespace() {
336 x += 1;
337 }
338 }
339
340 pos.column = x;
341 break;
342 }
343
344 Some(pos)
345 }
346
347 #[cfg_attr(coverage_nightly, coverage(off))]
348 fn word_backward(
349 buffer: &Buffer,
350 mut pos: Position,
351 boundary: WordBoundary,
352 ) -> Option<Position> {
353 if buffer.is_empty() {
354 return Some(pos);
355 }
356
357 loop {
358 let line = buffer.line(pos.line)?;
359 let chars: Vec<char> = line.chars().collect();
360
361 if pos.column == 0 {
363 if pos.line > 0 {
364 pos.line -= 1;
365 pos.column = buffer.line_len(pos.line).unwrap_or(0);
366 continue;
367 }
368 break;
369 }
370
371 let mut x = pos.column.saturating_sub(1);
372
373 while x > 0 && chars.get(x).is_some_and(|c| c.is_whitespace()) {
375 x -= 1;
376 }
377
378 if x == 0 && chars.first().is_some_and(|c| c.is_whitespace()) {
380 if pos.line > 0 {
381 pos.line -= 1;
382 pos.column = buffer.line_len(pos.line).unwrap_or(0);
383 continue;
384 }
385 pos.column = 0;
386 break;
387 }
388
389 if boundary == WordBoundary::Word {
391 if let Some(&c) = chars.get(x) {
392 if c.is_alphanumeric() || c == '_' {
393 while x > 0
394 && chars
395 .get(x - 1)
396 .is_some_and(|c| c.is_alphanumeric() || *c == '_')
397 {
398 x -= 1;
399 }
400 } else if !c.is_whitespace() {
401 while x > 0
402 && chars.get(x - 1).is_some_and(|c| {
403 !c.is_whitespace() && !c.is_alphanumeric() && *c != '_'
404 })
405 {
406 x -= 1;
407 }
408 }
409 }
410 } else {
411 while x > 0 && chars.get(x - 1).is_some_and(|c| !c.is_whitespace()) {
413 x -= 1;
414 }
415 }
416
417 pos.column = x;
418 break;
419 }
420
421 Some(pos)
422 }
423
424 fn word_end_backward(
425 buffer: &Buffer,
426 mut pos: Position,
427 _boundary: WordBoundary,
428 ) -> Option<Position> {
429 if buffer.is_empty() {
434 return Some(pos);
435 }
436
437 if pos.column > 0 {
439 pos.column -= 1;
440 } else if pos.line > 0 {
441 pos.line -= 1;
442 pos.column = buffer.line_len(pos.line).unwrap_or(0).saturating_sub(1);
443 }
444
445 loop {
446 let line = buffer.line(pos.line)?;
447 let chars: Vec<char> = line.chars().collect();
448
449 if chars.is_empty() {
450 if pos.line > 0 {
451 pos.line -= 1;
452 pos.column = buffer.line_len(pos.line).unwrap_or(0).saturating_sub(1);
453 continue;
454 }
455 break;
456 }
457
458 let mut x = pos.column.min(chars.len().saturating_sub(1));
459
460 while x > 0 && chars.get(x).is_some_and(|c| c.is_whitespace()) {
462 x -= 1;
463 }
464
465 if chars.get(x).is_some_and(|c| c.is_whitespace()) {
466 if pos.line > 0 {
468 pos.line -= 1;
469 pos.column = buffer.line_len(pos.line).unwrap_or(0).saturating_sub(1);
470 continue;
471 }
472 pos.column = 0;
473 break;
474 }
475
476 pos.column = x;
478 break;
479 }
480
481 Some(pos)
482 }
483
484 fn line_position(buffer: &Buffer, pos: Position, line_pos: LinePosition) -> Option<Position> {
485 let line = buffer.line(pos.line)?;
486 let chars: Vec<char> = line.chars().collect();
487 let line_len = chars.len();
488
489 let new_col = match line_pos {
490 LinePosition::Start => 0,
491 LinePosition::FirstNonBlank => {
492 chars.iter().position(|c| !c.is_whitespace()).unwrap_or(0)
493 }
494 LinePosition::End => line_len.saturating_sub(1),
495 LinePosition::LastNonBlank => chars
496 .iter()
497 .enumerate()
498 .rev()
499 .find(|(_, c)| !c.is_whitespace())
500 .map_or(0, |(i, _)| i),
501 };
502
503 Some(Position::new(pos.line, new_col))
504 }
505
506 #[cfg_attr(coverage_nightly, coverage(off))]
507 fn paragraph_motion(
508 buffer: &Buffer,
509 pos: Position,
510 direction: Direction,
511 count: usize,
512 ) -> Option<Position> {
513 let line_count = buffer.line_count();
514 if line_count == 0 {
515 return None;
516 }
517
518 let mut current_line = pos.line;
519 let mut found = 0;
520
521 match direction {
522 Direction::Forward => {
523 while current_line < line_count {
525 let line = buffer.line(current_line)?;
526 if line.trim().is_empty() {
527 break;
528 }
529 current_line += 1;
530 }
531
532 while current_line < line_count && found < count {
534 while current_line < line_count {
536 let line = buffer.line(current_line)?;
537 if !line.trim().is_empty() {
538 break;
539 }
540 current_line += 1;
541 }
542
543 if current_line >= line_count {
544 break;
545 }
546
547 found += 1;
548 if found >= count {
549 break;
550 }
551
552 while current_line < line_count {
554 let line = buffer.line(current_line)?;
555 if line.trim().is_empty() {
556 break;
557 }
558 current_line += 1;
559 }
560 }
561 }
562 Direction::Backward => {
563 while current_line > 0 {
565 let line = buffer.line(current_line)?;
566 if line.trim().is_empty() {
567 break;
568 }
569 current_line -= 1;
570 }
571
572 while current_line > 0 && found < count {
574 while current_line > 0 {
576 let line = buffer.line(current_line)?;
577 if !line.trim().is_empty() {
578 break;
579 }
580 current_line -= 1;
581 }
582
583 if current_line == 0 {
584 let line = buffer.line(0)?;
585 if line.trim().is_empty() {
586 break;
587 }
588 }
589
590 found += 1;
591 if found >= count {
592 while current_line > 0 {
594 let prev = buffer.line(current_line - 1)?;
595 if prev.trim().is_empty() {
596 break;
597 }
598 current_line -= 1;
599 }
600 break;
601 }
602
603 while current_line > 0 {
605 let line = buffer.line(current_line)?;
606 if line.trim().is_empty() {
607 break;
608 }
609 current_line -= 1;
610 }
611 }
612 }
613 }
614
615 Some(Position::new(current_line.min(line_count.saturating_sub(1)), 0))
616 }
617
618 fn find_char(
619 buffer: &Buffer,
620 pos: Position,
621 target: char,
622 direction: Direction,
623 till: bool,
624 count: usize,
625 ) -> Option<Position> {
626 let line = buffer.line(pos.line)?;
627 let chars: Vec<char> = line.chars().collect();
628
629 let mut found_count = 0;
630 let mut found_pos = None;
631
632 match direction {
633 Direction::Forward => {
634 for (i, &c) in chars.iter().enumerate().skip(pos.column + 1) {
635 if c == target {
636 found_count += 1;
637 if found_count == count {
638 found_pos = Some(i);
639 break;
640 }
641 }
642 }
643 }
644 Direction::Backward => {
645 for i in (0..pos.column).rev() {
646 if chars.get(i) == Some(&target) {
647 found_count += 1;
648 if found_count == count {
649 found_pos = Some(i);
650 break;
651 }
652 }
653 }
654 }
655 }
656
657 found_pos.map(|col| {
658 let adjusted_col = if till {
659 match direction {
660 Direction::Forward => col.saturating_sub(1).max(pos.column),
661 Direction::Backward => col.saturating_add(1).min(chars.len().saturating_sub(1)),
662 }
663 } else {
664 col
665 };
666 Position::new(pos.line, adjusted_col)
667 })
668 }
669
670 fn jump_line(buffer: &Buffer, _cursor: &Cursor, target: Option<usize>) -> Option<Position> {
671 let line_count = buffer.line_count();
672 if line_count == 0 {
673 return None;
674 }
675
676 let max_line = line_count.saturating_sub(1);
678 let new_line = target.map_or(max_line, |line| line.min(max_line));
679
680 let line_content = buffer.line(new_line)?;
682 let first_non_blank = line_content
683 .chars()
684 .position(|c| !c.is_whitespace())
685 .unwrap_or(0);
686
687 Some(Position::new(new_line, first_non_blank))
688 }
689
690 #[cfg_attr(coverage_nightly, coverage(off))]
691 fn match_bracket(buffer: &Buffer, pos: Position) -> Option<Position> {
692 let line = buffer.line(pos.line)?;
693 let chars: Vec<char> = line.chars().collect();
694 let cursor_char = chars.get(pos.column).copied();
695
696 if let Some(ch) = cursor_char {
698 for (open, close) in BRACKET_PAIRS {
699 if ch == open {
700 return Self::find_forward_bracket(buffer, pos, open, close);
701 } else if ch == close {
702 return Self::find_backward_bracket(buffer, pos, open, close);
703 }
704 }
705 }
706
707 for (offset, &ch) in chars.iter().enumerate().skip(pos.column + 1) {
709 for (open, close) in BRACKET_PAIRS {
710 if ch == open {
711 let search_pos = Position::new(pos.line, offset);
712 return Self::find_forward_bracket(buffer, search_pos, open, close);
713 } else if ch == close {
714 let search_pos = Position::new(pos.line, offset);
715 return Self::find_backward_bracket(buffer, search_pos, open, close);
716 }
717 }
718 }
719
720 None
721 }
722
723 fn find_forward_bracket(
724 buffer: &Buffer,
725 start: Position,
726 open: char,
727 close: char,
728 ) -> Option<Position> {
729 let mut depth = 1;
730 let mut line_idx = start.line;
731 let mut col = start.column + 1;
732
733 while line_idx < buffer.line_count() {
734 let line = buffer.line(line_idx)?;
735 let chars: Vec<char> = line.chars().collect();
736
737 while col < chars.len() {
738 let ch = chars[col];
739 if ch == open {
740 depth += 1;
741 } else if ch == close {
742 depth -= 1;
743 if depth == 0 {
744 return Some(Position::new(line_idx, col));
745 }
746 }
747 col += 1;
748 }
749
750 line_idx += 1;
751 col = 0;
752 }
753
754 None
755 }
756
757 #[cfg_attr(coverage_nightly, coverage(off))]
758 fn find_backward_bracket(
759 buffer: &Buffer,
760 start: Position,
761 open: char,
762 close: char,
763 ) -> Option<Position> {
764 let mut depth = 1;
765 let mut line_idx = start.line;
766
767 if let Some(line) = buffer.line(line_idx) {
769 let chars: Vec<char> = line.chars().collect();
770 if start.column > 0 {
771 for col in (0..start.column).rev() {
772 let ch = chars[col];
773 if ch == close {
774 depth += 1;
775 } else if ch == open {
776 depth -= 1;
777 if depth == 0 {
778 return Some(Position::new(line_idx, col));
779 }
780 }
781 }
782 }
783 }
784
785 while line_idx > 0 {
787 line_idx -= 1;
788 let line = buffer.line(line_idx)?;
789 let chars: Vec<char> = line.chars().collect();
790
791 for col in (0..chars.len()).rev() {
792 let ch = chars[col];
793 if ch == close {
794 depth += 1;
795 } else if ch == open {
796 depth -= 1;
797 if depth == 0 {
798 return Some(Position::new(line_idx, col));
799 }
800 }
801 }
802 }
803
804 None
805 }
806}