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