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