1use editor::{
2 Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
3 display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
4 movement::{
5 self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
6 },
7};
8use gpui::{Action, Context, Window, actions, px};
9use language::{CharKind, Point, Selection, SelectionGoal};
10use multi_buffer::MultiBufferRow;
11use schemars::JsonSchema;
12use serde::Deserialize;
13use std::ops::Range;
14use workspace::searchable::Direction;
15
16use crate::{
17 Vim,
18 normal::mark,
19 state::{Mode, Operator},
20 surrounds::SurroundsType,
21};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub(crate) enum MotionKind {
25 Linewise,
26 Exclusive,
27 Inclusive,
28}
29
30impl MotionKind {
31 pub(crate) fn for_mode(mode: Mode) -> Self {
32 match mode {
33 Mode::VisualLine => MotionKind::Linewise,
34 _ => MotionKind::Exclusive,
35 }
36 }
37
38 pub(crate) fn linewise(&self) -> bool {
39 matches!(self, MotionKind::Linewise)
40 }
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum Motion {
45 Left,
46 WrappingLeft,
47 Down {
48 display_lines: bool,
49 },
50 Up {
51 display_lines: bool,
52 },
53 Right,
54 WrappingRight,
55 NextWordStart {
56 ignore_punctuation: bool,
57 },
58 NextWordEnd {
59 ignore_punctuation: bool,
60 },
61 PreviousWordStart {
62 ignore_punctuation: bool,
63 },
64 PreviousWordEnd {
65 ignore_punctuation: bool,
66 },
67 NextSubwordStart {
68 ignore_punctuation: bool,
69 },
70 NextSubwordEnd {
71 ignore_punctuation: bool,
72 },
73 PreviousSubwordStart {
74 ignore_punctuation: bool,
75 },
76 PreviousSubwordEnd {
77 ignore_punctuation: bool,
78 },
79 FirstNonWhitespace {
80 display_lines: bool,
81 },
82 CurrentLine,
83 StartOfLine {
84 display_lines: bool,
85 },
86 MiddleOfLine {
87 display_lines: bool,
88 },
89 EndOfLine {
90 display_lines: bool,
91 },
92 SentenceBackward,
93 SentenceForward,
94 StartOfParagraph,
95 EndOfParagraph,
96 StartOfDocument,
97 EndOfDocument,
98 Matching,
99 GoToPercentage,
100 UnmatchedForward {
101 char: char,
102 },
103 UnmatchedBackward {
104 char: char,
105 },
106 FindForward {
107 before: bool,
108 char: char,
109 mode: FindRange,
110 smartcase: bool,
111 },
112 FindBackward {
113 after: bool,
114 char: char,
115 mode: FindRange,
116 smartcase: bool,
117 },
118 Sneak {
119 first_char: char,
120 second_char: char,
121 smartcase: bool,
122 },
123 SneakBackward {
124 first_char: char,
125 second_char: char,
126 smartcase: bool,
127 },
128 RepeatFind {
129 last_find: Box<Motion>,
130 },
131 RepeatFindReversed {
132 last_find: Box<Motion>,
133 },
134 NextLineStart,
135 PreviousLineStart,
136 StartOfLineDownward,
137 EndOfLineDownward,
138 GoToColumn,
139 WindowTop,
140 WindowMiddle,
141 WindowBottom,
142 NextSectionStart,
143 NextSectionEnd,
144 PreviousSectionStart,
145 PreviousSectionEnd,
146 NextMethodStart,
147 NextMethodEnd,
148 PreviousMethodStart,
149 PreviousMethodEnd,
150 NextComment,
151 PreviousComment,
152 PreviousLesserIndent,
153 PreviousGreaterIndent,
154 PreviousSameIndent,
155 NextLesserIndent,
156 NextGreaterIndent,
157 NextSameIndent,
158
159 // we don't have a good way to run a search synchronously, so
160 // we handle search motions by running the search async and then
161 // calling back into motion with this
162 ZedSearchResult {
163 prior_selections: Vec<Range<Anchor>>,
164 new_selections: Vec<Range<Anchor>>,
165 },
166 Jump {
167 anchor: Anchor,
168 line: bool,
169 },
170}
171
172#[derive(Clone, Copy)]
173enum IndentType {
174 Lesser,
175 Greater,
176 Same,
177}
178
179#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
180#[action(namespace = vim)]
181#[serde(deny_unknown_fields)]
182struct NextWordStart {
183 #[serde(default)]
184 ignore_punctuation: bool,
185}
186
187#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
188#[action(namespace = vim)]
189#[serde(deny_unknown_fields)]
190struct NextWordEnd {
191 #[serde(default)]
192 ignore_punctuation: bool,
193}
194
195#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
196#[action(namespace = vim)]
197#[serde(deny_unknown_fields)]
198struct PreviousWordStart {
199 #[serde(default)]
200 ignore_punctuation: bool,
201}
202
203#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
204#[action(namespace = vim)]
205#[serde(deny_unknown_fields)]
206struct PreviousWordEnd {
207 #[serde(default)]
208 ignore_punctuation: bool,
209}
210
211#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
212#[action(namespace = vim)]
213#[serde(deny_unknown_fields)]
214pub(crate) struct NextSubwordStart {
215 #[serde(default)]
216 pub(crate) ignore_punctuation: bool,
217}
218
219#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
220#[action(namespace = vim)]
221#[serde(deny_unknown_fields)]
222pub(crate) struct NextSubwordEnd {
223 #[serde(default)]
224 pub(crate) ignore_punctuation: bool,
225}
226
227#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
228#[action(namespace = vim)]
229#[serde(deny_unknown_fields)]
230pub(crate) struct PreviousSubwordStart {
231 #[serde(default)]
232 pub(crate) ignore_punctuation: bool,
233}
234
235#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
236#[action(namespace = vim)]
237#[serde(deny_unknown_fields)]
238pub(crate) struct PreviousSubwordEnd {
239 #[serde(default)]
240 pub(crate) ignore_punctuation: bool,
241}
242
243#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
244#[action(namespace = vim)]
245#[serde(deny_unknown_fields)]
246pub(crate) struct Up {
247 #[serde(default)]
248 pub(crate) display_lines: bool,
249}
250
251#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
252#[action(namespace = vim)]
253#[serde(deny_unknown_fields)]
254pub(crate) struct Down {
255 #[serde(default)]
256 pub(crate) display_lines: bool,
257}
258
259#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
260#[action(namespace = vim)]
261#[serde(deny_unknown_fields)]
262struct FirstNonWhitespace {
263 #[serde(default)]
264 display_lines: bool,
265}
266
267#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
268#[action(namespace = vim)]
269#[serde(deny_unknown_fields)]
270struct EndOfLine {
271 #[serde(default)]
272 display_lines: bool,
273}
274
275#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
276#[action(namespace = vim)]
277#[serde(deny_unknown_fields)]
278pub struct StartOfLine {
279 #[serde(default)]
280 pub(crate) display_lines: bool,
281}
282
283#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
284#[action(namespace = vim)]
285#[serde(deny_unknown_fields)]
286struct MiddleOfLine {
287 #[serde(default)]
288 display_lines: bool,
289}
290
291#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
292#[action(namespace = vim)]
293#[serde(deny_unknown_fields)]
294struct UnmatchedForward {
295 #[serde(default)]
296 char: char,
297}
298
299#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
300#[action(namespace = vim)]
301#[serde(deny_unknown_fields)]
302struct UnmatchedBackward {
303 #[serde(default)]
304 char: char,
305}
306
307actions!(
308 vim,
309 [
310 Left,
311 #[action(deprecated_aliases = ["vim::Backspace"])]
312 WrappingLeft,
313 Right,
314 #[action(deprecated_aliases = ["vim::Space"])]
315 WrappingRight,
316 CurrentLine,
317 SentenceForward,
318 SentenceBackward,
319 StartOfParagraph,
320 EndOfParagraph,
321 StartOfDocument,
322 EndOfDocument,
323 Matching,
324 GoToPercentage,
325 NextLineStart,
326 PreviousLineStart,
327 StartOfLineDownward,
328 EndOfLineDownward,
329 GoToColumn,
330 RepeatFind,
331 RepeatFindReversed,
332 WindowTop,
333 WindowMiddle,
334 WindowBottom,
335 NextSectionStart,
336 NextSectionEnd,
337 PreviousSectionStart,
338 PreviousSectionEnd,
339 NextMethodStart,
340 NextMethodEnd,
341 PreviousMethodStart,
342 PreviousMethodEnd,
343 NextComment,
344 PreviousComment,
345 PreviousLesserIndent,
346 PreviousGreaterIndent,
347 PreviousSameIndent,
348 NextLesserIndent,
349 NextGreaterIndent,
350 NextSameIndent,
351 ]
352);
353
354pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
355 Vim::action(editor, cx, |vim, _: &Left, window, cx| {
356 vim.motion(Motion::Left, window, cx)
357 });
358 Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
359 vim.motion(Motion::WrappingLeft, window, cx)
360 });
361 Vim::action(editor, cx, |vim, action: &Down, window, cx| {
362 vim.motion(
363 Motion::Down {
364 display_lines: action.display_lines,
365 },
366 window,
367 cx,
368 )
369 });
370 Vim::action(editor, cx, |vim, action: &Up, window, cx| {
371 vim.motion(
372 Motion::Up {
373 display_lines: action.display_lines,
374 },
375 window,
376 cx,
377 )
378 });
379 Vim::action(editor, cx, |vim, _: &Right, window, cx| {
380 vim.motion(Motion::Right, window, cx)
381 });
382 Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
383 vim.motion(Motion::WrappingRight, window, cx)
384 });
385 Vim::action(
386 editor,
387 cx,
388 |vim, action: &FirstNonWhitespace, window, cx| {
389 vim.motion(
390 Motion::FirstNonWhitespace {
391 display_lines: action.display_lines,
392 },
393 window,
394 cx,
395 )
396 },
397 );
398 Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
399 vim.motion(
400 Motion::StartOfLine {
401 display_lines: action.display_lines,
402 },
403 window,
404 cx,
405 )
406 });
407 Vim::action(editor, cx, |vim, action: &MiddleOfLine, window, cx| {
408 vim.motion(
409 Motion::MiddleOfLine {
410 display_lines: action.display_lines,
411 },
412 window,
413 cx,
414 )
415 });
416 Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| {
417 vim.motion(
418 Motion::EndOfLine {
419 display_lines: action.display_lines,
420 },
421 window,
422 cx,
423 )
424 });
425 Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| {
426 vim.motion(Motion::CurrentLine, window, cx)
427 });
428 Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| {
429 vim.motion(Motion::StartOfParagraph, window, cx)
430 });
431 Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| {
432 vim.motion(Motion::EndOfParagraph, window, cx)
433 });
434
435 Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| {
436 vim.motion(Motion::SentenceForward, window, cx)
437 });
438 Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| {
439 vim.motion(Motion::SentenceBackward, window, cx)
440 });
441 Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| {
442 vim.motion(Motion::StartOfDocument, window, cx)
443 });
444 Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| {
445 vim.motion(Motion::EndOfDocument, window, cx)
446 });
447 Vim::action(editor, cx, |vim, _: &Matching, window, cx| {
448 vim.motion(Motion::Matching, window, cx)
449 });
450 Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| {
451 vim.motion(Motion::GoToPercentage, window, cx)
452 });
453 Vim::action(
454 editor,
455 cx,
456 |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| {
457 vim.motion(Motion::UnmatchedForward { char }, window, cx)
458 },
459 );
460 Vim::action(
461 editor,
462 cx,
463 |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| {
464 vim.motion(Motion::UnmatchedBackward { char }, window, cx)
465 },
466 );
467 Vim::action(
468 editor,
469 cx,
470 |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| {
471 vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx)
472 },
473 );
474 Vim::action(
475 editor,
476 cx,
477 |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| {
478 vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx)
479 },
480 );
481 Vim::action(
482 editor,
483 cx,
484 |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| {
485 vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx)
486 },
487 );
488 Vim::action(
489 editor,
490 cx,
491 |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| {
492 vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx)
493 },
494 );
495 Vim::action(
496 editor,
497 cx,
498 |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| {
499 vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx)
500 },
501 );
502 Vim::action(
503 editor,
504 cx,
505 |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| {
506 vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx)
507 },
508 );
509 Vim::action(
510 editor,
511 cx,
512 |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| {
513 vim.motion(
514 Motion::PreviousSubwordStart { ignore_punctuation },
515 window,
516 cx,
517 )
518 },
519 );
520 Vim::action(
521 editor,
522 cx,
523 |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| {
524 vim.motion(
525 Motion::PreviousSubwordEnd { ignore_punctuation },
526 window,
527 cx,
528 )
529 },
530 );
531 Vim::action(editor, cx, |vim, &NextLineStart, window, cx| {
532 vim.motion(Motion::NextLineStart, window, cx)
533 });
534 Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| {
535 vim.motion(Motion::PreviousLineStart, window, cx)
536 });
537 Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| {
538 vim.motion(Motion::StartOfLineDownward, window, cx)
539 });
540 Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| {
541 vim.motion(Motion::EndOfLineDownward, window, cx)
542 });
543 Vim::action(editor, cx, |vim, &GoToColumn, window, cx| {
544 vim.motion(Motion::GoToColumn, window, cx)
545 });
546
547 Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| {
548 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
549 vim.motion(Motion::RepeatFind { last_find }, window, cx);
550 }
551 });
552
553 Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| {
554 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
555 vim.motion(Motion::RepeatFindReversed { last_find }, window, cx);
556 }
557 });
558 Vim::action(editor, cx, |vim, &WindowTop, window, cx| {
559 vim.motion(Motion::WindowTop, window, cx)
560 });
561 Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| {
562 vim.motion(Motion::WindowMiddle, window, cx)
563 });
564 Vim::action(editor, cx, |vim, &WindowBottom, window, cx| {
565 vim.motion(Motion::WindowBottom, window, cx)
566 });
567
568 Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| {
569 vim.motion(Motion::PreviousSectionStart, window, cx)
570 });
571 Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| {
572 vim.motion(Motion::NextSectionStart, window, cx)
573 });
574 Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| {
575 vim.motion(Motion::PreviousSectionEnd, window, cx)
576 });
577 Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| {
578 vim.motion(Motion::NextSectionEnd, window, cx)
579 });
580 Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| {
581 vim.motion(Motion::PreviousMethodStart, window, cx)
582 });
583 Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| {
584 vim.motion(Motion::NextMethodStart, window, cx)
585 });
586 Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| {
587 vim.motion(Motion::PreviousMethodEnd, window, cx)
588 });
589 Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| {
590 vim.motion(Motion::NextMethodEnd, window, cx)
591 });
592 Vim::action(editor, cx, |vim, &NextComment, window, cx| {
593 vim.motion(Motion::NextComment, window, cx)
594 });
595 Vim::action(editor, cx, |vim, &PreviousComment, window, cx| {
596 vim.motion(Motion::PreviousComment, window, cx)
597 });
598 Vim::action(editor, cx, |vim, &PreviousLesserIndent, window, cx| {
599 vim.motion(Motion::PreviousLesserIndent, window, cx)
600 });
601 Vim::action(editor, cx, |vim, &PreviousGreaterIndent, window, cx| {
602 vim.motion(Motion::PreviousGreaterIndent, window, cx)
603 });
604 Vim::action(editor, cx, |vim, &PreviousSameIndent, window, cx| {
605 vim.motion(Motion::PreviousSameIndent, window, cx)
606 });
607 Vim::action(editor, cx, |vim, &NextLesserIndent, window, cx| {
608 vim.motion(Motion::NextLesserIndent, window, cx)
609 });
610 Vim::action(editor, cx, |vim, &NextGreaterIndent, window, cx| {
611 vim.motion(Motion::NextGreaterIndent, window, cx)
612 });
613 Vim::action(editor, cx, |vim, &NextSameIndent, window, cx| {
614 vim.motion(Motion::NextSameIndent, window, cx)
615 });
616}
617
618impl Vim {
619 pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context<Self>) {
620 if let Motion::ZedSearchResult {
621 prior_selections, ..
622 } = &m
623 {
624 match self.mode {
625 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
626 if !prior_selections.is_empty() {
627 self.update_editor(window, cx, |_, editor, window, cx| {
628 editor.change_selections(Default::default(), window, cx, |s| {
629 s.select_ranges(prior_selections.iter().cloned())
630 })
631 });
632 }
633 }
634 Mode::Normal | Mode::Replace | Mode::Insert => {
635 if self.active_operator().is_none() {
636 return;
637 }
638 }
639
640 Mode::HelixNormal => {}
641 }
642 }
643
644 self.motion(m, window, cx)
645 }
646
647 pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context<Self>) {
648 if let Some(Operator::FindForward { .. })
649 | Some(Operator::Sneak { .. })
650 | Some(Operator::SneakBackward { .. })
651 | Some(Operator::FindBackward { .. }) = self.active_operator()
652 {
653 self.pop_operator(window, cx);
654 }
655
656 let count = Vim::take_count(cx);
657 let forced_motion = Vim::take_forced_motion(cx);
658 let active_operator = self.active_operator();
659 let mut waiting_operator: Option<Operator> = None;
660 match self.mode {
661 Mode::Normal | Mode::Replace | Mode::Insert => {
662 if active_operator == Some(Operator::AddSurrounds { target: None }) {
663 waiting_operator = Some(Operator::AddSurrounds {
664 target: Some(SurroundsType::Motion(motion)),
665 });
666 } else {
667 self.normal_motion(
668 motion.clone(),
669 active_operator.clone(),
670 count,
671 forced_motion,
672 window,
673 cx,
674 )
675 }
676 }
677 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
678 self.visual_motion(motion.clone(), count, window, cx)
679 }
680
681 Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, window, cx),
682 }
683 self.clear_operator(window, cx);
684 if let Some(operator) = waiting_operator {
685 self.push_operator(operator, window, cx);
686 Vim::globals(cx).pre_count = count
687 }
688 }
689}
690
691// Motion handling is specified here:
692// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
693impl Motion {
694 fn default_kind(&self) -> MotionKind {
695 use Motion::*;
696 match self {
697 Down { .. }
698 | Up { .. }
699 | StartOfDocument
700 | EndOfDocument
701 | CurrentLine
702 | NextLineStart
703 | PreviousLineStart
704 | StartOfLineDownward
705 | WindowTop
706 | WindowMiddle
707 | WindowBottom
708 | NextSectionStart
709 | NextSectionEnd
710 | PreviousSectionStart
711 | PreviousSectionEnd
712 | NextMethodStart
713 | NextMethodEnd
714 | PreviousMethodStart
715 | PreviousMethodEnd
716 | NextComment
717 | PreviousComment
718 | PreviousLesserIndent
719 | PreviousGreaterIndent
720 | PreviousSameIndent
721 | NextLesserIndent
722 | NextGreaterIndent
723 | NextSameIndent
724 | GoToPercentage
725 | Jump { line: true, .. } => MotionKind::Linewise,
726 EndOfLine { .. }
727 | EndOfLineDownward
728 | Matching
729 | FindForward { .. }
730 | NextWordEnd { .. }
731 | PreviousWordEnd { .. }
732 | NextSubwordEnd { .. }
733 | PreviousSubwordEnd { .. } => MotionKind::Inclusive,
734 Left
735 | WrappingLeft
736 | Right
737 | WrappingRight
738 | StartOfLine { .. }
739 | StartOfParagraph
740 | EndOfParagraph
741 | SentenceBackward
742 | SentenceForward
743 | GoToColumn
744 | MiddleOfLine { .. }
745 | UnmatchedForward { .. }
746 | UnmatchedBackward { .. }
747 | NextWordStart { .. }
748 | PreviousWordStart { .. }
749 | NextSubwordStart { .. }
750 | PreviousSubwordStart { .. }
751 | FirstNonWhitespace { .. }
752 | FindBackward { .. }
753 | Sneak { .. }
754 | SneakBackward { .. }
755 | Jump { .. }
756 | ZedSearchResult { .. } => MotionKind::Exclusive,
757 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
758 motion.default_kind()
759 }
760 }
761 }
762
763 fn skip_exclusive_special_case(&self) -> bool {
764 match self {
765 Motion::WrappingLeft | Motion::WrappingRight => true,
766 _ => false,
767 }
768 }
769
770 pub(crate) fn push_to_jump_list(&self) -> bool {
771 use Motion::*;
772 match self {
773 CurrentLine
774 | Down { .. }
775 | EndOfLine { .. }
776 | EndOfLineDownward
777 | FindBackward { .. }
778 | FindForward { .. }
779 | FirstNonWhitespace { .. }
780 | GoToColumn
781 | Left
782 | MiddleOfLine { .. }
783 | NextLineStart
784 | NextSubwordEnd { .. }
785 | NextSubwordStart { .. }
786 | NextWordEnd { .. }
787 | NextWordStart { .. }
788 | PreviousLineStart
789 | PreviousSubwordEnd { .. }
790 | PreviousSubwordStart { .. }
791 | PreviousWordEnd { .. }
792 | PreviousWordStart { .. }
793 | RepeatFind { .. }
794 | RepeatFindReversed { .. }
795 | Right
796 | StartOfLine { .. }
797 | StartOfLineDownward
798 | Up { .. }
799 | WrappingLeft
800 | WrappingRight => false,
801 EndOfDocument
802 | EndOfParagraph
803 | GoToPercentage
804 | Jump { .. }
805 | Matching
806 | NextComment
807 | NextGreaterIndent
808 | NextLesserIndent
809 | NextMethodEnd
810 | NextMethodStart
811 | NextSameIndent
812 | NextSectionEnd
813 | NextSectionStart
814 | PreviousComment
815 | PreviousGreaterIndent
816 | PreviousLesserIndent
817 | PreviousMethodEnd
818 | PreviousMethodStart
819 | PreviousSameIndent
820 | PreviousSectionEnd
821 | PreviousSectionStart
822 | SentenceBackward
823 | SentenceForward
824 | Sneak { .. }
825 | SneakBackward { .. }
826 | StartOfDocument
827 | StartOfParagraph
828 | UnmatchedBackward { .. }
829 | UnmatchedForward { .. }
830 | WindowBottom
831 | WindowMiddle
832 | WindowTop
833 | ZedSearchResult { .. } => true,
834 }
835 }
836
837 pub fn infallible(&self) -> bool {
838 use Motion::*;
839 match self {
840 StartOfDocument | EndOfDocument | CurrentLine => true,
841 Down { .. }
842 | Up { .. }
843 | EndOfLine { .. }
844 | MiddleOfLine { .. }
845 | Matching
846 | UnmatchedForward { .. }
847 | UnmatchedBackward { .. }
848 | FindForward { .. }
849 | RepeatFind { .. }
850 | Left
851 | WrappingLeft
852 | Right
853 | WrappingRight
854 | StartOfLine { .. }
855 | StartOfParagraph
856 | EndOfParagraph
857 | SentenceBackward
858 | SentenceForward
859 | StartOfLineDownward
860 | EndOfLineDownward
861 | GoToColumn
862 | GoToPercentage
863 | NextWordStart { .. }
864 | NextWordEnd { .. }
865 | PreviousWordStart { .. }
866 | PreviousWordEnd { .. }
867 | NextSubwordStart { .. }
868 | NextSubwordEnd { .. }
869 | PreviousSubwordStart { .. }
870 | PreviousSubwordEnd { .. }
871 | FirstNonWhitespace { .. }
872 | FindBackward { .. }
873 | Sneak { .. }
874 | SneakBackward { .. }
875 | RepeatFindReversed { .. }
876 | WindowTop
877 | WindowMiddle
878 | WindowBottom
879 | NextLineStart
880 | PreviousLineStart
881 | ZedSearchResult { .. }
882 | NextSectionStart
883 | NextSectionEnd
884 | PreviousSectionStart
885 | PreviousSectionEnd
886 | NextMethodStart
887 | NextMethodEnd
888 | PreviousMethodStart
889 | PreviousMethodEnd
890 | NextComment
891 | PreviousComment
892 | PreviousLesserIndent
893 | PreviousGreaterIndent
894 | PreviousSameIndent
895 | NextLesserIndent
896 | NextGreaterIndent
897 | NextSameIndent
898 | Jump { .. } => false,
899 }
900 }
901
902 pub fn move_point(
903 &self,
904 map: &DisplaySnapshot,
905 point: DisplayPoint,
906 goal: SelectionGoal,
907 maybe_times: Option<usize>,
908 text_layout_details: &TextLayoutDetails,
909 ) -> Option<(DisplayPoint, SelectionGoal)> {
910 let times = maybe_times.unwrap_or(1);
911 use Motion::*;
912 let infallible = self.infallible();
913 let (new_point, goal) = match self {
914 Left => (left(map, point, times), SelectionGoal::None),
915 WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None),
916 Down {
917 display_lines: false,
918 } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
919 Down {
920 display_lines: true,
921 } => down_display(map, point, goal, times, text_layout_details),
922 Up {
923 display_lines: false,
924 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details),
925 Up {
926 display_lines: true,
927 } => up_display(map, point, goal, times, text_layout_details),
928 Right => (right(map, point, times), SelectionGoal::None),
929 WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None),
930 NextWordStart { ignore_punctuation } => (
931 next_word_start(map, point, *ignore_punctuation, times),
932 SelectionGoal::None,
933 ),
934 NextWordEnd { ignore_punctuation } => (
935 next_word_end(map, point, *ignore_punctuation, times, true),
936 SelectionGoal::None,
937 ),
938 PreviousWordStart { ignore_punctuation } => (
939 previous_word_start(map, point, *ignore_punctuation, times),
940 SelectionGoal::None,
941 ),
942 PreviousWordEnd { ignore_punctuation } => (
943 previous_word_end(map, point, *ignore_punctuation, times),
944 SelectionGoal::None,
945 ),
946 NextSubwordStart { ignore_punctuation } => (
947 next_subword_start(map, point, *ignore_punctuation, times),
948 SelectionGoal::None,
949 ),
950 NextSubwordEnd { ignore_punctuation } => (
951 next_subword_end(map, point, *ignore_punctuation, times, true),
952 SelectionGoal::None,
953 ),
954 PreviousSubwordStart { ignore_punctuation } => (
955 previous_subword_start(map, point, *ignore_punctuation, times),
956 SelectionGoal::None,
957 ),
958 PreviousSubwordEnd { ignore_punctuation } => (
959 previous_subword_end(map, point, *ignore_punctuation, times),
960 SelectionGoal::None,
961 ),
962 FirstNonWhitespace { display_lines } => (
963 first_non_whitespace(map, *display_lines, point),
964 SelectionGoal::None,
965 ),
966 StartOfLine { display_lines } => (
967 start_of_line(map, *display_lines, point),
968 SelectionGoal::None,
969 ),
970 MiddleOfLine { display_lines } => (
971 middle_of_line(map, *display_lines, point, maybe_times),
972 SelectionGoal::None,
973 ),
974 EndOfLine { display_lines } => (
975 end_of_line(map, *display_lines, point, times),
976 SelectionGoal::None,
977 ),
978 SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
979 SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
980 StartOfParagraph => (
981 movement::start_of_paragraph(map, point, times),
982 SelectionGoal::None,
983 ),
984 EndOfParagraph => (
985 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
986 SelectionGoal::None,
987 ),
988 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
989 StartOfDocument => (
990 start_of_document(map, point, maybe_times),
991 SelectionGoal::None,
992 ),
993 EndOfDocument => (
994 end_of_document(map, point, maybe_times),
995 SelectionGoal::None,
996 ),
997 Matching => (matching(map, point), SelectionGoal::None),
998 GoToPercentage => (go_to_percentage(map, point, times), SelectionGoal::None),
999 UnmatchedForward { char } => (
1000 unmatched_forward(map, point, *char, times),
1001 SelectionGoal::None,
1002 ),
1003 UnmatchedBackward { char } => (
1004 unmatched_backward(map, point, *char, times),
1005 SelectionGoal::None,
1006 ),
1007 // t f
1008 FindForward {
1009 before,
1010 char,
1011 mode,
1012 smartcase,
1013 } => {
1014 return find_forward(map, point, *before, *char, times, *mode, *smartcase)
1015 .map(|new_point| (new_point, SelectionGoal::None));
1016 }
1017 // T F
1018 FindBackward {
1019 after,
1020 char,
1021 mode,
1022 smartcase,
1023 } => (
1024 find_backward(map, point, *after, *char, times, *mode, *smartcase),
1025 SelectionGoal::None,
1026 ),
1027 Sneak {
1028 first_char,
1029 second_char,
1030 smartcase,
1031 } => {
1032 return sneak(map, point, *first_char, *second_char, times, *smartcase)
1033 .map(|new_point| (new_point, SelectionGoal::None));
1034 }
1035 SneakBackward {
1036 first_char,
1037 second_char,
1038 smartcase,
1039 } => {
1040 return sneak_backward(map, point, *first_char, *second_char, times, *smartcase)
1041 .map(|new_point| (new_point, SelectionGoal::None));
1042 }
1043 // ; -- repeat the last find done with t, f, T, F
1044 RepeatFind { last_find } => match **last_find {
1045 Motion::FindForward {
1046 before,
1047 char,
1048 mode,
1049 smartcase,
1050 } => {
1051 let mut new_point =
1052 find_forward(map, point, before, char, times, mode, smartcase);
1053 if new_point == Some(point) {
1054 new_point =
1055 find_forward(map, point, before, char, times + 1, mode, smartcase);
1056 }
1057
1058 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1059 }
1060
1061 Motion::FindBackward {
1062 after,
1063 char,
1064 mode,
1065 smartcase,
1066 } => {
1067 let mut new_point =
1068 find_backward(map, point, after, char, times, mode, smartcase);
1069 if new_point == point {
1070 new_point =
1071 find_backward(map, point, after, char, times + 1, mode, smartcase);
1072 }
1073
1074 (new_point, SelectionGoal::None)
1075 }
1076 Motion::Sneak {
1077 first_char,
1078 second_char,
1079 smartcase,
1080 } => {
1081 let mut new_point =
1082 sneak(map, point, first_char, second_char, times, smartcase);
1083 if new_point == Some(point) {
1084 new_point =
1085 sneak(map, point, first_char, second_char, times + 1, smartcase);
1086 }
1087
1088 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1089 }
1090
1091 Motion::SneakBackward {
1092 first_char,
1093 second_char,
1094 smartcase,
1095 } => {
1096 let mut new_point =
1097 sneak_backward(map, point, first_char, second_char, times, smartcase);
1098 if new_point == Some(point) {
1099 new_point = sneak_backward(
1100 map,
1101 point,
1102 first_char,
1103 second_char,
1104 times + 1,
1105 smartcase,
1106 );
1107 }
1108
1109 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1110 }
1111 _ => return None,
1112 },
1113 // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction
1114 RepeatFindReversed { last_find } => match **last_find {
1115 Motion::FindForward {
1116 before,
1117 char,
1118 mode,
1119 smartcase,
1120 } => {
1121 let mut new_point =
1122 find_backward(map, point, before, char, times, mode, smartcase);
1123 if new_point == point {
1124 new_point =
1125 find_backward(map, point, before, char, times + 1, mode, smartcase);
1126 }
1127
1128 (new_point, SelectionGoal::None)
1129 }
1130
1131 Motion::FindBackward {
1132 after,
1133 char,
1134 mode,
1135 smartcase,
1136 } => {
1137 let mut new_point =
1138 find_forward(map, point, after, char, times, mode, smartcase);
1139 if new_point == Some(point) {
1140 new_point =
1141 find_forward(map, point, after, char, times + 1, mode, smartcase);
1142 }
1143
1144 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1145 }
1146
1147 Motion::Sneak {
1148 first_char,
1149 second_char,
1150 smartcase,
1151 } => {
1152 let mut new_point =
1153 sneak_backward(map, point, first_char, second_char, times, smartcase);
1154 if new_point == Some(point) {
1155 new_point = sneak_backward(
1156 map,
1157 point,
1158 first_char,
1159 second_char,
1160 times + 1,
1161 smartcase,
1162 );
1163 }
1164
1165 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1166 }
1167
1168 Motion::SneakBackward {
1169 first_char,
1170 second_char,
1171 smartcase,
1172 } => {
1173 let mut new_point =
1174 sneak(map, point, first_char, second_char, times, smartcase);
1175 if new_point == Some(point) {
1176 new_point =
1177 sneak(map, point, first_char, second_char, times + 1, smartcase);
1178 }
1179
1180 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1181 }
1182 _ => return None,
1183 },
1184 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
1185 PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
1186 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
1187 EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
1188 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
1189 WindowTop => window_top(map, point, text_layout_details, times - 1),
1190 WindowMiddle => window_middle(map, point, text_layout_details),
1191 WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
1192 Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
1193 ZedSearchResult { new_selections, .. } => {
1194 // There will be only one selection, as
1195 // Search::SelectNextMatch selects a single match.
1196 if let Some(new_selection) = new_selections.first() {
1197 (
1198 new_selection.start.to_display_point(map),
1199 SelectionGoal::None,
1200 )
1201 } else {
1202 return None;
1203 }
1204 }
1205 NextSectionStart => (
1206 section_motion(map, point, times, Direction::Next, true),
1207 SelectionGoal::None,
1208 ),
1209 NextSectionEnd => (
1210 section_motion(map, point, times, Direction::Next, false),
1211 SelectionGoal::None,
1212 ),
1213 PreviousSectionStart => (
1214 section_motion(map, point, times, Direction::Prev, true),
1215 SelectionGoal::None,
1216 ),
1217 PreviousSectionEnd => (
1218 section_motion(map, point, times, Direction::Prev, false),
1219 SelectionGoal::None,
1220 ),
1221
1222 NextMethodStart => (
1223 method_motion(map, point, times, Direction::Next, true),
1224 SelectionGoal::None,
1225 ),
1226 NextMethodEnd => (
1227 method_motion(map, point, times, Direction::Next, false),
1228 SelectionGoal::None,
1229 ),
1230 PreviousMethodStart => (
1231 method_motion(map, point, times, Direction::Prev, true),
1232 SelectionGoal::None,
1233 ),
1234 PreviousMethodEnd => (
1235 method_motion(map, point, times, Direction::Prev, false),
1236 SelectionGoal::None,
1237 ),
1238 NextComment => (
1239 comment_motion(map, point, times, Direction::Next),
1240 SelectionGoal::None,
1241 ),
1242 PreviousComment => (
1243 comment_motion(map, point, times, Direction::Prev),
1244 SelectionGoal::None,
1245 ),
1246 PreviousLesserIndent => (
1247 indent_motion(map, point, times, Direction::Prev, IndentType::Lesser),
1248 SelectionGoal::None,
1249 ),
1250 PreviousGreaterIndent => (
1251 indent_motion(map, point, times, Direction::Prev, IndentType::Greater),
1252 SelectionGoal::None,
1253 ),
1254 PreviousSameIndent => (
1255 indent_motion(map, point, times, Direction::Prev, IndentType::Same),
1256 SelectionGoal::None,
1257 ),
1258 NextLesserIndent => (
1259 indent_motion(map, point, times, Direction::Next, IndentType::Lesser),
1260 SelectionGoal::None,
1261 ),
1262 NextGreaterIndent => (
1263 indent_motion(map, point, times, Direction::Next, IndentType::Greater),
1264 SelectionGoal::None,
1265 ),
1266 NextSameIndent => (
1267 indent_motion(map, point, times, Direction::Next, IndentType::Same),
1268 SelectionGoal::None,
1269 ),
1270 };
1271 (new_point != point || infallible).then_some((new_point, goal))
1272 }
1273
1274 // Get the range value after self is applied to the specified selection.
1275 pub fn range(
1276 &self,
1277 map: &DisplaySnapshot,
1278 selection: Selection<DisplayPoint>,
1279 times: Option<usize>,
1280 text_layout_details: &TextLayoutDetails,
1281 forced_motion: bool,
1282 ) -> Option<(Range<DisplayPoint>, MotionKind)> {
1283 if let Motion::ZedSearchResult {
1284 prior_selections,
1285 new_selections,
1286 } = self
1287 {
1288 if let Some((prior_selection, new_selection)) =
1289 prior_selections.first().zip(new_selections.first())
1290 {
1291 let start = prior_selection
1292 .start
1293 .to_display_point(map)
1294 .min(new_selection.start.to_display_point(map));
1295 let end = new_selection
1296 .end
1297 .to_display_point(map)
1298 .max(prior_selection.end.to_display_point(map));
1299
1300 if start < end {
1301 return Some((start..end, MotionKind::Exclusive));
1302 } else {
1303 return Some((end..start, MotionKind::Exclusive));
1304 }
1305 } else {
1306 return None;
1307 }
1308 }
1309 let maybe_new_point = self.move_point(
1310 map,
1311 selection.head(),
1312 selection.goal,
1313 times,
1314 text_layout_details,
1315 );
1316
1317 let (new_head, goal) = match (maybe_new_point, forced_motion) {
1318 (Some((p, g)), _) => Some((p, g)),
1319 (None, false) => None,
1320 (None, true) => Some((selection.head(), selection.goal)),
1321 }?;
1322
1323 let mut selection = selection.clone();
1324 selection.set_head(new_head, goal);
1325
1326 let mut kind = match (self.default_kind(), forced_motion) {
1327 (MotionKind::Linewise, true) => MotionKind::Exclusive,
1328 (MotionKind::Exclusive, true) => MotionKind::Inclusive,
1329 (MotionKind::Inclusive, true) => MotionKind::Exclusive,
1330 (kind, false) => kind,
1331 };
1332
1333 if let Motion::NextWordStart {
1334 ignore_punctuation: _,
1335 } = self
1336 {
1337 // Another special case: When using the "w" motion in combination with an
1338 // operator and the last word moved over is at the end of a line, the end of
1339 // that word becomes the end of the operated text, not the first word in the
1340 // next line.
1341 let start = selection.start.to_point(map);
1342 let end = selection.end.to_point(map);
1343 let start_row = MultiBufferRow(selection.start.to_point(map).row);
1344 if end.row > start.row {
1345 selection.end = Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
1346 .to_display_point(map);
1347
1348 // a bit of a hack, we need `cw` on a blank line to not delete the newline,
1349 // but dw on a blank line should. The `Linewise` returned from this method
1350 // causes the `d` operator to include the trailing newline.
1351 if selection.start == selection.end {
1352 return Some((selection.start..selection.end, MotionKind::Linewise));
1353 }
1354 }
1355 } else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() {
1356 let start_point = selection.start.to_point(map);
1357 let mut end_point = selection.end.to_point(map);
1358 let mut next_point = selection.end;
1359 *next_point.column_mut() += 1;
1360 next_point = map.clip_point(next_point, Bias::Right);
1361 if next_point.to_point(map) == end_point && forced_motion {
1362 selection.end = movement::saturating_left(map, selection.end);
1363 }
1364
1365 if end_point.row > start_point.row {
1366 let first_non_blank_of_start_row = map
1367 .line_indent_for_buffer_row(MultiBufferRow(start_point.row))
1368 .raw_len();
1369 // https://github.com/neovim/neovim/blob/ee143aaf65a0e662c42c636aa4a959682858b3e7/src/nvim/ops.c#L6178-L6203
1370 if end_point.column == 0 {
1371 // If the motion is exclusive and the end of the motion is in column 1, the
1372 // end of the motion is moved to the end of the previous line and the motion
1373 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
1374 // but "d}" will not include that line.
1375 //
1376 // If the motion is exclusive, the end of the motion is in column 1 and the
1377 // start of the motion was at or before the first non-blank in the line, the
1378 // motion becomes linewise. Example: If a paragraph begins with some blanks
1379 // and you do "d}" while standing on the first non-blank, all the lines of
1380 // the paragraph are deleted, including the blanks.
1381 if start_point.column <= first_non_blank_of_start_row {
1382 kind = MotionKind::Linewise;
1383 } else {
1384 kind = MotionKind::Inclusive;
1385 }
1386 end_point.row -= 1;
1387 end_point.column = 0;
1388 selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
1389 } else if let Motion::EndOfParagraph = self {
1390 // Special case: When using the "}" motion, it's possible
1391 // that there's no blank lines after the paragraph the
1392 // cursor is currently on.
1393 // In this situation the `end_point.column` value will be
1394 // greater than 0, so the selection doesn't actually end on
1395 // the first character of a blank line. In that case, we'll
1396 // want to move one column to the right, to actually include
1397 // all characters of the last non-blank line.
1398 selection.end = movement::saturating_right(map, selection.end)
1399 }
1400 }
1401 } else if kind == MotionKind::Inclusive {
1402 selection.end = movement::saturating_right(map, selection.end)
1403 }
1404
1405 if kind == MotionKind::Linewise {
1406 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
1407 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
1408 }
1409 Some((selection.start..selection.end, kind))
1410 }
1411
1412 // Expands a selection using self for an operator
1413 pub fn expand_selection(
1414 &self,
1415 map: &DisplaySnapshot,
1416 selection: &mut Selection<DisplayPoint>,
1417 times: Option<usize>,
1418 text_layout_details: &TextLayoutDetails,
1419 forced_motion: bool,
1420 ) -> Option<MotionKind> {
1421 let (range, kind) = self.range(
1422 map,
1423 selection.clone(),
1424 times,
1425 text_layout_details,
1426 forced_motion,
1427 )?;
1428 selection.start = range.start;
1429 selection.end = range.end;
1430 Some(kind)
1431 }
1432}
1433
1434fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1435 for _ in 0..times {
1436 point = movement::saturating_left(map, point);
1437 if point.column() == 0 {
1438 break;
1439 }
1440 }
1441 point
1442}
1443
1444pub(crate) fn wrapping_left(
1445 map: &DisplaySnapshot,
1446 mut point: DisplayPoint,
1447 times: usize,
1448) -> DisplayPoint {
1449 for _ in 0..times {
1450 point = movement::left(map, point);
1451 if point.is_zero() {
1452 break;
1453 }
1454 }
1455 point
1456}
1457
1458fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1459 for _ in 0..times {
1460 point = wrapping_right_single(map, point);
1461 if point == map.max_point() {
1462 break;
1463 }
1464 }
1465 point
1466}
1467
1468fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
1469 let mut next_point = point;
1470 *next_point.column_mut() += 1;
1471 next_point = map.clip_point(next_point, Bias::Right);
1472 if next_point == point {
1473 if next_point.row() == map.max_point().row() {
1474 next_point
1475 } else {
1476 DisplayPoint::new(next_point.row().next_row(), 0)
1477 }
1478 } else {
1479 next_point
1480 }
1481}
1482
1483pub(crate) fn start_of_relative_buffer_row(
1484 map: &DisplaySnapshot,
1485 point: DisplayPoint,
1486 times: isize,
1487) -> DisplayPoint {
1488 let start = map.display_point_to_fold_point(point, Bias::Left);
1489 let target = start.row() as isize + times;
1490 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1491
1492 map.clip_point(
1493 map.fold_point_to_display_point(
1494 map.fold_snapshot
1495 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1496 ),
1497 Bias::Right,
1498 )
1499}
1500
1501fn up_down_buffer_rows(
1502 map: &DisplaySnapshot,
1503 mut point: DisplayPoint,
1504 mut goal: SelectionGoal,
1505 mut times: isize,
1506 text_layout_details: &TextLayoutDetails,
1507) -> (DisplayPoint, SelectionGoal) {
1508 let bias = if times < 0 { Bias::Left } else { Bias::Right };
1509
1510 while map.is_folded_buffer_header(point.row()) {
1511 if times < 0 {
1512 (point, _) = movement::up(map, point, goal, true, text_layout_details);
1513 times += 1;
1514 } else if times > 0 {
1515 (point, _) = movement::down(map, point, goal, true, text_layout_details);
1516 times -= 1;
1517 } else {
1518 break;
1519 }
1520 }
1521
1522 let start = map.display_point_to_fold_point(point, Bias::Left);
1523 let begin_folded_line = map.fold_point_to_display_point(
1524 map.fold_snapshot
1525 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1526 );
1527 let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1528
1529 let (goal_wrap, goal_x) = match goal {
1530 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1531 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1532 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1533 _ => {
1534 let x = map.x_for_display_point(point, text_layout_details);
1535 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1536 (select_nth_wrapped_row, x.0)
1537 }
1538 };
1539
1540 let target = start.row() as isize + times;
1541 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1542
1543 let mut begin_folded_line = map.fold_point_to_display_point(
1544 map.fold_snapshot
1545 .clip_point(FoldPoint::new(new_row, 0), bias),
1546 );
1547
1548 let mut i = 0;
1549 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1550 let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1551 if map
1552 .display_point_to_fold_point(next_folded_line, bias)
1553 .row()
1554 == new_row
1555 {
1556 i += 1;
1557 begin_folded_line = next_folded_line;
1558 } else {
1559 break;
1560 }
1561 }
1562
1563 let new_col = if i == goal_wrap {
1564 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1565 } else {
1566 map.line_len(begin_folded_line.row())
1567 };
1568
1569 (
1570 map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
1571 goal,
1572 )
1573}
1574
1575fn down_display(
1576 map: &DisplaySnapshot,
1577 mut point: DisplayPoint,
1578 mut goal: SelectionGoal,
1579 times: usize,
1580 text_layout_details: &TextLayoutDetails,
1581) -> (DisplayPoint, SelectionGoal) {
1582 for _ in 0..times {
1583 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1584 }
1585
1586 (point, goal)
1587}
1588
1589fn up_display(
1590 map: &DisplaySnapshot,
1591 mut point: DisplayPoint,
1592 mut goal: SelectionGoal,
1593 times: usize,
1594 text_layout_details: &TextLayoutDetails,
1595) -> (DisplayPoint, SelectionGoal) {
1596 for _ in 0..times {
1597 (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1598 }
1599
1600 (point, goal)
1601}
1602
1603pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1604 for _ in 0..times {
1605 let new_point = movement::saturating_right(map, point);
1606 if point == new_point {
1607 break;
1608 }
1609 point = new_point;
1610 }
1611 point
1612}
1613
1614pub(crate) fn next_char(
1615 map: &DisplaySnapshot,
1616 point: DisplayPoint,
1617 allow_cross_newline: bool,
1618) -> DisplayPoint {
1619 let mut new_point = point;
1620 let mut max_column = map.line_len(new_point.row());
1621 if !allow_cross_newline {
1622 max_column -= 1;
1623 }
1624 if new_point.column() < max_column {
1625 *new_point.column_mut() += 1;
1626 } else if new_point < map.max_point() && allow_cross_newline {
1627 *new_point.row_mut() += 1;
1628 *new_point.column_mut() = 0;
1629 }
1630 map.clip_ignoring_line_ends(new_point, Bias::Right)
1631}
1632
1633pub(crate) fn next_word_start(
1634 map: &DisplaySnapshot,
1635 mut point: DisplayPoint,
1636 ignore_punctuation: bool,
1637 times: usize,
1638) -> DisplayPoint {
1639 let classifier = map
1640 .buffer_snapshot
1641 .char_classifier_at(point.to_point(map))
1642 .ignore_punctuation(ignore_punctuation);
1643 for _ in 0..times {
1644 let mut crossed_newline = false;
1645 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1646 let left_kind = classifier.kind(left);
1647 let right_kind = classifier.kind(right);
1648 let at_newline = right == '\n';
1649
1650 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1651 || at_newline && crossed_newline
1652 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1653
1654 crossed_newline |= at_newline;
1655 found
1656 });
1657 if point == new_point {
1658 break;
1659 }
1660 point = new_point;
1661 }
1662 point
1663}
1664
1665pub(crate) fn next_word_end(
1666 map: &DisplaySnapshot,
1667 mut point: DisplayPoint,
1668 ignore_punctuation: bool,
1669 times: usize,
1670 allow_cross_newline: bool,
1671) -> DisplayPoint {
1672 let classifier = map
1673 .buffer_snapshot
1674 .char_classifier_at(point.to_point(map))
1675 .ignore_punctuation(ignore_punctuation);
1676 for _ in 0..times {
1677 let new_point = next_char(map, point, allow_cross_newline);
1678 let mut need_next_char = false;
1679 let new_point = movement::find_boundary_exclusive(
1680 map,
1681 new_point,
1682 FindRange::MultiLine,
1683 |left, right| {
1684 let left_kind = classifier.kind(left);
1685 let right_kind = classifier.kind(right);
1686 let at_newline = right == '\n';
1687
1688 if !allow_cross_newline && at_newline {
1689 need_next_char = true;
1690 return true;
1691 }
1692
1693 left_kind != right_kind && left_kind != CharKind::Whitespace
1694 },
1695 );
1696 let new_point = if need_next_char {
1697 next_char(map, new_point, true)
1698 } else {
1699 new_point
1700 };
1701 let new_point = map.clip_point(new_point, Bias::Left);
1702 if point == new_point {
1703 break;
1704 }
1705 point = new_point;
1706 }
1707 point
1708}
1709
1710fn previous_word_start(
1711 map: &DisplaySnapshot,
1712 mut point: DisplayPoint,
1713 ignore_punctuation: bool,
1714 times: usize,
1715) -> DisplayPoint {
1716 let classifier = map
1717 .buffer_snapshot
1718 .char_classifier_at(point.to_point(map))
1719 .ignore_punctuation(ignore_punctuation);
1720 for _ in 0..times {
1721 // This works even though find_preceding_boundary is called for every character in the line containing
1722 // cursor because the newline is checked only once.
1723 let new_point = movement::find_preceding_boundary_display_point(
1724 map,
1725 point,
1726 FindRange::MultiLine,
1727 |left, right| {
1728 let left_kind = classifier.kind(left);
1729 let right_kind = classifier.kind(right);
1730
1731 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1732 },
1733 );
1734 if point == new_point {
1735 break;
1736 }
1737 point = new_point;
1738 }
1739 point
1740}
1741
1742fn previous_word_end(
1743 map: &DisplaySnapshot,
1744 point: DisplayPoint,
1745 ignore_punctuation: bool,
1746 times: usize,
1747) -> DisplayPoint {
1748 let classifier = map
1749 .buffer_snapshot
1750 .char_classifier_at(point.to_point(map))
1751 .ignore_punctuation(ignore_punctuation);
1752 let mut point = point.to_point(map);
1753
1754 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1755 if let Some(ch) = map.buffer_snapshot.chars_at(point).next() {
1756 point.column += ch.len_utf8() as u32;
1757 }
1758 }
1759 for _ in 0..times {
1760 let new_point = movement::find_preceding_boundary_point(
1761 &map.buffer_snapshot,
1762 point,
1763 FindRange::MultiLine,
1764 |left, right| {
1765 let left_kind = classifier.kind(left);
1766 let right_kind = classifier.kind(right);
1767 match (left_kind, right_kind) {
1768 (CharKind::Punctuation, CharKind::Whitespace)
1769 | (CharKind::Punctuation, CharKind::Word)
1770 | (CharKind::Word, CharKind::Whitespace)
1771 | (CharKind::Word, CharKind::Punctuation) => true,
1772 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1773 _ => false,
1774 }
1775 },
1776 );
1777 if new_point == point {
1778 break;
1779 }
1780 point = new_point;
1781 }
1782 movement::saturating_left(map, point.to_display_point(map))
1783}
1784
1785fn next_subword_start(
1786 map: &DisplaySnapshot,
1787 mut point: DisplayPoint,
1788 ignore_punctuation: bool,
1789 times: usize,
1790) -> DisplayPoint {
1791 let classifier = map
1792 .buffer_snapshot
1793 .char_classifier_at(point.to_point(map))
1794 .ignore_punctuation(ignore_punctuation);
1795 for _ in 0..times {
1796 let mut crossed_newline = false;
1797 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1798 let left_kind = classifier.kind(left);
1799 let right_kind = classifier.kind(right);
1800 let at_newline = right == '\n';
1801
1802 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1803 let is_subword_start =
1804 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1805
1806 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1807 || at_newline && crossed_newline
1808 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1809
1810 crossed_newline |= at_newline;
1811 found
1812 });
1813 if point == new_point {
1814 break;
1815 }
1816 point = new_point;
1817 }
1818 point
1819}
1820
1821pub(crate) fn next_subword_end(
1822 map: &DisplaySnapshot,
1823 mut point: DisplayPoint,
1824 ignore_punctuation: bool,
1825 times: usize,
1826 allow_cross_newline: bool,
1827) -> DisplayPoint {
1828 let classifier = map
1829 .buffer_snapshot
1830 .char_classifier_at(point.to_point(map))
1831 .ignore_punctuation(ignore_punctuation);
1832 for _ in 0..times {
1833 let new_point = next_char(map, point, allow_cross_newline);
1834
1835 let mut crossed_newline = false;
1836 let mut need_backtrack = false;
1837 let new_point =
1838 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1839 let left_kind = classifier.kind(left);
1840 let right_kind = classifier.kind(right);
1841 let at_newline = right == '\n';
1842
1843 if !allow_cross_newline && at_newline {
1844 return true;
1845 }
1846
1847 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1848 let is_subword_end =
1849 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1850
1851 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1852
1853 if found && (is_word_end || is_subword_end) {
1854 need_backtrack = true;
1855 }
1856
1857 crossed_newline |= at_newline;
1858 found
1859 });
1860 let mut new_point = map.clip_point(new_point, Bias::Left);
1861 if need_backtrack {
1862 *new_point.column_mut() -= 1;
1863 }
1864 let new_point = map.clip_point(new_point, Bias::Left);
1865 if point == new_point {
1866 break;
1867 }
1868 point = new_point;
1869 }
1870 point
1871}
1872
1873fn previous_subword_start(
1874 map: &DisplaySnapshot,
1875 mut point: DisplayPoint,
1876 ignore_punctuation: bool,
1877 times: usize,
1878) -> DisplayPoint {
1879 let classifier = map
1880 .buffer_snapshot
1881 .char_classifier_at(point.to_point(map))
1882 .ignore_punctuation(ignore_punctuation);
1883 for _ in 0..times {
1884 let mut crossed_newline = false;
1885 // This works even though find_preceding_boundary is called for every character in the line containing
1886 // cursor because the newline is checked only once.
1887 let new_point = movement::find_preceding_boundary_display_point(
1888 map,
1889 point,
1890 FindRange::MultiLine,
1891 |left, right| {
1892 let left_kind = classifier.kind(left);
1893 let right_kind = classifier.kind(right);
1894 let at_newline = right == '\n';
1895
1896 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1897 let is_subword_start =
1898 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1899
1900 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1901 || at_newline && crossed_newline
1902 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1903
1904 crossed_newline |= at_newline;
1905
1906 found
1907 },
1908 );
1909 if point == new_point {
1910 break;
1911 }
1912 point = new_point;
1913 }
1914 point
1915}
1916
1917fn previous_subword_end(
1918 map: &DisplaySnapshot,
1919 point: DisplayPoint,
1920 ignore_punctuation: bool,
1921 times: usize,
1922) -> DisplayPoint {
1923 let classifier = map
1924 .buffer_snapshot
1925 .char_classifier_at(point.to_point(map))
1926 .ignore_punctuation(ignore_punctuation);
1927 let mut point = point.to_point(map);
1928
1929 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1930 if let Some(ch) = map.buffer_snapshot.chars_at(point).next() {
1931 point.column += ch.len_utf8() as u32;
1932 }
1933 }
1934 for _ in 0..times {
1935 let new_point = movement::find_preceding_boundary_point(
1936 &map.buffer_snapshot,
1937 point,
1938 FindRange::MultiLine,
1939 |left, right| {
1940 let left_kind = classifier.kind(left);
1941 let right_kind = classifier.kind(right);
1942
1943 let is_subword_end =
1944 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1945
1946 if is_subword_end {
1947 return true;
1948 }
1949
1950 match (left_kind, right_kind) {
1951 (CharKind::Word, CharKind::Whitespace)
1952 | (CharKind::Word, CharKind::Punctuation) => true,
1953 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1954 _ => false,
1955 }
1956 },
1957 );
1958 if new_point == point {
1959 break;
1960 }
1961 point = new_point;
1962 }
1963 movement::saturating_left(map, point.to_display_point(map))
1964}
1965
1966pub(crate) fn first_non_whitespace(
1967 map: &DisplaySnapshot,
1968 display_lines: bool,
1969 from: DisplayPoint,
1970) -> DisplayPoint {
1971 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1972 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1973 for (ch, offset) in map.buffer_chars_at(start_offset) {
1974 if ch == '\n' {
1975 return from;
1976 }
1977
1978 start_offset = offset;
1979
1980 if classifier.kind(ch) != CharKind::Whitespace {
1981 break;
1982 }
1983 }
1984
1985 start_offset.to_display_point(map)
1986}
1987
1988pub(crate) fn last_non_whitespace(
1989 map: &DisplaySnapshot,
1990 from: DisplayPoint,
1991 count: usize,
1992) -> DisplayPoint {
1993 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1994 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1995
1996 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1997 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1998 if classifier.kind(ch) != CharKind::Whitespace {
1999 return end_of_line.to_display_point(map);
2000 }
2001 }
2002
2003 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2004 if ch == '\n' {
2005 break;
2006 }
2007 end_of_line = offset;
2008 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2009 break;
2010 }
2011 }
2012
2013 end_of_line.to_display_point(map)
2014}
2015
2016pub(crate) fn start_of_line(
2017 map: &DisplaySnapshot,
2018 display_lines: bool,
2019 point: DisplayPoint,
2020) -> DisplayPoint {
2021 if display_lines {
2022 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2023 } else {
2024 map.prev_line_boundary(point.to_point(map)).1
2025 }
2026}
2027
2028pub(crate) fn middle_of_line(
2029 map: &DisplaySnapshot,
2030 display_lines: bool,
2031 point: DisplayPoint,
2032 times: Option<usize>,
2033) -> DisplayPoint {
2034 let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2035 times as f64 / 100.
2036 } else {
2037 0.5
2038 };
2039 if display_lines {
2040 map.clip_point(
2041 DisplayPoint::new(
2042 point.row(),
2043 (map.line_len(point.row()) as f64 * percent) as u32,
2044 ),
2045 Bias::Left,
2046 )
2047 } else {
2048 let mut buffer_point = point.to_point(map);
2049 buffer_point.column = (map
2050 .buffer_snapshot
2051 .line_len(MultiBufferRow(buffer_point.row)) as f64
2052 * percent) as u32;
2053
2054 map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2055 }
2056}
2057
2058pub(crate) fn end_of_line(
2059 map: &DisplaySnapshot,
2060 display_lines: bool,
2061 mut point: DisplayPoint,
2062 times: usize,
2063) -> DisplayPoint {
2064 if times > 1 {
2065 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2066 }
2067 if display_lines {
2068 map.clip_point(
2069 DisplayPoint::new(point.row(), map.line_len(point.row())),
2070 Bias::Left,
2071 )
2072 } else {
2073 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2074 }
2075}
2076
2077pub(crate) fn sentence_backwards(
2078 map: &DisplaySnapshot,
2079 point: DisplayPoint,
2080 mut times: usize,
2081) -> DisplayPoint {
2082 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
2083 let mut chars = map.reverse_buffer_chars_at(start).peekable();
2084
2085 let mut was_newline = map
2086 .buffer_chars_at(start)
2087 .next()
2088 .is_some_and(|(c, _)| c == '\n');
2089
2090 while let Some((ch, offset)) = chars.next() {
2091 let start_of_next_sentence = if was_newline && ch == '\n' {
2092 Some(offset + ch.len_utf8())
2093 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2094 Some(next_non_blank(map, offset + ch.len_utf8()))
2095 } else if ch == '.' || ch == '?' || ch == '!' {
2096 start_of_next_sentence(map, offset + ch.len_utf8())
2097 } else {
2098 None
2099 };
2100
2101 if let Some(start_of_next_sentence) = start_of_next_sentence {
2102 if start_of_next_sentence < start {
2103 times = times.saturating_sub(1);
2104 }
2105 if times == 0 || offset == 0 {
2106 return map.clip_point(
2107 start_of_next_sentence
2108 .to_offset(&map.buffer_snapshot)
2109 .to_display_point(map),
2110 Bias::Left,
2111 );
2112 }
2113 }
2114 if was_newline {
2115 start = offset;
2116 }
2117 was_newline = ch == '\n';
2118 }
2119
2120 DisplayPoint::zero()
2121}
2122
2123pub(crate) fn sentence_forwards(
2124 map: &DisplaySnapshot,
2125 point: DisplayPoint,
2126 mut times: usize,
2127) -> DisplayPoint {
2128 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
2129 let mut chars = map.buffer_chars_at(start).peekable();
2130
2131 let mut was_newline = map
2132 .reverse_buffer_chars_at(start)
2133 .next()
2134 .is_some_and(|(c, _)| c == '\n')
2135 && chars.peek().is_some_and(|(c, _)| *c == '\n');
2136
2137 while let Some((ch, offset)) = chars.next() {
2138 if was_newline && ch == '\n' {
2139 continue;
2140 }
2141 let start_of_next_sentence = if was_newline {
2142 Some(next_non_blank(map, offset))
2143 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2144 Some(next_non_blank(map, offset + ch.len_utf8()))
2145 } else if ch == '.' || ch == '?' || ch == '!' {
2146 start_of_next_sentence(map, offset + ch.len_utf8())
2147 } else {
2148 None
2149 };
2150
2151 if let Some(start_of_next_sentence) = start_of_next_sentence {
2152 times = times.saturating_sub(1);
2153 if times == 0 {
2154 return map.clip_point(
2155 start_of_next_sentence
2156 .to_offset(&map.buffer_snapshot)
2157 .to_display_point(map),
2158 Bias::Right,
2159 );
2160 }
2161 }
2162
2163 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2164 }
2165
2166 map.max_point()
2167}
2168
2169fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
2170 for (c, o) in map.buffer_chars_at(start) {
2171 if c == '\n' || !c.is_whitespace() {
2172 return o;
2173 }
2174 }
2175
2176 map.buffer_snapshot.len()
2177}
2178
2179// given the offset after a ., !, or ? find the start of the next sentence.
2180// if this is not a sentence boundary, returns None.
2181fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
2182 let chars = map.buffer_chars_at(end_of_sentence);
2183 let mut seen_space = false;
2184
2185 for (char, offset) in chars {
2186 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2187 continue;
2188 }
2189
2190 if char == '\n' && seen_space {
2191 return Some(offset);
2192 } else if char.is_whitespace() {
2193 seen_space = true;
2194 } else if seen_space {
2195 return Some(offset);
2196 } else {
2197 return None;
2198 }
2199 }
2200
2201 Some(map.buffer_snapshot.len())
2202}
2203
2204fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2205 let point = map.display_point_to_point(display_point, Bias::Left);
2206 let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2207 return display_point;
2208 };
2209 let offset = excerpt.buffer().point_to_offset(
2210 excerpt
2211 .buffer()
2212 .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2213 );
2214 let buffer_range = excerpt.buffer_range();
2215 if offset >= buffer_range.start && offset <= buffer_range.end {
2216 let point = map
2217 .buffer_snapshot
2218 .offset_to_point(excerpt.map_offset_from_buffer(offset));
2219 return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2220 }
2221 let mut last_position = None;
2222 for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2223 let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
2224 ..language::ToOffset::to_offset(&range.context.end, &buffer);
2225 if offset >= excerpt_range.start && offset <= excerpt_range.end {
2226 let text_anchor = buffer.anchor_after(offset);
2227 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2228 return anchor.to_display_point(map);
2229 } else if offset <= excerpt_range.start {
2230 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2231 return anchor.to_display_point(map);
2232 } else {
2233 last_position = Some(Anchor::in_buffer(
2234 excerpt,
2235 buffer.remote_id(),
2236 range.context.end,
2237 ));
2238 }
2239 }
2240
2241 let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2242 last_point.column = point.column;
2243
2244 map.clip_point(
2245 map.point_to_display_point(
2246 map.buffer_snapshot.clip_point(point, Bias::Left),
2247 Bias::Left,
2248 ),
2249 Bias::Left,
2250 )
2251}
2252
2253fn start_of_document(
2254 map: &DisplaySnapshot,
2255 display_point: DisplayPoint,
2256 maybe_times: Option<usize>,
2257) -> DisplayPoint {
2258 if let Some(times) = maybe_times {
2259 return go_to_line(map, display_point, times);
2260 }
2261
2262 let point = map.display_point_to_point(display_point, Bias::Left);
2263 let mut first_point = Point::zero();
2264 first_point.column = point.column;
2265
2266 map.clip_point(
2267 map.point_to_display_point(
2268 map.buffer_snapshot.clip_point(first_point, Bias::Left),
2269 Bias::Left,
2270 ),
2271 Bias::Left,
2272 )
2273}
2274
2275fn end_of_document(
2276 map: &DisplaySnapshot,
2277 display_point: DisplayPoint,
2278 maybe_times: Option<usize>,
2279) -> DisplayPoint {
2280 if let Some(times) = maybe_times {
2281 return go_to_line(map, display_point, times);
2282 };
2283 let point = map.display_point_to_point(display_point, Bias::Left);
2284 let mut last_point = map.buffer_snapshot.max_point();
2285 last_point.column = point.column;
2286
2287 map.clip_point(
2288 map.point_to_display_point(
2289 map.buffer_snapshot.clip_point(last_point, Bias::Left),
2290 Bias::Left,
2291 ),
2292 Bias::Left,
2293 )
2294}
2295
2296fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2297 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2298 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2299
2300 if head > outer.start && head < inner.start {
2301 let mut offset = inner.end.to_offset(map, Bias::Left);
2302 for c in map.buffer_snapshot.chars_at(offset) {
2303 if c == '/' || c == '\n' || c == '>' {
2304 return Some(offset.to_display_point(map));
2305 }
2306 offset += c.len_utf8();
2307 }
2308 } else {
2309 let mut offset = outer.start.to_offset(map, Bias::Left);
2310 for c in map.buffer_snapshot.chars_at(offset) {
2311 offset += c.len_utf8();
2312 if c == '<' || c == '\n' {
2313 return Some(offset.to_display_point(map));
2314 }
2315 }
2316 }
2317
2318 return None;
2319}
2320
2321fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2322 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2323 let display_point = map.clip_at_line_end(display_point);
2324 let point = display_point.to_point(map);
2325 let offset = point.to_offset(&map.buffer_snapshot);
2326
2327 // Ensure the range is contained by the current line.
2328 let mut line_end = map.next_line_boundary(point).0;
2329 if line_end == point {
2330 line_end = map.max_point().to_point(map);
2331 }
2332
2333 if let Some((opening_range, closing_range)) = map
2334 .buffer_snapshot
2335 .innermost_enclosing_bracket_ranges(offset..offset, None)
2336 {
2337 if opening_range.contains(&offset) {
2338 return closing_range.start.to_display_point(map);
2339 } else if closing_range.contains(&offset) {
2340 return opening_range.start.to_display_point(map);
2341 }
2342 }
2343
2344 let line_range = map.prev_line_boundary(point).0..line_end;
2345 let visible_line_range =
2346 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2347 let ranges = map
2348 .buffer_snapshot
2349 .bracket_ranges(visible_line_range.clone());
2350 if let Some(ranges) = ranges {
2351 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2352 ..line_range.end.to_offset(&map.buffer_snapshot);
2353 let mut closest_pair_destination = None;
2354 let mut closest_distance = usize::MAX;
2355
2356 for (open_range, close_range) in ranges {
2357 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2358 if offset > open_range.start && offset < close_range.start {
2359 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2360 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2361 return display_point;
2362 }
2363 if let Some(tag) = matching_tag(map, display_point) {
2364 return tag;
2365 }
2366 } else if close_range.contains(&offset) {
2367 return open_range.start.to_display_point(map);
2368 } else if open_range.contains(&offset) {
2369 return (close_range.end - 1).to_display_point(map);
2370 }
2371 }
2372
2373 if (open_range.contains(&offset) || open_range.start >= offset)
2374 && line_range.contains(&open_range.start)
2375 {
2376 let distance = open_range.start.saturating_sub(offset);
2377 if distance < closest_distance {
2378 closest_pair_destination = Some(close_range.start);
2379 closest_distance = distance;
2380 continue;
2381 }
2382 }
2383
2384 if (close_range.contains(&offset) || close_range.start >= offset)
2385 && line_range.contains(&close_range.start)
2386 {
2387 let distance = close_range.start.saturating_sub(offset);
2388 if distance < closest_distance {
2389 closest_pair_destination = Some(open_range.start);
2390 closest_distance = distance;
2391 continue;
2392 }
2393 }
2394
2395 continue;
2396 }
2397
2398 closest_pair_destination
2399 .map(|destination| destination.to_display_point(map))
2400 .unwrap_or(display_point)
2401 } else {
2402 display_point
2403 }
2404}
2405
2406// Go to {count} percentage in the file, on the first
2407// non-blank in the line linewise. To compute the new
2408// line number this formula is used:
2409// ({count} * number-of-lines + 99) / 100
2410//
2411// https://neovim.io/doc/user/motion.html#N%25
2412fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2413 let total_lines = map.buffer_snapshot.max_point().row + 1;
2414 let target_line = (count * total_lines as usize).div_ceil(100);
2415 let target_point = DisplayPoint::new(
2416 DisplayRow(target_line.saturating_sub(1) as u32),
2417 point.column(),
2418 );
2419 map.clip_point(target_point, Bias::Left)
2420}
2421
2422fn unmatched_forward(
2423 map: &DisplaySnapshot,
2424 mut display_point: DisplayPoint,
2425 char: char,
2426 times: usize,
2427) -> DisplayPoint {
2428 for _ in 0..times {
2429 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2430 let point = display_point.to_point(map);
2431 let offset = point.to_offset(&map.buffer_snapshot);
2432
2433 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2434 let Some(ranges) = ranges else { break };
2435 let mut closest_closing_destination = None;
2436 let mut closest_distance = usize::MAX;
2437
2438 for (_, close_range) in ranges {
2439 if close_range.start > offset {
2440 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2441 if Some(char) == chars.next() {
2442 let distance = close_range.start - offset;
2443 if distance < closest_distance {
2444 closest_closing_destination = Some(close_range.start);
2445 closest_distance = distance;
2446 continue;
2447 }
2448 }
2449 }
2450 }
2451
2452 let new_point = closest_closing_destination
2453 .map(|destination| destination.to_display_point(map))
2454 .unwrap_or(display_point);
2455 if new_point == display_point {
2456 break;
2457 }
2458 display_point = new_point;
2459 }
2460 return display_point;
2461}
2462
2463fn unmatched_backward(
2464 map: &DisplaySnapshot,
2465 mut display_point: DisplayPoint,
2466 char: char,
2467 times: usize,
2468) -> DisplayPoint {
2469 for _ in 0..times {
2470 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2471 let point = display_point.to_point(map);
2472 let offset = point.to_offset(&map.buffer_snapshot);
2473
2474 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2475 let Some(ranges) = ranges else {
2476 break;
2477 };
2478
2479 let mut closest_starting_destination = None;
2480 let mut closest_distance = usize::MAX;
2481
2482 for (start_range, _) in ranges {
2483 if start_range.start < offset {
2484 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2485 if Some(char) == chars.next() {
2486 let distance = offset - start_range.start;
2487 if distance < closest_distance {
2488 closest_starting_destination = Some(start_range.start);
2489 closest_distance = distance;
2490 continue;
2491 }
2492 }
2493 }
2494 }
2495
2496 let new_point = closest_starting_destination
2497 .map(|destination| destination.to_display_point(map))
2498 .unwrap_or(display_point);
2499 if new_point == display_point {
2500 break;
2501 } else {
2502 display_point = new_point;
2503 }
2504 }
2505 display_point
2506}
2507
2508fn find_forward(
2509 map: &DisplaySnapshot,
2510 from: DisplayPoint,
2511 before: bool,
2512 target: char,
2513 times: usize,
2514 mode: FindRange,
2515 smartcase: bool,
2516) -> Option<DisplayPoint> {
2517 let mut to = from;
2518 let mut found = false;
2519
2520 for _ in 0..times {
2521 found = false;
2522 let new_to = find_boundary(map, to, mode, |_, right| {
2523 found = is_character_match(target, right, smartcase);
2524 found
2525 });
2526 if to == new_to {
2527 break;
2528 }
2529 to = new_to;
2530 }
2531
2532 if found {
2533 if before && to.column() > 0 {
2534 *to.column_mut() -= 1;
2535 Some(map.clip_point(to, Bias::Left))
2536 } else if before && to.row().0 > 0 {
2537 *to.row_mut() -= 1;
2538 *to.column_mut() = map.line(to.row()).len() as u32;
2539 Some(map.clip_point(to, Bias::Left))
2540 } else {
2541 Some(to)
2542 }
2543 } else {
2544 None
2545 }
2546}
2547
2548fn find_backward(
2549 map: &DisplaySnapshot,
2550 from: DisplayPoint,
2551 after: bool,
2552 target: char,
2553 times: usize,
2554 mode: FindRange,
2555 smartcase: bool,
2556) -> DisplayPoint {
2557 let mut to = from;
2558
2559 for _ in 0..times {
2560 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2561 is_character_match(target, right, smartcase)
2562 });
2563 if to == new_to {
2564 break;
2565 }
2566 to = new_to;
2567 }
2568
2569 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2570 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2571 if after {
2572 *to.column_mut() += 1;
2573 map.clip_point(to, Bias::Right)
2574 } else {
2575 to
2576 }
2577 } else {
2578 from
2579 }
2580}
2581
2582fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2583 if smartcase {
2584 if target.is_uppercase() {
2585 target == other
2586 } else {
2587 target == other.to_ascii_lowercase()
2588 }
2589 } else {
2590 target == other
2591 }
2592}
2593
2594fn sneak(
2595 map: &DisplaySnapshot,
2596 from: DisplayPoint,
2597 first_target: char,
2598 second_target: char,
2599 times: usize,
2600 smartcase: bool,
2601) -> Option<DisplayPoint> {
2602 let mut to = from;
2603 let mut found = false;
2604
2605 for _ in 0..times {
2606 found = false;
2607 let new_to = find_boundary(
2608 map,
2609 movement::right(map, to),
2610 FindRange::MultiLine,
2611 |left, right| {
2612 found = is_character_match(first_target, left, smartcase)
2613 && is_character_match(second_target, right, smartcase);
2614 found
2615 },
2616 );
2617 if to == new_to {
2618 break;
2619 }
2620 to = new_to;
2621 }
2622
2623 if found {
2624 Some(movement::left(map, to))
2625 } else {
2626 None
2627 }
2628}
2629
2630fn sneak_backward(
2631 map: &DisplaySnapshot,
2632 from: DisplayPoint,
2633 first_target: char,
2634 second_target: char,
2635 times: usize,
2636 smartcase: bool,
2637) -> Option<DisplayPoint> {
2638 let mut to = from;
2639 let mut found = false;
2640
2641 for _ in 0..times {
2642 found = false;
2643 let new_to =
2644 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2645 found = is_character_match(first_target, left, smartcase)
2646 && is_character_match(second_target, right, smartcase);
2647 found
2648 });
2649 if to == new_to {
2650 break;
2651 }
2652 to = new_to;
2653 }
2654
2655 if found {
2656 Some(movement::left(map, to))
2657 } else {
2658 None
2659 }
2660}
2661
2662fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2663 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2664 first_non_whitespace(map, false, correct_line)
2665}
2666
2667fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2668 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2669 first_non_whitespace(map, false, correct_line)
2670}
2671
2672fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2673 let correct_line = start_of_relative_buffer_row(map, point, 0);
2674 right(map, correct_line, times.saturating_sub(1))
2675}
2676
2677pub(crate) fn next_line_end(
2678 map: &DisplaySnapshot,
2679 mut point: DisplayPoint,
2680 times: usize,
2681) -> DisplayPoint {
2682 if times > 1 {
2683 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2684 }
2685 end_of_line(map, false, point, 1)
2686}
2687
2688fn window_top(
2689 map: &DisplaySnapshot,
2690 point: DisplayPoint,
2691 text_layout_details: &TextLayoutDetails,
2692 mut times: usize,
2693) -> (DisplayPoint, SelectionGoal) {
2694 let first_visible_line = text_layout_details
2695 .scroll_anchor
2696 .anchor
2697 .to_display_point(map);
2698
2699 if first_visible_line.row() != DisplayRow(0)
2700 && text_layout_details.vertical_scroll_margin as usize > times
2701 {
2702 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2703 }
2704
2705 if let Some(visible_rows) = text_layout_details.visible_rows {
2706 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2707 let new_row = (first_visible_line.row().0 + (times as u32))
2708 .min(bottom_row)
2709 .min(map.max_point().row().0);
2710 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2711
2712 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2713 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2714 } else {
2715 let new_row =
2716 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2717 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2718
2719 let new_point = DisplayPoint::new(new_row, new_col);
2720 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2721 }
2722}
2723
2724fn window_middle(
2725 map: &DisplaySnapshot,
2726 point: DisplayPoint,
2727 text_layout_details: &TextLayoutDetails,
2728) -> (DisplayPoint, SelectionGoal) {
2729 if let Some(visible_rows) = text_layout_details.visible_rows {
2730 let first_visible_line = text_layout_details
2731 .scroll_anchor
2732 .anchor
2733 .to_display_point(map);
2734
2735 let max_visible_rows =
2736 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2737
2738 let new_row =
2739 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2740 let new_row = DisplayRow(new_row);
2741 let new_col = point.column().min(map.line_len(new_row));
2742 let new_point = DisplayPoint::new(new_row, new_col);
2743 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2744 } else {
2745 (point, SelectionGoal::None)
2746 }
2747}
2748
2749fn window_bottom(
2750 map: &DisplaySnapshot,
2751 point: DisplayPoint,
2752 text_layout_details: &TextLayoutDetails,
2753 mut times: usize,
2754) -> (DisplayPoint, SelectionGoal) {
2755 if let Some(visible_rows) = text_layout_details.visible_rows {
2756 let first_visible_line = text_layout_details
2757 .scroll_anchor
2758 .anchor
2759 .to_display_point(map);
2760 let bottom_row = first_visible_line.row().0
2761 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2762 if bottom_row < map.max_point().row().0
2763 && text_layout_details.vertical_scroll_margin as usize > times
2764 {
2765 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2766 }
2767 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2768 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2769 {
2770 first_visible_line.row()
2771 } else {
2772 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2773 };
2774 let new_col = point.column().min(map.line_len(new_row));
2775 let new_point = DisplayPoint::new(new_row, new_col);
2776 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2777 } else {
2778 (point, SelectionGoal::None)
2779 }
2780}
2781
2782fn method_motion(
2783 map: &DisplaySnapshot,
2784 mut display_point: DisplayPoint,
2785 times: usize,
2786 direction: Direction,
2787 is_start: bool,
2788) -> DisplayPoint {
2789 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2790 return display_point;
2791 };
2792
2793 for _ in 0..times {
2794 let point = map.display_point_to_point(display_point, Bias::Left);
2795 let offset = point.to_offset(&map.buffer_snapshot);
2796 let range = if direction == Direction::Prev {
2797 0..offset
2798 } else {
2799 offset..buffer.len()
2800 };
2801
2802 let possibilities = buffer
2803 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2804 .filter_map(|(range, object)| {
2805 if !matches!(object, language::TextObject::AroundFunction) {
2806 return None;
2807 }
2808
2809 let relevant = if is_start { range.start } else { range.end };
2810 if direction == Direction::Prev && relevant < offset {
2811 Some(relevant)
2812 } else if direction == Direction::Next && relevant > offset + 1 {
2813 Some(relevant)
2814 } else {
2815 None
2816 }
2817 });
2818
2819 let dest = if direction == Direction::Prev {
2820 possibilities.max().unwrap_or(offset)
2821 } else {
2822 possibilities.min().unwrap_or(offset)
2823 };
2824 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2825 if new_point == display_point {
2826 break;
2827 }
2828 display_point = new_point;
2829 }
2830 display_point
2831}
2832
2833fn comment_motion(
2834 map: &DisplaySnapshot,
2835 mut display_point: DisplayPoint,
2836 times: usize,
2837 direction: Direction,
2838) -> DisplayPoint {
2839 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2840 return display_point;
2841 };
2842
2843 for _ in 0..times {
2844 let point = map.display_point_to_point(display_point, Bias::Left);
2845 let offset = point.to_offset(&map.buffer_snapshot);
2846 let range = if direction == Direction::Prev {
2847 0..offset
2848 } else {
2849 offset..buffer.len()
2850 };
2851
2852 let possibilities = buffer
2853 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2854 .filter_map(|(range, object)| {
2855 if !matches!(object, language::TextObject::AroundComment) {
2856 return None;
2857 }
2858
2859 let relevant = if direction == Direction::Prev {
2860 range.start
2861 } else {
2862 range.end
2863 };
2864 if direction == Direction::Prev && relevant < offset {
2865 Some(relevant)
2866 } else if direction == Direction::Next && relevant > offset + 1 {
2867 Some(relevant)
2868 } else {
2869 None
2870 }
2871 });
2872
2873 let dest = if direction == Direction::Prev {
2874 possibilities.max().unwrap_or(offset)
2875 } else {
2876 possibilities.min().unwrap_or(offset)
2877 };
2878 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2879 if new_point == display_point {
2880 break;
2881 }
2882 display_point = new_point;
2883 }
2884
2885 display_point
2886}
2887
2888fn section_motion(
2889 map: &DisplaySnapshot,
2890 mut display_point: DisplayPoint,
2891 times: usize,
2892 direction: Direction,
2893 is_start: bool,
2894) -> DisplayPoint {
2895 if map.buffer_snapshot.as_singleton().is_some() {
2896 for _ in 0..times {
2897 let offset = map
2898 .display_point_to_point(display_point, Bias::Left)
2899 .to_offset(&map.buffer_snapshot);
2900 let range = if direction == Direction::Prev {
2901 0..offset
2902 } else {
2903 offset..map.buffer_snapshot.len()
2904 };
2905
2906 // we set a max start depth here because we want a section to only be "top level"
2907 // similar to vim's default of '{' in the first column.
2908 // (and without it, ]] at the start of editor.rs is -very- slow)
2909 let mut possibilities = map
2910 .buffer_snapshot
2911 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2912 .filter(|(_, object)| {
2913 matches!(
2914 object,
2915 language::TextObject::AroundClass | language::TextObject::AroundFunction
2916 )
2917 })
2918 .collect::<Vec<_>>();
2919 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2920 let mut prev_end = None;
2921 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2922 if t == language::TextObject::AroundFunction
2923 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2924 {
2925 return None;
2926 }
2927 prev_end = Some(range.end);
2928
2929 let relevant = if is_start { range.start } else { range.end };
2930 if direction == Direction::Prev && relevant < offset {
2931 Some(relevant)
2932 } else if direction == Direction::Next && relevant > offset + 1 {
2933 Some(relevant)
2934 } else {
2935 None
2936 }
2937 });
2938
2939 let offset = if direction == Direction::Prev {
2940 possibilities.max().unwrap_or(0)
2941 } else {
2942 possibilities.min().unwrap_or(map.buffer_snapshot.len())
2943 };
2944
2945 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2946 if new_point == display_point {
2947 break;
2948 }
2949 display_point = new_point;
2950 }
2951 return display_point;
2952 };
2953
2954 for _ in 0..times {
2955 let next_point = if is_start {
2956 movement::start_of_excerpt(map, display_point, direction)
2957 } else {
2958 movement::end_of_excerpt(map, display_point, direction)
2959 };
2960 if next_point == display_point {
2961 break;
2962 }
2963 display_point = next_point;
2964 }
2965
2966 display_point
2967}
2968
2969fn matches_indent_type(
2970 target_indent: &text::LineIndent,
2971 current_indent: &text::LineIndent,
2972 indent_type: IndentType,
2973) -> bool {
2974 match indent_type {
2975 IndentType::Lesser => {
2976 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
2977 }
2978 IndentType::Greater => {
2979 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
2980 }
2981 IndentType::Same => {
2982 target_indent.spaces == current_indent.spaces
2983 && target_indent.tabs == current_indent.tabs
2984 }
2985 }
2986}
2987
2988fn indent_motion(
2989 map: &DisplaySnapshot,
2990 mut display_point: DisplayPoint,
2991 times: usize,
2992 direction: Direction,
2993 indent_type: IndentType,
2994) -> DisplayPoint {
2995 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
2996 let current_row = MultiBufferRow(buffer_point.row);
2997 let current_indent = map.line_indent_for_buffer_row(current_row);
2998 if current_indent.is_line_empty() {
2999 return display_point;
3000 }
3001 let max_row = map.max_point().to_point(map).row;
3002
3003 for _ in 0..times {
3004 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3005
3006 let target_row = match direction {
3007 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3008 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3009 !indent.is_line_empty()
3010 && matches_indent_type(&indent, ¤t_indent, indent_type)
3011 }),
3012 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3013 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3014 !indent.is_line_empty()
3015 && matches_indent_type(&indent, ¤t_indent, indent_type)
3016 }),
3017 }
3018 .unwrap_or(current_buffer_row);
3019
3020 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3021 let new_point = first_non_whitespace(map, false, new_point);
3022 if new_point == display_point {
3023 break;
3024 }
3025 display_point = new_point;
3026 }
3027 display_point
3028}
3029
3030#[cfg(test)]
3031mod test {
3032
3033 use crate::{
3034 state::Mode,
3035 test::{NeovimBackedTestContext, VimTestContext},
3036 };
3037 use editor::display_map::Inlay;
3038 use indoc::indoc;
3039 use language::Point;
3040 use multi_buffer::MultiBufferRow;
3041
3042 #[gpui::test]
3043 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3044 let mut cx = NeovimBackedTestContext::new(cx).await;
3045
3046 let initial_state = indoc! {r"ˇabc
3047 def
3048
3049 paragraph
3050 the second
3051
3052
3053
3054 third and
3055 final"};
3056
3057 // goes down once
3058 cx.set_shared_state(initial_state).await;
3059 cx.simulate_shared_keystrokes("}").await;
3060 cx.shared_state().await.assert_eq(indoc! {r"abc
3061 def
3062 ˇ
3063 paragraph
3064 the second
3065
3066
3067
3068 third and
3069 final"});
3070
3071 // goes up once
3072 cx.simulate_shared_keystrokes("{").await;
3073 cx.shared_state().await.assert_eq(initial_state);
3074
3075 // goes down twice
3076 cx.simulate_shared_keystrokes("2 }").await;
3077 cx.shared_state().await.assert_eq(indoc! {r"abc
3078 def
3079
3080 paragraph
3081 the second
3082 ˇ
3083
3084
3085 third and
3086 final"});
3087
3088 // goes down over multiple blanks
3089 cx.simulate_shared_keystrokes("}").await;
3090 cx.shared_state().await.assert_eq(indoc! {r"abc
3091 def
3092
3093 paragraph
3094 the second
3095
3096
3097
3098 third and
3099 finaˇl"});
3100
3101 // goes up twice
3102 cx.simulate_shared_keystrokes("2 {").await;
3103 cx.shared_state().await.assert_eq(indoc! {r"abc
3104 def
3105 ˇ
3106 paragraph
3107 the second
3108
3109
3110
3111 third and
3112 final"});
3113 }
3114
3115 #[gpui::test]
3116 async fn test_matching(cx: &mut gpui::TestAppContext) {
3117 let mut cx = NeovimBackedTestContext::new(cx).await;
3118
3119 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3120 do(something(with<Types>.and_arrays[0, 2]))
3121 }"})
3122 .await;
3123 cx.simulate_shared_keystrokes("%").await;
3124 cx.shared_state()
3125 .await
3126 .assert_eq(indoc! {r"func (a stringˇ) {
3127 do(something(with<Types>.and_arrays[0, 2]))
3128 }"});
3129
3130 // test it works on the last character of the line
3131 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3132 do(something(with<Types>.and_arrays[0, 2]))
3133 }"})
3134 .await;
3135 cx.simulate_shared_keystrokes("%").await;
3136 cx.shared_state()
3137 .await
3138 .assert_eq(indoc! {r"func (a string) {
3139 do(something(with<Types>.and_arrays[0, 2]))
3140 ˇ}"});
3141
3142 // test it works on immediate nesting
3143 cx.set_shared_state("ˇ{()}").await;
3144 cx.simulate_shared_keystrokes("%").await;
3145 cx.shared_state().await.assert_eq("{()ˇ}");
3146 cx.simulate_shared_keystrokes("%").await;
3147 cx.shared_state().await.assert_eq("ˇ{()}");
3148
3149 // test it works on immediate nesting inside braces
3150 cx.set_shared_state("{\n ˇ{()}\n}").await;
3151 cx.simulate_shared_keystrokes("%").await;
3152 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3153
3154 // test it jumps to the next paren on a line
3155 cx.set_shared_state("func ˇboop() {\n}").await;
3156 cx.simulate_shared_keystrokes("%").await;
3157 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3158 }
3159
3160 #[gpui::test]
3161 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3162 let mut cx = NeovimBackedTestContext::new(cx).await;
3163
3164 // test it works with curly braces
3165 cx.set_shared_state(indoc! {r"func (a string) {
3166 do(something(with<Types>.anˇd_arrays[0, 2]))
3167 }"})
3168 .await;
3169 cx.simulate_shared_keystrokes("] }").await;
3170 cx.shared_state()
3171 .await
3172 .assert_eq(indoc! {r"func (a string) {
3173 do(something(with<Types>.and_arrays[0, 2]))
3174 ˇ}"});
3175
3176 // test it works with brackets
3177 cx.set_shared_state(indoc! {r"func (a string) {
3178 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3179 }"})
3180 .await;
3181 cx.simulate_shared_keystrokes("] )").await;
3182 cx.shared_state()
3183 .await
3184 .assert_eq(indoc! {r"func (a string) {
3185 do(something(with<Types>.and_arrays[0, 2])ˇ)
3186 }"});
3187
3188 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3189 .await;
3190 cx.simulate_shared_keystrokes("] )").await;
3191 cx.shared_state()
3192 .await
3193 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3194
3195 // test it works on immediate nesting
3196 cx.set_shared_state("{ˇ {}{}}").await;
3197 cx.simulate_shared_keystrokes("] }").await;
3198 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3199 cx.set_shared_state("(ˇ ()())").await;
3200 cx.simulate_shared_keystrokes("] )").await;
3201 cx.shared_state().await.assert_eq("( ()()ˇ)");
3202
3203 // test it works on immediate nesting inside braces
3204 cx.set_shared_state("{\n ˇ {()}\n}").await;
3205 cx.simulate_shared_keystrokes("] }").await;
3206 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3207 cx.set_shared_state("(\n ˇ {()}\n)").await;
3208 cx.simulate_shared_keystrokes("] )").await;
3209 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3210 }
3211
3212 #[gpui::test]
3213 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3214 let mut cx = NeovimBackedTestContext::new(cx).await;
3215
3216 // test it works with curly braces
3217 cx.set_shared_state(indoc! {r"func (a string) {
3218 do(something(with<Types>.anˇd_arrays[0, 2]))
3219 }"})
3220 .await;
3221 cx.simulate_shared_keystrokes("[ {").await;
3222 cx.shared_state()
3223 .await
3224 .assert_eq(indoc! {r"func (a string) ˇ{
3225 do(something(with<Types>.and_arrays[0, 2]))
3226 }"});
3227
3228 // test it works with brackets
3229 cx.set_shared_state(indoc! {r"func (a string) {
3230 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3231 }"})
3232 .await;
3233 cx.simulate_shared_keystrokes("[ (").await;
3234 cx.shared_state()
3235 .await
3236 .assert_eq(indoc! {r"func (a string) {
3237 doˇ(something(with<Types>.and_arrays[0, 2]))
3238 }"});
3239
3240 // test it works on immediate nesting
3241 cx.set_shared_state("{{}{} ˇ }").await;
3242 cx.simulate_shared_keystrokes("[ {").await;
3243 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3244 cx.set_shared_state("(()() ˇ )").await;
3245 cx.simulate_shared_keystrokes("[ (").await;
3246 cx.shared_state().await.assert_eq("ˇ(()() )");
3247
3248 // test it works on immediate nesting inside braces
3249 cx.set_shared_state("{\n {()} ˇ\n}").await;
3250 cx.simulate_shared_keystrokes("[ {").await;
3251 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3252 cx.set_shared_state("(\n {()} ˇ\n)").await;
3253 cx.simulate_shared_keystrokes("[ (").await;
3254 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3255 }
3256
3257 #[gpui::test]
3258 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3259 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3260
3261 cx.neovim.exec("set filetype=html").await;
3262
3263 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3264 cx.simulate_shared_keystrokes("%").await;
3265 cx.shared_state()
3266 .await
3267 .assert_eq(indoc! {r"<body><ˇ/body>"});
3268 cx.simulate_shared_keystrokes("%").await;
3269
3270 // test jumping backwards
3271 cx.shared_state()
3272 .await
3273 .assert_eq(indoc! {r"<ˇbody></body>"});
3274
3275 // test self-closing tags
3276 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3277 cx.simulate_shared_keystrokes("%").await;
3278 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3279
3280 // test tag with attributes
3281 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3282 </div>
3283 "})
3284 .await;
3285 cx.simulate_shared_keystrokes("%").await;
3286 cx.shared_state()
3287 .await
3288 .assert_eq(indoc! {r"<div class='test' id='main'>
3289 <ˇ/div>
3290 "});
3291
3292 // test multi-line self-closing tag
3293 cx.set_shared_state(indoc! {r#"<a>
3294 <br
3295 test = "test"
3296 /ˇ>
3297 </a>"#})
3298 .await;
3299 cx.simulate_shared_keystrokes("%").await;
3300 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3301 ˇ<br
3302 test = "test"
3303 />
3304 </a>"#});
3305 }
3306
3307 #[gpui::test]
3308 async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3309 let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3310
3311 // test brackets within tags
3312 cx.set_shared_state(indoc! {r"function f() {
3313 return (
3314 <div rules={ˇ[{ a: 1 }]}>
3315 <h1>test</h1>
3316 </div>
3317 );
3318 }"})
3319 .await;
3320 cx.simulate_shared_keystrokes("%").await;
3321 cx.shared_state().await.assert_eq(indoc! {r"function f() {
3322 return (
3323 <div rules={[{ a: 1 }ˇ]}>
3324 <h1>test</h1>
3325 </div>
3326 );
3327 }"});
3328 }
3329
3330 #[gpui::test]
3331 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3332 let mut cx = NeovimBackedTestContext::new(cx).await;
3333
3334 // f and F
3335 cx.set_shared_state("ˇone two three four").await;
3336 cx.simulate_shared_keystrokes("f o").await;
3337 cx.shared_state().await.assert_eq("one twˇo three four");
3338 cx.simulate_shared_keystrokes(",").await;
3339 cx.shared_state().await.assert_eq("ˇone two three four");
3340 cx.simulate_shared_keystrokes("2 ;").await;
3341 cx.shared_state().await.assert_eq("one two three fˇour");
3342 cx.simulate_shared_keystrokes("shift-f e").await;
3343 cx.shared_state().await.assert_eq("one two threˇe four");
3344 cx.simulate_shared_keystrokes("2 ;").await;
3345 cx.shared_state().await.assert_eq("onˇe two three four");
3346 cx.simulate_shared_keystrokes(",").await;
3347 cx.shared_state().await.assert_eq("one two thrˇee four");
3348
3349 // t and T
3350 cx.set_shared_state("ˇone two three four").await;
3351 cx.simulate_shared_keystrokes("t o").await;
3352 cx.shared_state().await.assert_eq("one tˇwo three four");
3353 cx.simulate_shared_keystrokes(",").await;
3354 cx.shared_state().await.assert_eq("oˇne two three four");
3355 cx.simulate_shared_keystrokes("2 ;").await;
3356 cx.shared_state().await.assert_eq("one two three ˇfour");
3357 cx.simulate_shared_keystrokes("shift-t e").await;
3358 cx.shared_state().await.assert_eq("one two threeˇ four");
3359 cx.simulate_shared_keystrokes("3 ;").await;
3360 cx.shared_state().await.assert_eq("oneˇ two three four");
3361 cx.simulate_shared_keystrokes(",").await;
3362 cx.shared_state().await.assert_eq("one two thˇree four");
3363 }
3364
3365 #[gpui::test]
3366 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3367 let mut cx = NeovimBackedTestContext::new(cx).await;
3368 let initial_state = indoc! {r"something(ˇfoo)"};
3369 cx.set_shared_state(initial_state).await;
3370 cx.simulate_shared_keystrokes("}").await;
3371 cx.shared_state().await.assert_eq("something(fooˇ)");
3372 }
3373
3374 #[gpui::test]
3375 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3376 let mut cx = NeovimBackedTestContext::new(cx).await;
3377 cx.set_shared_state("ˇone\n two\nthree").await;
3378 cx.simulate_shared_keystrokes("enter").await;
3379 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3380 }
3381
3382 #[gpui::test]
3383 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3384 let mut cx = NeovimBackedTestContext::new(cx).await;
3385 cx.set_shared_state("ˇ one\n two \nthree").await;
3386 cx.simulate_shared_keystrokes("g _").await;
3387 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3388
3389 cx.set_shared_state("ˇ one \n two \nthree").await;
3390 cx.simulate_shared_keystrokes("g _").await;
3391 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3392 cx.simulate_shared_keystrokes("2 g _").await;
3393 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3394 }
3395
3396 #[gpui::test]
3397 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3398 let mut cx = NeovimBackedTestContext::new(cx).await;
3399 let initial_state = indoc! {r"abc
3400 def
3401 paragraph
3402 the second
3403 third ˇand
3404 final"};
3405
3406 cx.set_shared_state(initial_state).await;
3407 cx.simulate_shared_keystrokes("shift-h").await;
3408 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3409 def
3410 paragraph
3411 the second
3412 third and
3413 final"});
3414
3415 // clip point
3416 cx.set_shared_state(indoc! {r"
3417 1 2 3
3418 4 5 6
3419 7 8 ˇ9
3420 "})
3421 .await;
3422 cx.simulate_shared_keystrokes("shift-h").await;
3423 cx.shared_state().await.assert_eq(indoc! {"
3424 1 2 ˇ3
3425 4 5 6
3426 7 8 9
3427 "});
3428
3429 cx.set_shared_state(indoc! {r"
3430 1 2 3
3431 4 5 6
3432 ˇ7 8 9
3433 "})
3434 .await;
3435 cx.simulate_shared_keystrokes("shift-h").await;
3436 cx.shared_state().await.assert_eq(indoc! {"
3437 ˇ1 2 3
3438 4 5 6
3439 7 8 9
3440 "});
3441
3442 cx.set_shared_state(indoc! {r"
3443 1 2 3
3444 4 5 ˇ6
3445 7 8 9"})
3446 .await;
3447 cx.simulate_shared_keystrokes("9 shift-h").await;
3448 cx.shared_state().await.assert_eq(indoc! {"
3449 1 2 3
3450 4 5 6
3451 7 8 ˇ9"});
3452 }
3453
3454 #[gpui::test]
3455 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3456 let mut cx = NeovimBackedTestContext::new(cx).await;
3457 let initial_state = indoc! {r"abˇc
3458 def
3459 paragraph
3460 the second
3461 third and
3462 final"};
3463
3464 cx.set_shared_state(initial_state).await;
3465 cx.simulate_shared_keystrokes("shift-m").await;
3466 cx.shared_state().await.assert_eq(indoc! {r"abc
3467 def
3468 paˇragraph
3469 the second
3470 third and
3471 final"});
3472
3473 cx.set_shared_state(indoc! {r"
3474 1 2 3
3475 4 5 6
3476 7 8 ˇ9
3477 "})
3478 .await;
3479 cx.simulate_shared_keystrokes("shift-m").await;
3480 cx.shared_state().await.assert_eq(indoc! {"
3481 1 2 3
3482 4 5 ˇ6
3483 7 8 9
3484 "});
3485 cx.set_shared_state(indoc! {r"
3486 1 2 3
3487 4 5 6
3488 ˇ7 8 9
3489 "})
3490 .await;
3491 cx.simulate_shared_keystrokes("shift-m").await;
3492 cx.shared_state().await.assert_eq(indoc! {"
3493 1 2 3
3494 ˇ4 5 6
3495 7 8 9
3496 "});
3497 cx.set_shared_state(indoc! {r"
3498 ˇ1 2 3
3499 4 5 6
3500 7 8 9
3501 "})
3502 .await;
3503 cx.simulate_shared_keystrokes("shift-m").await;
3504 cx.shared_state().await.assert_eq(indoc! {"
3505 1 2 3
3506 ˇ4 5 6
3507 7 8 9
3508 "});
3509 cx.set_shared_state(indoc! {r"
3510 1 2 3
3511 ˇ4 5 6
3512 7 8 9
3513 "})
3514 .await;
3515 cx.simulate_shared_keystrokes("shift-m").await;
3516 cx.shared_state().await.assert_eq(indoc! {"
3517 1 2 3
3518 ˇ4 5 6
3519 7 8 9
3520 "});
3521 cx.set_shared_state(indoc! {r"
3522 1 2 3
3523 4 5 ˇ6
3524 7 8 9
3525 "})
3526 .await;
3527 cx.simulate_shared_keystrokes("shift-m").await;
3528 cx.shared_state().await.assert_eq(indoc! {"
3529 1 2 3
3530 4 5 ˇ6
3531 7 8 9
3532 "});
3533 }
3534
3535 #[gpui::test]
3536 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3537 let mut cx = NeovimBackedTestContext::new(cx).await;
3538 let initial_state = indoc! {r"abc
3539 deˇf
3540 paragraph
3541 the second
3542 third and
3543 final"};
3544
3545 cx.set_shared_state(initial_state).await;
3546 cx.simulate_shared_keystrokes("shift-l").await;
3547 cx.shared_state().await.assert_eq(indoc! {r"abc
3548 def
3549 paragraph
3550 the second
3551 third and
3552 fiˇnal"});
3553
3554 cx.set_shared_state(indoc! {r"
3555 1 2 3
3556 4 5 ˇ6
3557 7 8 9
3558 "})
3559 .await;
3560 cx.simulate_shared_keystrokes("shift-l").await;
3561 cx.shared_state().await.assert_eq(indoc! {"
3562 1 2 3
3563 4 5 6
3564 7 8 9
3565 ˇ"});
3566
3567 cx.set_shared_state(indoc! {r"
3568 1 2 3
3569 ˇ4 5 6
3570 7 8 9
3571 "})
3572 .await;
3573 cx.simulate_shared_keystrokes("shift-l").await;
3574 cx.shared_state().await.assert_eq(indoc! {"
3575 1 2 3
3576 4 5 6
3577 7 8 9
3578 ˇ"});
3579
3580 cx.set_shared_state(indoc! {r"
3581 1 2 ˇ3
3582 4 5 6
3583 7 8 9
3584 "})
3585 .await;
3586 cx.simulate_shared_keystrokes("shift-l").await;
3587 cx.shared_state().await.assert_eq(indoc! {"
3588 1 2 3
3589 4 5 6
3590 7 8 9
3591 ˇ"});
3592
3593 cx.set_shared_state(indoc! {r"
3594 ˇ1 2 3
3595 4 5 6
3596 7 8 9
3597 "})
3598 .await;
3599 cx.simulate_shared_keystrokes("shift-l").await;
3600 cx.shared_state().await.assert_eq(indoc! {"
3601 1 2 3
3602 4 5 6
3603 7 8 9
3604 ˇ"});
3605
3606 cx.set_shared_state(indoc! {r"
3607 1 2 3
3608 4 5 ˇ6
3609 7 8 9
3610 "})
3611 .await;
3612 cx.simulate_shared_keystrokes("9 shift-l").await;
3613 cx.shared_state().await.assert_eq(indoc! {"
3614 1 2 ˇ3
3615 4 5 6
3616 7 8 9
3617 "});
3618 }
3619
3620 #[gpui::test]
3621 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3622 let mut cx = NeovimBackedTestContext::new(cx).await;
3623 cx.set_shared_state(indoc! {r"
3624 456 5ˇ67 678
3625 "})
3626 .await;
3627 cx.simulate_shared_keystrokes("g e").await;
3628 cx.shared_state().await.assert_eq(indoc! {"
3629 45ˇ6 567 678
3630 "});
3631
3632 // Test times
3633 cx.set_shared_state(indoc! {r"
3634 123 234 345
3635 456 5ˇ67 678
3636 "})
3637 .await;
3638 cx.simulate_shared_keystrokes("4 g e").await;
3639 cx.shared_state().await.assert_eq(indoc! {"
3640 12ˇ3 234 345
3641 456 567 678
3642 "});
3643
3644 // With punctuation
3645 cx.set_shared_state(indoc! {r"
3646 123 234 345
3647 4;5.6 5ˇ67 678
3648 789 890 901
3649 "})
3650 .await;
3651 cx.simulate_shared_keystrokes("g e").await;
3652 cx.shared_state().await.assert_eq(indoc! {"
3653 123 234 345
3654 4;5.ˇ6 567 678
3655 789 890 901
3656 "});
3657
3658 // With punctuation and count
3659 cx.set_shared_state(indoc! {r"
3660 123 234 345
3661 4;5.6 5ˇ67 678
3662 789 890 901
3663 "})
3664 .await;
3665 cx.simulate_shared_keystrokes("5 g e").await;
3666 cx.shared_state().await.assert_eq(indoc! {"
3667 123 234 345
3668 ˇ4;5.6 567 678
3669 789 890 901
3670 "});
3671
3672 // newlines
3673 cx.set_shared_state(indoc! {r"
3674 123 234 345
3675
3676 78ˇ9 890 901
3677 "})
3678 .await;
3679 cx.simulate_shared_keystrokes("g e").await;
3680 cx.shared_state().await.assert_eq(indoc! {"
3681 123 234 345
3682 ˇ
3683 789 890 901
3684 "});
3685 cx.simulate_shared_keystrokes("g e").await;
3686 cx.shared_state().await.assert_eq(indoc! {"
3687 123 234 34ˇ5
3688
3689 789 890 901
3690 "});
3691
3692 // With punctuation
3693 cx.set_shared_state(indoc! {r"
3694 123 234 345
3695 4;5.ˇ6 567 678
3696 789 890 901
3697 "})
3698 .await;
3699 cx.simulate_shared_keystrokes("g shift-e").await;
3700 cx.shared_state().await.assert_eq(indoc! {"
3701 123 234 34ˇ5
3702 4;5.6 567 678
3703 789 890 901
3704 "});
3705
3706 // With multi byte char
3707 cx.set_shared_state(indoc! {r"
3708 bar ˇó
3709 "})
3710 .await;
3711 cx.simulate_shared_keystrokes("g e").await;
3712 cx.shared_state().await.assert_eq(indoc! {"
3713 baˇr ó
3714 "});
3715 }
3716
3717 #[gpui::test]
3718 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3719 let mut cx = NeovimBackedTestContext::new(cx).await;
3720
3721 cx.set_shared_state(indoc! {"
3722 fn aˇ() {
3723 return
3724 }
3725 "})
3726 .await;
3727 cx.simulate_shared_keystrokes("v $ %").await;
3728 cx.shared_state().await.assert_eq(indoc! {"
3729 fn a«() {
3730 return
3731 }ˇ»
3732 "});
3733 }
3734
3735 #[gpui::test]
3736 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3737 let mut cx = VimTestContext::new(cx, true).await;
3738
3739 cx.set_state(
3740 indoc! {"
3741 struct Foo {
3742 ˇ
3743 }
3744 "},
3745 Mode::Normal,
3746 );
3747
3748 cx.update_editor(|editor, _window, cx| {
3749 let range = editor.selections.newest_anchor().range();
3750 let inlay_text = " field: int,\n field2: string\n field3: float";
3751 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3752 editor.splice_inlays(&[], vec![inlay], cx);
3753 });
3754
3755 cx.simulate_keystrokes("j");
3756 cx.assert_state(
3757 indoc! {"
3758 struct Foo {
3759
3760 ˇ}
3761 "},
3762 Mode::Normal,
3763 );
3764 }
3765
3766 #[gpui::test]
3767 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3768 let mut cx = VimTestContext::new(cx, true).await;
3769
3770 cx.set_state(
3771 indoc! {"
3772 ˇstruct Foo {
3773
3774 }
3775 "},
3776 Mode::Normal,
3777 );
3778 cx.update_editor(|editor, _window, cx| {
3779 let snapshot = editor.buffer().read(cx).snapshot(cx);
3780 let end_of_line =
3781 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3782 let inlay_text = " hint";
3783 let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3784 editor.splice_inlays(&[], vec![inlay], cx);
3785 });
3786 cx.simulate_keystrokes("$");
3787 cx.assert_state(
3788 indoc! {"
3789 struct Foo ˇ{
3790
3791 }
3792 "},
3793 Mode::Normal,
3794 );
3795 }
3796
3797 #[gpui::test]
3798 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3799 let mut cx = NeovimBackedTestContext::new(cx).await;
3800 // Normal mode
3801 cx.set_shared_state(indoc! {"
3802 The ˇquick brown
3803 fox jumps over
3804 the lazy dog
3805 The quick brown
3806 fox jumps over
3807 the lazy dog
3808 The quick brown
3809 fox jumps over
3810 the lazy dog"})
3811 .await;
3812 cx.simulate_shared_keystrokes("2 0 %").await;
3813 cx.shared_state().await.assert_eq(indoc! {"
3814 The quick brown
3815 fox ˇjumps over
3816 the lazy dog
3817 The quick brown
3818 fox jumps over
3819 the lazy dog
3820 The quick brown
3821 fox jumps over
3822 the lazy dog"});
3823
3824 cx.simulate_shared_keystrokes("2 5 %").await;
3825 cx.shared_state().await.assert_eq(indoc! {"
3826 The quick brown
3827 fox jumps over
3828 the ˇlazy dog
3829 The quick brown
3830 fox jumps over
3831 the lazy dog
3832 The quick brown
3833 fox jumps over
3834 the lazy dog"});
3835
3836 cx.simulate_shared_keystrokes("7 5 %").await;
3837 cx.shared_state().await.assert_eq(indoc! {"
3838 The quick brown
3839 fox jumps over
3840 the lazy dog
3841 The quick brown
3842 fox jumps over
3843 the lazy dog
3844 The ˇquick brown
3845 fox jumps over
3846 the lazy dog"});
3847
3848 // Visual mode
3849 cx.set_shared_state(indoc! {"
3850 The ˇquick brown
3851 fox jumps over
3852 the lazy dog
3853 The quick brown
3854 fox jumps over
3855 the lazy dog
3856 The quick brown
3857 fox jumps over
3858 the lazy dog"})
3859 .await;
3860 cx.simulate_shared_keystrokes("v 5 0 %").await;
3861 cx.shared_state().await.assert_eq(indoc! {"
3862 The «quick brown
3863 fox jumps over
3864 the lazy dog
3865 The quick brown
3866 fox jˇ»umps over
3867 the lazy dog
3868 The quick brown
3869 fox jumps over
3870 the lazy dog"});
3871
3872 cx.set_shared_state(indoc! {"
3873 The ˇquick brown
3874 fox jumps over
3875 the lazy dog
3876 The quick brown
3877 fox jumps over
3878 the lazy dog
3879 The quick brown
3880 fox jumps over
3881 the lazy dog"})
3882 .await;
3883 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3884 cx.shared_state().await.assert_eq(indoc! {"
3885 The «quick brown
3886 fox jumps over
3887 the lazy dog
3888 The quick brown
3889 fox jumps over
3890 the lazy dog
3891 The quick brown
3892 fox jumps over
3893 the lˇ»azy dog"});
3894 }
3895
3896 #[gpui::test]
3897 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3898 let mut cx = NeovimBackedTestContext::new(cx).await;
3899
3900 cx.set_shared_state("ˇπππππ").await;
3901 cx.simulate_shared_keystrokes("3 space").await;
3902 cx.shared_state().await.assert_eq("πππˇππ");
3903 }
3904
3905 #[gpui::test]
3906 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3907 let mut cx = NeovimBackedTestContext::new(cx).await;
3908
3909 cx.set_shared_state(indoc! {"
3910 ππππˇπ
3911 πanotherline"})
3912 .await;
3913 cx.simulate_shared_keystrokes("4 space").await;
3914 cx.shared_state().await.assert_eq(indoc! {"
3915 πππππ
3916 πanˇotherline"});
3917 }
3918
3919 #[gpui::test]
3920 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3921 let mut cx = NeovimBackedTestContext::new(cx).await;
3922
3923 cx.set_shared_state(indoc! {"
3924 ππππ
3925 πanˇotherline"})
3926 .await;
3927 cx.simulate_shared_keystrokes("4 backspace").await;
3928 cx.shared_state().await.assert_eq(indoc! {"
3929 πππˇπ
3930 πanotherline"});
3931 }
3932
3933 #[gpui::test]
3934 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
3935 let mut cx = VimTestContext::new(cx, true).await;
3936 cx.set_state(
3937 indoc! {
3938 "func empty(a string) bool {
3939 ˇif a == \"\" {
3940 return true
3941 }
3942 return false
3943 }"
3944 },
3945 Mode::Normal,
3946 );
3947 cx.simulate_keystrokes("[ -");
3948 cx.assert_state(
3949 indoc! {
3950 "ˇfunc empty(a string) bool {
3951 if a == \"\" {
3952 return true
3953 }
3954 return false
3955 }"
3956 },
3957 Mode::Normal,
3958 );
3959 cx.simulate_keystrokes("] =");
3960 cx.assert_state(
3961 indoc! {
3962 "func empty(a string) bool {
3963 if a == \"\" {
3964 return true
3965 }
3966 return false
3967 ˇ}"
3968 },
3969 Mode::Normal,
3970 );
3971 cx.simulate_keystrokes("[ +");
3972 cx.assert_state(
3973 indoc! {
3974 "func empty(a string) bool {
3975 if a == \"\" {
3976 return true
3977 }
3978 ˇreturn false
3979 }"
3980 },
3981 Mode::Normal,
3982 );
3983 cx.simulate_keystrokes("2 [ =");
3984 cx.assert_state(
3985 indoc! {
3986 "func empty(a string) bool {
3987 ˇif a == \"\" {
3988 return true
3989 }
3990 return false
3991 }"
3992 },
3993 Mode::Normal,
3994 );
3995 cx.simulate_keystrokes("] +");
3996 cx.assert_state(
3997 indoc! {
3998 "func empty(a string) bool {
3999 if a == \"\" {
4000 ˇreturn true
4001 }
4002 return false
4003 }"
4004 },
4005 Mode::Normal,
4006 );
4007 cx.simulate_keystrokes("] -");
4008 cx.assert_state(
4009 indoc! {
4010 "func empty(a string) bool {
4011 if a == \"\" {
4012 return true
4013 ˇ}
4014 return false
4015 }"
4016 },
4017 Mode::Normal,
4018 );
4019 }
4020
4021 #[gpui::test]
4022 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4023 let mut cx = NeovimBackedTestContext::new(cx).await;
4024 cx.set_shared_state("abˇc").await;
4025 cx.simulate_shared_keystrokes("delete").await;
4026 cx.shared_state().await.assert_eq("aˇb");
4027 }
4028
4029 #[gpui::test]
4030 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4031 let mut cx = NeovimBackedTestContext::new(cx).await;
4032
4033 cx.set_shared_state(indoc! {"
4034 ˇthe quick brown fox
4035 jumped over the lazy dog"})
4036 .await;
4037 cx.simulate_shared_keystrokes("d v 0").await;
4038 cx.shared_state().await.assert_eq(indoc! {"
4039 ˇhe quick brown fox
4040 jumped over the lazy dog"});
4041 assert_eq!(cx.cx.forced_motion(), false);
4042
4043 cx.set_shared_state(indoc! {"
4044 the quick bˇrown fox
4045 jumped over the lazy dog"})
4046 .await;
4047 cx.simulate_shared_keystrokes("d v 0").await;
4048 cx.shared_state().await.assert_eq(indoc! {"
4049 ˇown fox
4050 jumped over the lazy dog"});
4051 assert_eq!(cx.cx.forced_motion(), false);
4052
4053 cx.set_shared_state(indoc! {"
4054 the quick brown foˇx
4055 jumped over the lazy dog"})
4056 .await;
4057 cx.simulate_shared_keystrokes("d v 0").await;
4058 cx.shared_state().await.assert_eq(indoc! {"
4059 ˇ
4060 jumped over the lazy dog"});
4061 assert_eq!(cx.cx.forced_motion(), false);
4062 }
4063
4064 #[gpui::test]
4065 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4066 let mut cx = NeovimBackedTestContext::new(cx).await;
4067
4068 cx.set_shared_state(indoc! {"
4069 ˇthe quick brown fox
4070 jumped over the lazy dog"})
4071 .await;
4072 cx.simulate_shared_keystrokes("d v g shift-m").await;
4073 cx.shared_state().await.assert_eq(indoc! {"
4074 ˇbrown fox
4075 jumped over the lazy dog"});
4076 assert_eq!(cx.cx.forced_motion(), false);
4077
4078 cx.set_shared_state(indoc! {"
4079 the quick bˇrown fox
4080 jumped over the lazy dog"})
4081 .await;
4082 cx.simulate_shared_keystrokes("d v g shift-m").await;
4083 cx.shared_state().await.assert_eq(indoc! {"
4084 the quickˇown fox
4085 jumped over the lazy dog"});
4086 assert_eq!(cx.cx.forced_motion(), false);
4087
4088 cx.set_shared_state(indoc! {"
4089 the quick brown foˇx
4090 jumped over the lazy dog"})
4091 .await;
4092 cx.simulate_shared_keystrokes("d v g shift-m").await;
4093 cx.shared_state().await.assert_eq(indoc! {"
4094 the quicˇk
4095 jumped over the lazy dog"});
4096 assert_eq!(cx.cx.forced_motion(), false);
4097
4098 cx.set_shared_state(indoc! {"
4099 ˇthe quick brown fox
4100 jumped over the lazy dog"})
4101 .await;
4102 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4103 cx.shared_state().await.assert_eq(indoc! {"
4104 ˇ fox
4105 jumped over the lazy dog"});
4106 assert_eq!(cx.cx.forced_motion(), false);
4107
4108 cx.set_shared_state(indoc! {"
4109 ˇthe quick brown fox
4110 jumped over the lazy dog"})
4111 .await;
4112 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4113 cx.shared_state().await.assert_eq(indoc! {"
4114 ˇuick brown fox
4115 jumped over the lazy dog"});
4116 assert_eq!(cx.cx.forced_motion(), false);
4117 }
4118
4119 #[gpui::test]
4120 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4121 let mut cx = NeovimBackedTestContext::new(cx).await;
4122
4123 cx.set_shared_state(indoc! {"
4124 the quick brown foˇx
4125 jumped over the lazy dog"})
4126 .await;
4127 cx.simulate_shared_keystrokes("d v $").await;
4128 cx.shared_state().await.assert_eq(indoc! {"
4129 the quick brown foˇx
4130 jumped over the lazy dog"});
4131 assert_eq!(cx.cx.forced_motion(), false);
4132
4133 cx.set_shared_state(indoc! {"
4134 ˇthe quick brown fox
4135 jumped over the lazy dog"})
4136 .await;
4137 cx.simulate_shared_keystrokes("d v $").await;
4138 cx.shared_state().await.assert_eq(indoc! {"
4139 ˇx
4140 jumped over the lazy dog"});
4141 assert_eq!(cx.cx.forced_motion(), false);
4142 }
4143
4144 #[gpui::test]
4145 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4146 let mut cx = NeovimBackedTestContext::new(cx).await;
4147
4148 cx.set_shared_state(indoc! {"
4149 ˇthe quick brown fox
4150 jumped over the lazy dog"})
4151 .await;
4152 cx.simulate_shared_keystrokes("y v j p").await;
4153 cx.shared_state().await.assert_eq(indoc! {"
4154 the quick brown fox
4155 ˇthe quick brown fox
4156 jumped over the lazy dog"});
4157 assert_eq!(cx.cx.forced_motion(), false);
4158
4159 cx.set_shared_state(indoc! {"
4160 the quick bˇrown fox
4161 jumped over the lazy dog"})
4162 .await;
4163 cx.simulate_shared_keystrokes("y v j p").await;
4164 cx.shared_state().await.assert_eq(indoc! {"
4165 the quick brˇrown fox
4166 jumped overown fox
4167 jumped over the lazy dog"});
4168 assert_eq!(cx.cx.forced_motion(), false);
4169
4170 cx.set_shared_state(indoc! {"
4171 the quick brown foˇx
4172 jumped over the lazy dog"})
4173 .await;
4174 cx.simulate_shared_keystrokes("y v j p").await;
4175 cx.shared_state().await.assert_eq(indoc! {"
4176 the quick brown foxˇx
4177 jumped over the la
4178 jumped over the lazy dog"});
4179 assert_eq!(cx.cx.forced_motion(), false);
4180
4181 cx.set_shared_state(indoc! {"
4182 the quick brown fox
4183 jˇumped over the lazy dog"})
4184 .await;
4185 cx.simulate_shared_keystrokes("y v k p").await;
4186 cx.shared_state().await.assert_eq(indoc! {"
4187 thˇhe quick brown fox
4188 je quick brown fox
4189 jumped over the lazy dog"});
4190 assert_eq!(cx.cx.forced_motion(), false);
4191 }
4192
4193 #[gpui::test]
4194 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4195 let mut cx = NeovimBackedTestContext::new(cx).await;
4196
4197 cx.set_shared_state(indoc! {"
4198 ˇthe quick brown fox
4199 jumped over the lazy dog"})
4200 .await;
4201 cx.simulate_shared_keystrokes("d v e").await;
4202 cx.shared_state().await.assert_eq(indoc! {"
4203 ˇe quick brown fox
4204 jumped over the lazy dog"});
4205 assert_eq!(cx.cx.forced_motion(), false);
4206
4207 cx.set_shared_state(indoc! {"
4208 the quick bˇrown fox
4209 jumped over the lazy dog"})
4210 .await;
4211 cx.simulate_shared_keystrokes("d v e").await;
4212 cx.shared_state().await.assert_eq(indoc! {"
4213 the quick bˇn fox
4214 jumped over the lazy dog"});
4215 assert_eq!(cx.cx.forced_motion(), false);
4216
4217 cx.set_shared_state(indoc! {"
4218 the quick brown foˇx
4219 jumped over the lazy dog"})
4220 .await;
4221 cx.simulate_shared_keystrokes("d v e").await;
4222 cx.shared_state().await.assert_eq(indoc! {"
4223 the quick brown foˇd over the lazy dog"});
4224 assert_eq!(cx.cx.forced_motion(), false);
4225 }
4226}