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