1use editor::{
2 Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
3 display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
4 movement::{
5 self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
6 },
7};
8use gpui::{Action, Context, Window, actions, px};
9use language::{CharKind, Point, Selection, SelectionGoal};
10use multi_buffer::MultiBufferRow;
11use schemars::JsonSchema;
12use serde::Deserialize;
13use std::ops::Range;
14use workspace::searchable::Direction;
15
16use crate::{
17 Vim,
18 normal::mark,
19 state::{Mode, Operator},
20 surrounds::SurroundsType,
21};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub(crate) enum MotionKind {
25 Linewise,
26 Exclusive,
27 Inclusive,
28}
29
30impl MotionKind {
31 pub(crate) fn for_mode(mode: Mode) -> Self {
32 match mode {
33 Mode::VisualLine => MotionKind::Linewise,
34 _ => MotionKind::Exclusive,
35 }
36 }
37
38 pub(crate) fn linewise(&self) -> bool {
39 matches!(self, MotionKind::Linewise)
40 }
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum Motion {
45 Left,
46 WrappingLeft,
47 Down {
48 display_lines: bool,
49 },
50 Up {
51 display_lines: bool,
52 },
53 Right,
54 WrappingRight,
55 NextWordStart {
56 ignore_punctuation: bool,
57 },
58 NextWordEnd {
59 ignore_punctuation: bool,
60 },
61 PreviousWordStart {
62 ignore_punctuation: bool,
63 },
64 PreviousWordEnd {
65 ignore_punctuation: bool,
66 },
67 NextSubwordStart {
68 ignore_punctuation: bool,
69 },
70 NextSubwordEnd {
71 ignore_punctuation: bool,
72 },
73 PreviousSubwordStart {
74 ignore_punctuation: bool,
75 },
76 PreviousSubwordEnd {
77 ignore_punctuation: bool,
78 },
79 FirstNonWhitespace {
80 display_lines: bool,
81 },
82 CurrentLine,
83 StartOfLine {
84 display_lines: bool,
85 },
86 MiddleOfLine {
87 display_lines: bool,
88 },
89 EndOfLine {
90 display_lines: bool,
91 },
92 SentenceBackward,
93 SentenceForward,
94 StartOfParagraph,
95 EndOfParagraph,
96 StartOfDocument,
97 EndOfDocument,
98 Matching,
99 GoToPercentage,
100 UnmatchedForward {
101 char: char,
102 },
103 UnmatchedBackward {
104 char: char,
105 },
106 FindForward {
107 before: bool,
108 char: char,
109 mode: FindRange,
110 smartcase: bool,
111 },
112 FindBackward {
113 after: bool,
114 char: char,
115 mode: FindRange,
116 smartcase: bool,
117 },
118 Sneak {
119 first_char: char,
120 second_char: char,
121 smartcase: bool,
122 },
123 SneakBackward {
124 first_char: char,
125 second_char: char,
126 smartcase: bool,
127 },
128 RepeatFind {
129 last_find: Box<Motion>,
130 },
131 RepeatFindReversed {
132 last_find: Box<Motion>,
133 },
134 NextLineStart,
135 PreviousLineStart,
136 StartOfLineDownward,
137 EndOfLineDownward,
138 GoToColumn,
139 WindowTop,
140 WindowMiddle,
141 WindowBottom,
142 NextSectionStart,
143 NextSectionEnd,
144 PreviousSectionStart,
145 PreviousSectionEnd,
146 NextMethodStart,
147 NextMethodEnd,
148 PreviousMethodStart,
149 PreviousMethodEnd,
150 NextComment,
151 PreviousComment,
152 PreviousLesserIndent,
153 PreviousGreaterIndent,
154 PreviousSameIndent,
155 NextLesserIndent,
156 NextGreaterIndent,
157 NextSameIndent,
158
159 // we don't have a good way to run a search synchronously, so
160 // we handle search motions by running the search async and then
161 // calling back into motion with this
162 ZedSearchResult {
163 prior_selections: Vec<Range<Anchor>>,
164 new_selections: Vec<Range<Anchor>>,
165 },
166 Jump {
167 anchor: Anchor,
168 line: bool,
169 },
170}
171
172#[derive(Clone, Copy)]
173enum IndentType {
174 Lesser,
175 Greater,
176 Same,
177}
178
179/// 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
1528pub(crate) fn start_of_relative_buffer_row(
1529 map: &DisplaySnapshot,
1530 point: DisplayPoint,
1531 times: isize,
1532) -> DisplayPoint {
1533 let start = map.display_point_to_fold_point(point, Bias::Left);
1534 let target = start.row() as isize + times;
1535 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1536
1537 map.clip_point(
1538 map.fold_point_to_display_point(
1539 map.fold_snapshot
1540 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1541 ),
1542 Bias::Right,
1543 )
1544}
1545
1546fn up_down_buffer_rows(
1547 map: &DisplaySnapshot,
1548 mut point: DisplayPoint,
1549 mut goal: SelectionGoal,
1550 mut times: isize,
1551 text_layout_details: &TextLayoutDetails,
1552) -> (DisplayPoint, SelectionGoal) {
1553 let bias = if times < 0 { Bias::Left } else { Bias::Right };
1554
1555 while map.is_folded_buffer_header(point.row()) {
1556 if times < 0 {
1557 (point, _) = movement::up(map, point, goal, true, text_layout_details);
1558 times += 1;
1559 } else if times > 0 {
1560 (point, _) = movement::down(map, point, goal, true, text_layout_details);
1561 times -= 1;
1562 } else {
1563 break;
1564 }
1565 }
1566
1567 let start = map.display_point_to_fold_point(point, Bias::Left);
1568 let begin_folded_line = map.fold_point_to_display_point(
1569 map.fold_snapshot
1570 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1571 );
1572 let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1573
1574 let (goal_wrap, goal_x) = match goal {
1575 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1576 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end as f32),
1577 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x as f32),
1578 _ => {
1579 let x = map.x_for_display_point(point, text_layout_details);
1580 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.into()));
1581 (select_nth_wrapped_row, x.into())
1582 }
1583 };
1584
1585 let target = start.row() as isize + times;
1586 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1587
1588 let mut begin_folded_line = map.fold_point_to_display_point(
1589 map.fold_snapshot
1590 .clip_point(FoldPoint::new(new_row, 0), bias),
1591 );
1592
1593 let mut i = 0;
1594 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1595 let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1596 if map
1597 .display_point_to_fold_point(next_folded_line, bias)
1598 .row()
1599 == new_row
1600 {
1601 i += 1;
1602 begin_folded_line = next_folded_line;
1603 } else {
1604 break;
1605 }
1606 }
1607
1608 let new_col = if i == goal_wrap {
1609 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1610 } else {
1611 map.line_len(begin_folded_line.row())
1612 };
1613
1614 let point = DisplayPoint::new(begin_folded_line.row(), new_col);
1615 let mut clipped_point = map.clip_point(point, bias);
1616
1617 // When navigating vertically in vim mode with inlay hints present,
1618 // we need to handle the case where clipping moves us to a different row.
1619 // This can happen when moving down (Bias::Right) and hitting an inlay hint.
1620 // Re-clip with opposite bias to stay on the intended line.
1621 //
1622 // See: https://github.com/zed-industries/zed/issues/29134
1623 if clipped_point.row() > point.row() {
1624 clipped_point = map.clip_point(point, Bias::Left);
1625 }
1626
1627 (clipped_point, goal)
1628}
1629
1630fn down_display(
1631 map: &DisplaySnapshot,
1632 mut point: DisplayPoint,
1633 mut goal: SelectionGoal,
1634 times: usize,
1635 text_layout_details: &TextLayoutDetails,
1636) -> (DisplayPoint, SelectionGoal) {
1637 for _ in 0..times {
1638 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1639 }
1640
1641 (point, goal)
1642}
1643
1644fn up_display(
1645 map: &DisplaySnapshot,
1646 mut point: DisplayPoint,
1647 mut goal: SelectionGoal,
1648 times: usize,
1649 text_layout_details: &TextLayoutDetails,
1650) -> (DisplayPoint, SelectionGoal) {
1651 for _ in 0..times {
1652 (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1653 }
1654
1655 (point, goal)
1656}
1657
1658pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1659 for _ in 0..times {
1660 let new_point = movement::saturating_right(map, point);
1661 if point == new_point {
1662 break;
1663 }
1664 point = new_point;
1665 }
1666 point
1667}
1668
1669pub(crate) fn next_char(
1670 map: &DisplaySnapshot,
1671 point: DisplayPoint,
1672 allow_cross_newline: bool,
1673) -> DisplayPoint {
1674 let mut new_point = point;
1675 let mut max_column = map.line_len(new_point.row());
1676 if !allow_cross_newline {
1677 max_column -= 1;
1678 }
1679 if new_point.column() < max_column {
1680 *new_point.column_mut() += 1;
1681 } else if new_point < map.max_point() && allow_cross_newline {
1682 *new_point.row_mut() += 1;
1683 *new_point.column_mut() = 0;
1684 }
1685 map.clip_ignoring_line_ends(new_point, Bias::Right)
1686}
1687
1688pub(crate) fn next_word_start(
1689 map: &DisplaySnapshot,
1690 mut point: DisplayPoint,
1691 ignore_punctuation: bool,
1692 times: usize,
1693) -> DisplayPoint {
1694 let classifier = map
1695 .buffer_snapshot
1696 .char_classifier_at(point.to_point(map))
1697 .ignore_punctuation(ignore_punctuation);
1698 for _ in 0..times {
1699 let mut crossed_newline = false;
1700 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1701 let left_kind = classifier.kind(left);
1702 let right_kind = classifier.kind(right);
1703 let at_newline = right == '\n';
1704
1705 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1706 || at_newline && crossed_newline
1707 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1708
1709 crossed_newline |= at_newline;
1710 found
1711 });
1712 if point == new_point {
1713 break;
1714 }
1715 point = new_point;
1716 }
1717 point
1718}
1719
1720pub(crate) fn next_word_end(
1721 map: &DisplaySnapshot,
1722 mut point: DisplayPoint,
1723 ignore_punctuation: bool,
1724 times: usize,
1725 allow_cross_newline: bool,
1726 always_advance: bool,
1727) -> DisplayPoint {
1728 let classifier = map
1729 .buffer_snapshot
1730 .char_classifier_at(point.to_point(map))
1731 .ignore_punctuation(ignore_punctuation);
1732 for _ in 0..times {
1733 let mut need_next_char = false;
1734 let new_point = if always_advance {
1735 next_char(map, point, allow_cross_newline)
1736 } else {
1737 point
1738 };
1739 let new_point = movement::find_boundary_exclusive(
1740 map,
1741 new_point,
1742 FindRange::MultiLine,
1743 |left, right| {
1744 let left_kind = classifier.kind(left);
1745 let right_kind = classifier.kind(right);
1746 let at_newline = right == '\n';
1747
1748 if !allow_cross_newline && at_newline {
1749 need_next_char = true;
1750 return true;
1751 }
1752
1753 left_kind != right_kind && left_kind != CharKind::Whitespace
1754 },
1755 );
1756 let new_point = if need_next_char {
1757 next_char(map, new_point, true)
1758 } else {
1759 new_point
1760 };
1761 let new_point = map.clip_point(new_point, Bias::Left);
1762 if point == new_point {
1763 break;
1764 }
1765 point = new_point;
1766 }
1767 point
1768}
1769
1770fn previous_word_start(
1771 map: &DisplaySnapshot,
1772 mut point: DisplayPoint,
1773 ignore_punctuation: bool,
1774 times: usize,
1775) -> DisplayPoint {
1776 let classifier = map
1777 .buffer_snapshot
1778 .char_classifier_at(point.to_point(map))
1779 .ignore_punctuation(ignore_punctuation);
1780 for _ in 0..times {
1781 // This works even though find_preceding_boundary is called for every character in the line containing
1782 // cursor because the newline is checked only once.
1783 let new_point = movement::find_preceding_boundary_display_point(
1784 map,
1785 point,
1786 FindRange::MultiLine,
1787 |left, right| {
1788 let left_kind = classifier.kind(left);
1789 let right_kind = classifier.kind(right);
1790
1791 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1792 },
1793 );
1794 if point == new_point {
1795 break;
1796 }
1797 point = new_point;
1798 }
1799 point
1800}
1801
1802fn previous_word_end(
1803 map: &DisplaySnapshot,
1804 point: DisplayPoint,
1805 ignore_punctuation: bool,
1806 times: usize,
1807) -> DisplayPoint {
1808 let classifier = map
1809 .buffer_snapshot
1810 .char_classifier_at(point.to_point(map))
1811 .ignore_punctuation(ignore_punctuation);
1812 let mut point = point.to_point(map);
1813
1814 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row))
1815 && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1816 {
1817 point.column += ch.len_utf8() as u32;
1818 }
1819 for _ in 0..times {
1820 let new_point = movement::find_preceding_boundary_point(
1821 &map.buffer_snapshot,
1822 point,
1823 FindRange::MultiLine,
1824 |left, right| {
1825 let left_kind = classifier.kind(left);
1826 let right_kind = classifier.kind(right);
1827 match (left_kind, right_kind) {
1828 (CharKind::Punctuation, CharKind::Whitespace)
1829 | (CharKind::Punctuation, CharKind::Word)
1830 | (CharKind::Word, CharKind::Whitespace)
1831 | (CharKind::Word, CharKind::Punctuation) => true,
1832 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1833 _ => false,
1834 }
1835 },
1836 );
1837 if new_point == point {
1838 break;
1839 }
1840 point = new_point;
1841 }
1842 movement::saturating_left(map, point.to_display_point(map))
1843}
1844
1845fn next_subword_start(
1846 map: &DisplaySnapshot,
1847 mut point: DisplayPoint,
1848 ignore_punctuation: bool,
1849 times: usize,
1850) -> DisplayPoint {
1851 let classifier = map
1852 .buffer_snapshot
1853 .char_classifier_at(point.to_point(map))
1854 .ignore_punctuation(ignore_punctuation);
1855 for _ in 0..times {
1856 let mut crossed_newline = false;
1857 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1858 let left_kind = classifier.kind(left);
1859 let right_kind = classifier.kind(right);
1860 let at_newline = right == '\n';
1861
1862 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1863 let is_subword_start =
1864 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1865
1866 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1867 || at_newline && crossed_newline
1868 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1869
1870 crossed_newline |= at_newline;
1871 found
1872 });
1873 if point == new_point {
1874 break;
1875 }
1876 point = new_point;
1877 }
1878 point
1879}
1880
1881pub(crate) fn next_subword_end(
1882 map: &DisplaySnapshot,
1883 mut point: DisplayPoint,
1884 ignore_punctuation: bool,
1885 times: usize,
1886 allow_cross_newline: bool,
1887) -> DisplayPoint {
1888 let classifier = map
1889 .buffer_snapshot
1890 .char_classifier_at(point.to_point(map))
1891 .ignore_punctuation(ignore_punctuation);
1892 for _ in 0..times {
1893 let new_point = next_char(map, point, allow_cross_newline);
1894
1895 let mut crossed_newline = false;
1896 let mut need_backtrack = false;
1897 let new_point =
1898 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1899 let left_kind = classifier.kind(left);
1900 let right_kind = classifier.kind(right);
1901 let at_newline = right == '\n';
1902
1903 if !allow_cross_newline && at_newline {
1904 return true;
1905 }
1906
1907 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1908 let is_subword_end =
1909 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1910
1911 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1912
1913 if found && (is_word_end || is_subword_end) {
1914 need_backtrack = true;
1915 }
1916
1917 crossed_newline |= at_newline;
1918 found
1919 });
1920 let mut new_point = map.clip_point(new_point, Bias::Left);
1921 if need_backtrack {
1922 *new_point.column_mut() -= 1;
1923 }
1924 let new_point = map.clip_point(new_point, Bias::Left);
1925 if point == new_point {
1926 break;
1927 }
1928 point = new_point;
1929 }
1930 point
1931}
1932
1933fn previous_subword_start(
1934 map: &DisplaySnapshot,
1935 mut point: DisplayPoint,
1936 ignore_punctuation: bool,
1937 times: usize,
1938) -> DisplayPoint {
1939 let classifier = map
1940 .buffer_snapshot
1941 .char_classifier_at(point.to_point(map))
1942 .ignore_punctuation(ignore_punctuation);
1943 for _ in 0..times {
1944 let mut crossed_newline = false;
1945 // This works even though find_preceding_boundary is called for every character in the line containing
1946 // cursor because the newline is checked only once.
1947 let new_point = movement::find_preceding_boundary_display_point(
1948 map,
1949 point,
1950 FindRange::MultiLine,
1951 |left, right| {
1952 let left_kind = classifier.kind(left);
1953 let right_kind = classifier.kind(right);
1954 let at_newline = right == '\n';
1955
1956 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1957 let is_subword_start =
1958 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1959
1960 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1961 || at_newline && crossed_newline
1962 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1963
1964 crossed_newline |= at_newline;
1965
1966 found
1967 },
1968 );
1969 if point == new_point {
1970 break;
1971 }
1972 point = new_point;
1973 }
1974 point
1975}
1976
1977fn previous_subword_end(
1978 map: &DisplaySnapshot,
1979 point: DisplayPoint,
1980 ignore_punctuation: bool,
1981 times: usize,
1982) -> DisplayPoint {
1983 let classifier = map
1984 .buffer_snapshot
1985 .char_classifier_at(point.to_point(map))
1986 .ignore_punctuation(ignore_punctuation);
1987 let mut point = point.to_point(map);
1988
1989 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row))
1990 && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1991 {
1992 point.column += ch.len_utf8() as u32;
1993 }
1994 for _ in 0..times {
1995 let new_point = movement::find_preceding_boundary_point(
1996 &map.buffer_snapshot,
1997 point,
1998 FindRange::MultiLine,
1999 |left, right| {
2000 let left_kind = classifier.kind(left);
2001 let right_kind = classifier.kind(right);
2002
2003 let is_subword_end =
2004 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
2005
2006 if is_subword_end {
2007 return true;
2008 }
2009
2010 match (left_kind, right_kind) {
2011 (CharKind::Word, CharKind::Whitespace)
2012 | (CharKind::Word, CharKind::Punctuation) => true,
2013 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2014 _ => false,
2015 }
2016 },
2017 );
2018 if new_point == point {
2019 break;
2020 }
2021 point = new_point;
2022 }
2023 movement::saturating_left(map, point.to_display_point(map))
2024}
2025
2026pub(crate) fn first_non_whitespace(
2027 map: &DisplaySnapshot,
2028 display_lines: bool,
2029 from: DisplayPoint,
2030) -> DisplayPoint {
2031 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2032 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2033 for (ch, offset) in map.buffer_chars_at(start_offset) {
2034 if ch == '\n' {
2035 return from;
2036 }
2037
2038 start_offset = offset;
2039
2040 if classifier.kind(ch) != CharKind::Whitespace {
2041 break;
2042 }
2043 }
2044
2045 start_offset.to_display_point(map)
2046}
2047
2048pub(crate) fn last_non_whitespace(
2049 map: &DisplaySnapshot,
2050 from: DisplayPoint,
2051 count: usize,
2052) -> DisplayPoint {
2053 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2054 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2055
2056 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2057 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next()
2058 && classifier.kind(ch) != CharKind::Whitespace
2059 {
2060 return end_of_line.to_display_point(map);
2061 }
2062
2063 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2064 if ch == '\n' {
2065 break;
2066 }
2067 end_of_line = offset;
2068 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2069 break;
2070 }
2071 }
2072
2073 end_of_line.to_display_point(map)
2074}
2075
2076pub(crate) fn start_of_line(
2077 map: &DisplaySnapshot,
2078 display_lines: bool,
2079 point: DisplayPoint,
2080) -> DisplayPoint {
2081 if display_lines {
2082 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2083 } else {
2084 map.prev_line_boundary(point.to_point(map)).1
2085 }
2086}
2087
2088pub(crate) fn middle_of_line(
2089 map: &DisplaySnapshot,
2090 display_lines: bool,
2091 point: DisplayPoint,
2092 times: Option<usize>,
2093) -> DisplayPoint {
2094 let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2095 times as f64 / 100.
2096 } else {
2097 0.5
2098 };
2099 if display_lines {
2100 map.clip_point(
2101 DisplayPoint::new(
2102 point.row(),
2103 (map.line_len(point.row()) as f64 * percent) as u32,
2104 ),
2105 Bias::Left,
2106 )
2107 } else {
2108 let mut buffer_point = point.to_point(map);
2109 buffer_point.column = (map
2110 .buffer_snapshot
2111 .line_len(MultiBufferRow(buffer_point.row)) as f64
2112 * percent) as u32;
2113
2114 map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2115 }
2116}
2117
2118pub(crate) fn end_of_line(
2119 map: &DisplaySnapshot,
2120 display_lines: bool,
2121 mut point: DisplayPoint,
2122 times: usize,
2123) -> DisplayPoint {
2124 if times > 1 {
2125 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2126 }
2127 if display_lines {
2128 map.clip_point(
2129 DisplayPoint::new(point.row(), map.line_len(point.row())),
2130 Bias::Left,
2131 )
2132 } else {
2133 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2134 }
2135}
2136
2137pub(crate) fn sentence_backwards(
2138 map: &DisplaySnapshot,
2139 point: DisplayPoint,
2140 mut times: usize,
2141) -> DisplayPoint {
2142 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
2143 let mut chars = map.reverse_buffer_chars_at(start).peekable();
2144
2145 let mut was_newline = map
2146 .buffer_chars_at(start)
2147 .next()
2148 .is_some_and(|(c, _)| c == '\n');
2149
2150 while let Some((ch, offset)) = chars.next() {
2151 let start_of_next_sentence = if was_newline && ch == '\n' {
2152 Some(offset + ch.len_utf8())
2153 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2154 Some(next_non_blank(map, offset + ch.len_utf8()))
2155 } else if ch == '.' || ch == '?' || ch == '!' {
2156 start_of_next_sentence(map, offset + ch.len_utf8())
2157 } else {
2158 None
2159 };
2160
2161 if let Some(start_of_next_sentence) = start_of_next_sentence {
2162 if start_of_next_sentence < start {
2163 times = times.saturating_sub(1);
2164 }
2165 if times == 0 || offset == 0 {
2166 return map.clip_point(
2167 start_of_next_sentence
2168 .to_offset(&map.buffer_snapshot)
2169 .to_display_point(map),
2170 Bias::Left,
2171 );
2172 }
2173 }
2174 if was_newline {
2175 start = offset;
2176 }
2177 was_newline = ch == '\n';
2178 }
2179
2180 DisplayPoint::zero()
2181}
2182
2183pub(crate) fn sentence_forwards(
2184 map: &DisplaySnapshot,
2185 point: DisplayPoint,
2186 mut times: usize,
2187) -> DisplayPoint {
2188 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
2189 let mut chars = map.buffer_chars_at(start).peekable();
2190
2191 let mut was_newline = map
2192 .reverse_buffer_chars_at(start)
2193 .next()
2194 .is_some_and(|(c, _)| c == '\n')
2195 && chars.peek().is_some_and(|(c, _)| *c == '\n');
2196
2197 while let Some((ch, offset)) = chars.next() {
2198 if was_newline && ch == '\n' {
2199 continue;
2200 }
2201 let start_of_next_sentence = if was_newline {
2202 Some(next_non_blank(map, offset))
2203 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2204 Some(next_non_blank(map, offset + ch.len_utf8()))
2205 } else if ch == '.' || ch == '?' || ch == '!' {
2206 start_of_next_sentence(map, offset + ch.len_utf8())
2207 } else {
2208 None
2209 };
2210
2211 if let Some(start_of_next_sentence) = start_of_next_sentence {
2212 times = times.saturating_sub(1);
2213 if times == 0 {
2214 return map.clip_point(
2215 start_of_next_sentence
2216 .to_offset(&map.buffer_snapshot)
2217 .to_display_point(map),
2218 Bias::Right,
2219 );
2220 }
2221 }
2222
2223 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2224 }
2225
2226 map.max_point()
2227}
2228
2229fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
2230 for (c, o) in map.buffer_chars_at(start) {
2231 if c == '\n' || !c.is_whitespace() {
2232 return o;
2233 }
2234 }
2235
2236 map.buffer_snapshot.len()
2237}
2238
2239// given the offset after a ., !, or ? find the start of the next sentence.
2240// if this is not a sentence boundary, returns None.
2241fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
2242 let chars = map.buffer_chars_at(end_of_sentence);
2243 let mut seen_space = false;
2244
2245 for (char, offset) in chars {
2246 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2247 continue;
2248 }
2249
2250 if char == '\n' && seen_space {
2251 return Some(offset);
2252 } else if char.is_whitespace() {
2253 seen_space = true;
2254 } else if seen_space {
2255 return Some(offset);
2256 } else {
2257 return None;
2258 }
2259 }
2260
2261 Some(map.buffer_snapshot.len())
2262}
2263
2264fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2265 let point = map.display_point_to_point(display_point, Bias::Left);
2266 let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2267 return display_point;
2268 };
2269 let offset = excerpt.buffer().point_to_offset(
2270 excerpt
2271 .buffer()
2272 .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2273 );
2274 let buffer_range = excerpt.buffer_range();
2275 if offset >= buffer_range.start && offset <= buffer_range.end {
2276 let point = map
2277 .buffer_snapshot
2278 .offset_to_point(excerpt.map_offset_from_buffer(offset));
2279 return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2280 }
2281 let mut last_position = None;
2282 for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2283 let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
2284 ..language::ToOffset::to_offset(&range.context.end, buffer);
2285 if offset >= excerpt_range.start && offset <= excerpt_range.end {
2286 let text_anchor = buffer.anchor_after(offset);
2287 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2288 return anchor.to_display_point(map);
2289 } else if offset <= excerpt_range.start {
2290 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2291 return anchor.to_display_point(map);
2292 } else {
2293 last_position = Some(Anchor::in_buffer(
2294 excerpt,
2295 buffer.remote_id(),
2296 range.context.end,
2297 ));
2298 }
2299 }
2300
2301 let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2302 last_point.column = point.column;
2303
2304 map.clip_point(
2305 map.point_to_display_point(
2306 map.buffer_snapshot.clip_point(point, Bias::Left),
2307 Bias::Left,
2308 ),
2309 Bias::Left,
2310 )
2311}
2312
2313fn start_of_document(
2314 map: &DisplaySnapshot,
2315 display_point: DisplayPoint,
2316 maybe_times: Option<usize>,
2317) -> DisplayPoint {
2318 if let Some(times) = maybe_times {
2319 return go_to_line(map, display_point, times);
2320 }
2321
2322 let point = map.display_point_to_point(display_point, Bias::Left);
2323 let mut first_point = Point::zero();
2324 first_point.column = point.column;
2325
2326 map.clip_point(
2327 map.point_to_display_point(
2328 map.buffer_snapshot.clip_point(first_point, Bias::Left),
2329 Bias::Left,
2330 ),
2331 Bias::Left,
2332 )
2333}
2334
2335fn end_of_document(
2336 map: &DisplaySnapshot,
2337 display_point: DisplayPoint,
2338 maybe_times: Option<usize>,
2339) -> DisplayPoint {
2340 if let Some(times) = maybe_times {
2341 return go_to_line(map, display_point, times);
2342 };
2343 let point = map.display_point_to_point(display_point, Bias::Left);
2344 let mut last_point = map.buffer_snapshot.max_point();
2345 last_point.column = point.column;
2346
2347 map.clip_point(
2348 map.point_to_display_point(
2349 map.buffer_snapshot.clip_point(last_point, Bias::Left),
2350 Bias::Left,
2351 ),
2352 Bias::Left,
2353 )
2354}
2355
2356fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2357 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2358 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2359
2360 if head > outer.start && head < inner.start {
2361 let mut offset = inner.end.to_offset(map, Bias::Left);
2362 for c in map.buffer_snapshot.chars_at(offset) {
2363 if c == '/' || c == '\n' || c == '>' {
2364 return Some(offset.to_display_point(map));
2365 }
2366 offset += c.len_utf8();
2367 }
2368 } else {
2369 let mut offset = outer.start.to_offset(map, Bias::Left);
2370 for c in map.buffer_snapshot.chars_at(offset) {
2371 offset += c.len_utf8();
2372 if c == '<' || c == '\n' {
2373 return Some(offset.to_display_point(map));
2374 }
2375 }
2376 }
2377
2378 None
2379}
2380
2381fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2382 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2383 let display_point = map.clip_at_line_end(display_point);
2384 let point = display_point.to_point(map);
2385 let offset = point.to_offset(&map.buffer_snapshot);
2386
2387 // Ensure the range is contained by the current line.
2388 let mut line_end = map.next_line_boundary(point).0;
2389 if line_end == point {
2390 line_end = map.max_point().to_point(map);
2391 }
2392
2393 if let Some((opening_range, closing_range)) = map
2394 .buffer_snapshot
2395 .innermost_enclosing_bracket_ranges(offset..offset, None)
2396 {
2397 if opening_range.contains(&offset) {
2398 return closing_range.start.to_display_point(map);
2399 } else if closing_range.contains(&offset) {
2400 return opening_range.start.to_display_point(map);
2401 }
2402 }
2403
2404 let line_range = map.prev_line_boundary(point).0..line_end;
2405 let visible_line_range =
2406 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2407 let ranges = map.buffer_snapshot.bracket_ranges(visible_line_range);
2408 if let Some(ranges) = ranges {
2409 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2410 ..line_range.end.to_offset(&map.buffer_snapshot);
2411 let mut closest_pair_destination = None;
2412 let mut closest_distance = usize::MAX;
2413
2414 for (open_range, close_range) in ranges {
2415 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2416 if offset > open_range.start && offset < close_range.start {
2417 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2418 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2419 return display_point;
2420 }
2421 if let Some(tag) = matching_tag(map, display_point) {
2422 return tag;
2423 }
2424 } else if close_range.contains(&offset) {
2425 return open_range.start.to_display_point(map);
2426 } else if open_range.contains(&offset) {
2427 return (close_range.end - 1).to_display_point(map);
2428 }
2429 }
2430
2431 if (open_range.contains(&offset) || open_range.start >= offset)
2432 && line_range.contains(&open_range.start)
2433 {
2434 let distance = open_range.start.saturating_sub(offset);
2435 if distance < closest_distance {
2436 closest_pair_destination = Some(close_range.start);
2437 closest_distance = distance;
2438 continue;
2439 }
2440 }
2441
2442 if (close_range.contains(&offset) || close_range.start >= offset)
2443 && line_range.contains(&close_range.start)
2444 {
2445 let distance = close_range.start.saturating_sub(offset);
2446 if distance < closest_distance {
2447 closest_pair_destination = Some(open_range.start);
2448 closest_distance = distance;
2449 continue;
2450 }
2451 }
2452
2453 continue;
2454 }
2455
2456 closest_pair_destination
2457 .map(|destination| destination.to_display_point(map))
2458 .unwrap_or(display_point)
2459 } else {
2460 display_point
2461 }
2462}
2463
2464// Go to {count} percentage in the file, on the first
2465// non-blank in the line linewise. To compute the new
2466// line number this formula is used:
2467// ({count} * number-of-lines + 99) / 100
2468//
2469// https://neovim.io/doc/user/motion.html#N%25
2470fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2471 let total_lines = map.buffer_snapshot.max_point().row + 1;
2472 let target_line = (count * total_lines as usize).div_ceil(100);
2473 let target_point = DisplayPoint::new(
2474 DisplayRow(target_line.saturating_sub(1) as u32),
2475 point.column(),
2476 );
2477 map.clip_point(target_point, Bias::Left)
2478}
2479
2480fn unmatched_forward(
2481 map: &DisplaySnapshot,
2482 mut display_point: DisplayPoint,
2483 char: char,
2484 times: usize,
2485) -> DisplayPoint {
2486 for _ in 0..times {
2487 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2488 let point = display_point.to_point(map);
2489 let offset = point.to_offset(&map.buffer_snapshot);
2490
2491 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2492 let Some(ranges) = ranges else { break };
2493 let mut closest_closing_destination = None;
2494 let mut closest_distance = usize::MAX;
2495
2496 for (_, close_range) in ranges {
2497 if close_range.start > offset {
2498 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2499 if Some(char) == chars.next() {
2500 let distance = close_range.start - offset;
2501 if distance < closest_distance {
2502 closest_closing_destination = Some(close_range.start);
2503 closest_distance = distance;
2504 continue;
2505 }
2506 }
2507 }
2508 }
2509
2510 let new_point = closest_closing_destination
2511 .map(|destination| destination.to_display_point(map))
2512 .unwrap_or(display_point);
2513 if new_point == display_point {
2514 break;
2515 }
2516 display_point = new_point;
2517 }
2518 display_point
2519}
2520
2521fn unmatched_backward(
2522 map: &DisplaySnapshot,
2523 mut display_point: DisplayPoint,
2524 char: char,
2525 times: usize,
2526) -> DisplayPoint {
2527 for _ in 0..times {
2528 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2529 let point = display_point.to_point(map);
2530 let offset = point.to_offset(&map.buffer_snapshot);
2531
2532 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2533 let Some(ranges) = ranges else {
2534 break;
2535 };
2536
2537 let mut closest_starting_destination = None;
2538 let mut closest_distance = usize::MAX;
2539
2540 for (start_range, _) in ranges {
2541 if start_range.start < offset {
2542 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2543 if Some(char) == chars.next() {
2544 let distance = offset - start_range.start;
2545 if distance < closest_distance {
2546 closest_starting_destination = Some(start_range.start);
2547 closest_distance = distance;
2548 continue;
2549 }
2550 }
2551 }
2552 }
2553
2554 let new_point = closest_starting_destination
2555 .map(|destination| destination.to_display_point(map))
2556 .unwrap_or(display_point);
2557 if new_point == display_point {
2558 break;
2559 } else {
2560 display_point = new_point;
2561 }
2562 }
2563 display_point
2564}
2565
2566fn find_forward(
2567 map: &DisplaySnapshot,
2568 from: DisplayPoint,
2569 before: bool,
2570 target: char,
2571 times: usize,
2572 mode: FindRange,
2573 smartcase: bool,
2574) -> Option<DisplayPoint> {
2575 let mut to = from;
2576 let mut found = false;
2577
2578 for _ in 0..times {
2579 found = false;
2580 let new_to = find_boundary(map, to, mode, |_, right| {
2581 found = is_character_match(target, right, smartcase);
2582 found
2583 });
2584 if to == new_to {
2585 break;
2586 }
2587 to = new_to;
2588 }
2589
2590 if found {
2591 if before && to.column() > 0 {
2592 *to.column_mut() -= 1;
2593 Some(map.clip_point(to, Bias::Left))
2594 } else if before && to.row().0 > 0 {
2595 *to.row_mut() -= 1;
2596 *to.column_mut() = map.line(to.row()).len() as u32;
2597 Some(map.clip_point(to, Bias::Left))
2598 } else {
2599 Some(to)
2600 }
2601 } else {
2602 None
2603 }
2604}
2605
2606fn find_backward(
2607 map: &DisplaySnapshot,
2608 from: DisplayPoint,
2609 after: bool,
2610 target: char,
2611 times: usize,
2612 mode: FindRange,
2613 smartcase: bool,
2614) -> DisplayPoint {
2615 let mut to = from;
2616
2617 for _ in 0..times {
2618 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2619 is_character_match(target, right, smartcase)
2620 });
2621 if to == new_to {
2622 break;
2623 }
2624 to = new_to;
2625 }
2626
2627 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2628 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2629 if after {
2630 *to.column_mut() += 1;
2631 map.clip_point(to, Bias::Right)
2632 } else {
2633 to
2634 }
2635 } else {
2636 from
2637 }
2638}
2639
2640/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2641pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2642 if smartcase {
2643 if target.is_uppercase() {
2644 target == other
2645 } else {
2646 target == other.to_ascii_lowercase()
2647 }
2648 } else {
2649 target == other
2650 }
2651}
2652
2653fn sneak(
2654 map: &DisplaySnapshot,
2655 from: DisplayPoint,
2656 first_target: char,
2657 second_target: char,
2658 times: usize,
2659 smartcase: bool,
2660) -> Option<DisplayPoint> {
2661 let mut to = from;
2662 let mut found = false;
2663
2664 for _ in 0..times {
2665 found = false;
2666 let new_to = find_boundary(
2667 map,
2668 movement::right(map, to),
2669 FindRange::MultiLine,
2670 |left, right| {
2671 found = is_character_match(first_target, left, smartcase)
2672 && is_character_match(second_target, right, smartcase);
2673 found
2674 },
2675 );
2676 if to == new_to {
2677 break;
2678 }
2679 to = new_to;
2680 }
2681
2682 if found {
2683 Some(movement::left(map, to))
2684 } else {
2685 None
2686 }
2687}
2688
2689fn sneak_backward(
2690 map: &DisplaySnapshot,
2691 from: DisplayPoint,
2692 first_target: char,
2693 second_target: char,
2694 times: usize,
2695 smartcase: bool,
2696) -> Option<DisplayPoint> {
2697 let mut to = from;
2698 let mut found = false;
2699
2700 for _ in 0..times {
2701 found = false;
2702 let new_to =
2703 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2704 found = is_character_match(first_target, left, smartcase)
2705 && is_character_match(second_target, right, smartcase);
2706 found
2707 });
2708 if to == new_to {
2709 break;
2710 }
2711 to = new_to;
2712 }
2713
2714 if found {
2715 Some(movement::left(map, to))
2716 } else {
2717 None
2718 }
2719}
2720
2721fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2722 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2723 first_non_whitespace(map, false, correct_line)
2724}
2725
2726fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2727 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2728 first_non_whitespace(map, false, correct_line)
2729}
2730
2731fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2732 let correct_line = start_of_relative_buffer_row(map, point, 0);
2733 right(map, correct_line, times.saturating_sub(1))
2734}
2735
2736pub(crate) fn next_line_end(
2737 map: &DisplaySnapshot,
2738 mut point: DisplayPoint,
2739 times: usize,
2740) -> DisplayPoint {
2741 if times > 1 {
2742 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2743 }
2744 end_of_line(map, false, point, 1)
2745}
2746
2747fn window_top(
2748 map: &DisplaySnapshot,
2749 point: DisplayPoint,
2750 text_layout_details: &TextLayoutDetails,
2751 mut times: usize,
2752) -> (DisplayPoint, SelectionGoal) {
2753 let first_visible_line = text_layout_details
2754 .scroll_anchor
2755 .anchor
2756 .to_display_point(map);
2757
2758 if first_visible_line.row() != DisplayRow(0)
2759 && text_layout_details.vertical_scroll_margin as usize > times
2760 {
2761 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2762 }
2763
2764 if let Some(visible_rows) = text_layout_details.visible_rows {
2765 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2766 let new_row = (first_visible_line.row().0 + (times as u32))
2767 .min(bottom_row)
2768 .min(map.max_point().row().0);
2769 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2770
2771 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2772 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2773 } else {
2774 let new_row =
2775 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2776 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2777
2778 let new_point = DisplayPoint::new(new_row, new_col);
2779 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2780 }
2781}
2782
2783fn window_middle(
2784 map: &DisplaySnapshot,
2785 point: DisplayPoint,
2786 text_layout_details: &TextLayoutDetails,
2787) -> (DisplayPoint, SelectionGoal) {
2788 if let Some(visible_rows) = text_layout_details.visible_rows {
2789 let first_visible_line = text_layout_details
2790 .scroll_anchor
2791 .anchor
2792 .to_display_point(map);
2793
2794 let max_visible_rows =
2795 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2796
2797 let new_row =
2798 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2799 let new_row = DisplayRow(new_row);
2800 let new_col = point.column().min(map.line_len(new_row));
2801 let new_point = DisplayPoint::new(new_row, new_col);
2802 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2803 } else {
2804 (point, SelectionGoal::None)
2805 }
2806}
2807
2808fn window_bottom(
2809 map: &DisplaySnapshot,
2810 point: DisplayPoint,
2811 text_layout_details: &TextLayoutDetails,
2812 mut times: usize,
2813) -> (DisplayPoint, SelectionGoal) {
2814 if let Some(visible_rows) = text_layout_details.visible_rows {
2815 let first_visible_line = text_layout_details
2816 .scroll_anchor
2817 .anchor
2818 .to_display_point(map);
2819 let bottom_row = first_visible_line.row().0
2820 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2821 if bottom_row < map.max_point().row().0
2822 && text_layout_details.vertical_scroll_margin as usize > times
2823 {
2824 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2825 }
2826 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2827 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2828 {
2829 first_visible_line.row()
2830 } else {
2831 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2832 };
2833 let new_col = point.column().min(map.line_len(new_row));
2834 let new_point = DisplayPoint::new(new_row, new_col);
2835 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2836 } else {
2837 (point, SelectionGoal::None)
2838 }
2839}
2840
2841fn method_motion(
2842 map: &DisplaySnapshot,
2843 mut display_point: DisplayPoint,
2844 times: usize,
2845 direction: Direction,
2846 is_start: bool,
2847) -> DisplayPoint {
2848 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2849 return display_point;
2850 };
2851
2852 for _ in 0..times {
2853 let point = map.display_point_to_point(display_point, Bias::Left);
2854 let offset = point.to_offset(&map.buffer_snapshot);
2855 let range = if direction == Direction::Prev {
2856 0..offset
2857 } else {
2858 offset..buffer.len()
2859 };
2860
2861 let possibilities = buffer
2862 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2863 .filter_map(|(range, object)| {
2864 if !matches!(object, language::TextObject::AroundFunction) {
2865 return None;
2866 }
2867
2868 let relevant = if is_start { range.start } else { range.end };
2869 if direction == Direction::Prev && relevant < offset {
2870 Some(relevant)
2871 } else if direction == Direction::Next && relevant > offset + 1 {
2872 Some(relevant)
2873 } else {
2874 None
2875 }
2876 });
2877
2878 let dest = if direction == Direction::Prev {
2879 possibilities.max().unwrap_or(offset)
2880 } else {
2881 possibilities.min().unwrap_or(offset)
2882 };
2883 let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2884 if new_point == display_point {
2885 break;
2886 }
2887 display_point = new_point;
2888 }
2889 display_point
2890}
2891
2892fn comment_motion(
2893 map: &DisplaySnapshot,
2894 mut display_point: DisplayPoint,
2895 times: usize,
2896 direction: Direction,
2897) -> DisplayPoint {
2898 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2899 return display_point;
2900 };
2901
2902 for _ in 0..times {
2903 let point = map.display_point_to_point(display_point, Bias::Left);
2904 let offset = point.to_offset(&map.buffer_snapshot);
2905 let range = if direction == Direction::Prev {
2906 0..offset
2907 } else {
2908 offset..buffer.len()
2909 };
2910
2911 let possibilities = buffer
2912 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2913 .filter_map(|(range, object)| {
2914 if !matches!(object, language::TextObject::AroundComment) {
2915 return None;
2916 }
2917
2918 let relevant = if direction == Direction::Prev {
2919 range.start
2920 } else {
2921 range.end
2922 };
2923 if direction == Direction::Prev && relevant < offset {
2924 Some(relevant)
2925 } else if direction == Direction::Next && relevant > offset + 1 {
2926 Some(relevant)
2927 } else {
2928 None
2929 }
2930 });
2931
2932 let dest = if direction == Direction::Prev {
2933 possibilities.max().unwrap_or(offset)
2934 } else {
2935 possibilities.min().unwrap_or(offset)
2936 };
2937 let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2938 if new_point == display_point {
2939 break;
2940 }
2941 display_point = new_point;
2942 }
2943
2944 display_point
2945}
2946
2947fn section_motion(
2948 map: &DisplaySnapshot,
2949 mut display_point: DisplayPoint,
2950 times: usize,
2951 direction: Direction,
2952 is_start: bool,
2953) -> DisplayPoint {
2954 if map.buffer_snapshot.as_singleton().is_some() {
2955 for _ in 0..times {
2956 let offset = map
2957 .display_point_to_point(display_point, Bias::Left)
2958 .to_offset(&map.buffer_snapshot);
2959 let range = if direction == Direction::Prev {
2960 0..offset
2961 } else {
2962 offset..map.buffer_snapshot.len()
2963 };
2964
2965 // we set a max start depth here because we want a section to only be "top level"
2966 // similar to vim's default of '{' in the first column.
2967 // (and without it, ]] at the start of editor.rs is -very- slow)
2968 let mut possibilities = map
2969 .buffer_snapshot
2970 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2971 .filter(|(_, object)| {
2972 matches!(
2973 object,
2974 language::TextObject::AroundClass | language::TextObject::AroundFunction
2975 )
2976 })
2977 .collect::<Vec<_>>();
2978 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2979 let mut prev_end = None;
2980 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2981 if t == language::TextObject::AroundFunction
2982 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2983 {
2984 return None;
2985 }
2986 prev_end = Some(range.end);
2987
2988 let relevant = if is_start { range.start } else { range.end };
2989 if direction == Direction::Prev && relevant < offset {
2990 Some(relevant)
2991 } else if direction == Direction::Next && relevant > offset + 1 {
2992 Some(relevant)
2993 } else {
2994 None
2995 }
2996 });
2997
2998 let offset = if direction == Direction::Prev {
2999 possibilities.max().unwrap_or(0)
3000 } else {
3001 possibilities.min().unwrap_or(map.buffer_snapshot.len())
3002 };
3003
3004 let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3005 if new_point == display_point {
3006 break;
3007 }
3008 display_point = new_point;
3009 }
3010 return display_point;
3011 };
3012
3013 for _ in 0..times {
3014 let next_point = if is_start {
3015 movement::start_of_excerpt(map, display_point, direction)
3016 } else {
3017 movement::end_of_excerpt(map, display_point, direction)
3018 };
3019 if next_point == display_point {
3020 break;
3021 }
3022 display_point = next_point;
3023 }
3024
3025 display_point
3026}
3027
3028fn matches_indent_type(
3029 target_indent: &text::LineIndent,
3030 current_indent: &text::LineIndent,
3031 indent_type: IndentType,
3032) -> bool {
3033 match indent_type {
3034 IndentType::Lesser => {
3035 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3036 }
3037 IndentType::Greater => {
3038 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3039 }
3040 IndentType::Same => {
3041 target_indent.spaces == current_indent.spaces
3042 && target_indent.tabs == current_indent.tabs
3043 }
3044 }
3045}
3046
3047fn indent_motion(
3048 map: &DisplaySnapshot,
3049 mut display_point: DisplayPoint,
3050 times: usize,
3051 direction: Direction,
3052 indent_type: IndentType,
3053) -> DisplayPoint {
3054 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3055 let current_row = MultiBufferRow(buffer_point.row);
3056 let current_indent = map.line_indent_for_buffer_row(current_row);
3057 if current_indent.is_line_empty() {
3058 return display_point;
3059 }
3060 let max_row = map.max_point().to_point(map).row;
3061
3062 for _ in 0..times {
3063 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3064
3065 let target_row = match direction {
3066 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3067 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3068 !indent.is_line_empty()
3069 && matches_indent_type(&indent, ¤t_indent, indent_type)
3070 }),
3071 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3072 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3073 !indent.is_line_empty()
3074 && matches_indent_type(&indent, ¤t_indent, indent_type)
3075 }),
3076 }
3077 .unwrap_or(current_buffer_row);
3078
3079 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3080 let new_point = first_non_whitespace(map, false, new_point);
3081 if new_point == display_point {
3082 break;
3083 }
3084 display_point = new_point;
3085 }
3086 display_point
3087}
3088
3089#[cfg(test)]
3090mod test {
3091
3092 use crate::{
3093 state::Mode,
3094 test::{NeovimBackedTestContext, VimTestContext},
3095 };
3096 use editor::display_map::Inlay;
3097 use indoc::indoc;
3098 use language::Point;
3099 use multi_buffer::MultiBufferRow;
3100
3101 #[gpui::test]
3102 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3103 let mut cx = NeovimBackedTestContext::new(cx).await;
3104
3105 let initial_state = indoc! {r"ˇabc
3106 def
3107
3108 paragraph
3109 the second
3110
3111
3112
3113 third and
3114 final"};
3115
3116 // goes down once
3117 cx.set_shared_state(initial_state).await;
3118 cx.simulate_shared_keystrokes("}").await;
3119 cx.shared_state().await.assert_eq(indoc! {r"abc
3120 def
3121 ˇ
3122 paragraph
3123 the second
3124
3125
3126
3127 third and
3128 final"});
3129
3130 // goes up once
3131 cx.simulate_shared_keystrokes("{").await;
3132 cx.shared_state().await.assert_eq(initial_state);
3133
3134 // goes down twice
3135 cx.simulate_shared_keystrokes("2 }").await;
3136 cx.shared_state().await.assert_eq(indoc! {r"abc
3137 def
3138
3139 paragraph
3140 the second
3141 ˇ
3142
3143
3144 third and
3145 final"});
3146
3147 // goes down over multiple blanks
3148 cx.simulate_shared_keystrokes("}").await;
3149 cx.shared_state().await.assert_eq(indoc! {r"abc
3150 def
3151
3152 paragraph
3153 the second
3154
3155
3156
3157 third and
3158 finaˇl"});
3159
3160 // goes up twice
3161 cx.simulate_shared_keystrokes("2 {").await;
3162 cx.shared_state().await.assert_eq(indoc! {r"abc
3163 def
3164 ˇ
3165 paragraph
3166 the second
3167
3168
3169
3170 third and
3171 final"});
3172 }
3173
3174 #[gpui::test]
3175 async fn test_matching(cx: &mut gpui::TestAppContext) {
3176 let mut cx = NeovimBackedTestContext::new(cx).await;
3177
3178 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3179 do(something(with<Types>.and_arrays[0, 2]))
3180 }"})
3181 .await;
3182 cx.simulate_shared_keystrokes("%").await;
3183 cx.shared_state()
3184 .await
3185 .assert_eq(indoc! {r"func (a stringˇ) {
3186 do(something(with<Types>.and_arrays[0, 2]))
3187 }"});
3188
3189 // test it works on the last character of the line
3190 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3191 do(something(with<Types>.and_arrays[0, 2]))
3192 }"})
3193 .await;
3194 cx.simulate_shared_keystrokes("%").await;
3195 cx.shared_state()
3196 .await
3197 .assert_eq(indoc! {r"func (a string) {
3198 do(something(with<Types>.and_arrays[0, 2]))
3199 ˇ}"});
3200
3201 // test it works on immediate nesting
3202 cx.set_shared_state("ˇ{()}").await;
3203 cx.simulate_shared_keystrokes("%").await;
3204 cx.shared_state().await.assert_eq("{()ˇ}");
3205 cx.simulate_shared_keystrokes("%").await;
3206 cx.shared_state().await.assert_eq("ˇ{()}");
3207
3208 // test it works on immediate nesting inside braces
3209 cx.set_shared_state("{\n ˇ{()}\n}").await;
3210 cx.simulate_shared_keystrokes("%").await;
3211 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3212
3213 // test it jumps to the next paren on a line
3214 cx.set_shared_state("func ˇboop() {\n}").await;
3215 cx.simulate_shared_keystrokes("%").await;
3216 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3217 }
3218
3219 #[gpui::test]
3220 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3221 let mut cx = NeovimBackedTestContext::new(cx).await;
3222
3223 // test it works with curly braces
3224 cx.set_shared_state(indoc! {r"func (a string) {
3225 do(something(with<Types>.anˇd_arrays[0, 2]))
3226 }"})
3227 .await;
3228 cx.simulate_shared_keystrokes("] }").await;
3229 cx.shared_state()
3230 .await
3231 .assert_eq(indoc! {r"func (a string) {
3232 do(something(with<Types>.and_arrays[0, 2]))
3233 ˇ}"});
3234
3235 // test it works with brackets
3236 cx.set_shared_state(indoc! {r"func (a string) {
3237 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3238 }"})
3239 .await;
3240 cx.simulate_shared_keystrokes("] )").await;
3241 cx.shared_state()
3242 .await
3243 .assert_eq(indoc! {r"func (a string) {
3244 do(something(with<Types>.and_arrays[0, 2])ˇ)
3245 }"});
3246
3247 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3248 .await;
3249 cx.simulate_shared_keystrokes("] )").await;
3250 cx.shared_state()
3251 .await
3252 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3253
3254 // test it works on immediate nesting
3255 cx.set_shared_state("{ˇ {}{}}").await;
3256 cx.simulate_shared_keystrokes("] }").await;
3257 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3258 cx.set_shared_state("(ˇ ()())").await;
3259 cx.simulate_shared_keystrokes("] )").await;
3260 cx.shared_state().await.assert_eq("( ()()ˇ)");
3261
3262 // test it works on immediate nesting inside braces
3263 cx.set_shared_state("{\n ˇ {()}\n}").await;
3264 cx.simulate_shared_keystrokes("] }").await;
3265 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3266 cx.set_shared_state("(\n ˇ {()}\n)").await;
3267 cx.simulate_shared_keystrokes("] )").await;
3268 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3269 }
3270
3271 #[gpui::test]
3272 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3273 let mut cx = NeovimBackedTestContext::new(cx).await;
3274
3275 // test it works with curly braces
3276 cx.set_shared_state(indoc! {r"func (a string) {
3277 do(something(with<Types>.anˇd_arrays[0, 2]))
3278 }"})
3279 .await;
3280 cx.simulate_shared_keystrokes("[ {").await;
3281 cx.shared_state()
3282 .await
3283 .assert_eq(indoc! {r"func (a string) ˇ{
3284 do(something(with<Types>.and_arrays[0, 2]))
3285 }"});
3286
3287 // test it works with brackets
3288 cx.set_shared_state(indoc! {r"func (a string) {
3289 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3290 }"})
3291 .await;
3292 cx.simulate_shared_keystrokes("[ (").await;
3293 cx.shared_state()
3294 .await
3295 .assert_eq(indoc! {r"func (a string) {
3296 doˇ(something(with<Types>.and_arrays[0, 2]))
3297 }"});
3298
3299 // test it works on immediate nesting
3300 cx.set_shared_state("{{}{} ˇ }").await;
3301 cx.simulate_shared_keystrokes("[ {").await;
3302 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3303 cx.set_shared_state("(()() ˇ )").await;
3304 cx.simulate_shared_keystrokes("[ (").await;
3305 cx.shared_state().await.assert_eq("ˇ(()() )");
3306
3307 // test it works on immediate nesting inside braces
3308 cx.set_shared_state("{\n {()} ˇ\n}").await;
3309 cx.simulate_shared_keystrokes("[ {").await;
3310 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3311 cx.set_shared_state("(\n {()} ˇ\n)").await;
3312 cx.simulate_shared_keystrokes("[ (").await;
3313 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3314 }
3315
3316 #[gpui::test]
3317 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3318 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3319
3320 cx.neovim.exec("set filetype=html").await;
3321
3322 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3323 cx.simulate_shared_keystrokes("%").await;
3324 cx.shared_state()
3325 .await
3326 .assert_eq(indoc! {r"<body><ˇ/body>"});
3327 cx.simulate_shared_keystrokes("%").await;
3328
3329 // test jumping backwards
3330 cx.shared_state()
3331 .await
3332 .assert_eq(indoc! {r"<ˇbody></body>"});
3333
3334 // test self-closing tags
3335 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3336 cx.simulate_shared_keystrokes("%").await;
3337 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3338
3339 // test tag with attributes
3340 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3341 </div>
3342 "})
3343 .await;
3344 cx.simulate_shared_keystrokes("%").await;
3345 cx.shared_state()
3346 .await
3347 .assert_eq(indoc! {r"<div class='test' id='main'>
3348 <ˇ/div>
3349 "});
3350
3351 // test multi-line self-closing tag
3352 cx.set_shared_state(indoc! {r#"<a>
3353 <br
3354 test = "test"
3355 /ˇ>
3356 </a>"#})
3357 .await;
3358 cx.simulate_shared_keystrokes("%").await;
3359 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3360 ˇ<br
3361 test = "test"
3362 />
3363 </a>"#});
3364 }
3365
3366 #[gpui::test]
3367 async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3368 let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3369
3370 // test brackets within tags
3371 cx.set_shared_state(indoc! {r"function f() {
3372 return (
3373 <div rules={ˇ[{ a: 1 }]}>
3374 <h1>test</h1>
3375 </div>
3376 );
3377 }"})
3378 .await;
3379 cx.simulate_shared_keystrokes("%").await;
3380 cx.shared_state().await.assert_eq(indoc! {r"function f() {
3381 return (
3382 <div rules={[{ a: 1 }ˇ]}>
3383 <h1>test</h1>
3384 </div>
3385 );
3386 }"});
3387 }
3388
3389 #[gpui::test]
3390 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3391 let mut cx = NeovimBackedTestContext::new(cx).await;
3392
3393 // f and F
3394 cx.set_shared_state("ˇone two three four").await;
3395 cx.simulate_shared_keystrokes("f o").await;
3396 cx.shared_state().await.assert_eq("one twˇo three four");
3397 cx.simulate_shared_keystrokes(",").await;
3398 cx.shared_state().await.assert_eq("ˇone two three four");
3399 cx.simulate_shared_keystrokes("2 ;").await;
3400 cx.shared_state().await.assert_eq("one two three fˇour");
3401 cx.simulate_shared_keystrokes("shift-f e").await;
3402 cx.shared_state().await.assert_eq("one two threˇe four");
3403 cx.simulate_shared_keystrokes("2 ;").await;
3404 cx.shared_state().await.assert_eq("onˇe two three four");
3405 cx.simulate_shared_keystrokes(",").await;
3406 cx.shared_state().await.assert_eq("one two thrˇee four");
3407
3408 // t and T
3409 cx.set_shared_state("ˇone two three four").await;
3410 cx.simulate_shared_keystrokes("t o").await;
3411 cx.shared_state().await.assert_eq("one tˇwo three four");
3412 cx.simulate_shared_keystrokes(",").await;
3413 cx.shared_state().await.assert_eq("oˇne two three four");
3414 cx.simulate_shared_keystrokes("2 ;").await;
3415 cx.shared_state().await.assert_eq("one two three ˇfour");
3416 cx.simulate_shared_keystrokes("shift-t e").await;
3417 cx.shared_state().await.assert_eq("one two threeˇ four");
3418 cx.simulate_shared_keystrokes("3 ;").await;
3419 cx.shared_state().await.assert_eq("oneˇ two three four");
3420 cx.simulate_shared_keystrokes(",").await;
3421 cx.shared_state().await.assert_eq("one two thˇree four");
3422 }
3423
3424 #[gpui::test]
3425 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3426 let mut cx = NeovimBackedTestContext::new(cx).await;
3427 let initial_state = indoc! {r"something(ˇfoo)"};
3428 cx.set_shared_state(initial_state).await;
3429 cx.simulate_shared_keystrokes("}").await;
3430 cx.shared_state().await.assert_eq("something(fooˇ)");
3431 }
3432
3433 #[gpui::test]
3434 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3435 let mut cx = NeovimBackedTestContext::new(cx).await;
3436 cx.set_shared_state("ˇone\n two\nthree").await;
3437 cx.simulate_shared_keystrokes("enter").await;
3438 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3439 }
3440
3441 #[gpui::test]
3442 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3443 let mut cx = NeovimBackedTestContext::new(cx).await;
3444 cx.set_shared_state("ˇ one\n two \nthree").await;
3445 cx.simulate_shared_keystrokes("g _").await;
3446 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3447
3448 cx.set_shared_state("ˇ one \n two \nthree").await;
3449 cx.simulate_shared_keystrokes("g _").await;
3450 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3451 cx.simulate_shared_keystrokes("2 g _").await;
3452 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3453 }
3454
3455 #[gpui::test]
3456 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3457 let mut cx = NeovimBackedTestContext::new(cx).await;
3458 let initial_state = indoc! {r"abc
3459 def
3460 paragraph
3461 the second
3462 third ˇand
3463 final"};
3464
3465 cx.set_shared_state(initial_state).await;
3466 cx.simulate_shared_keystrokes("shift-h").await;
3467 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3468 def
3469 paragraph
3470 the second
3471 third and
3472 final"});
3473
3474 // clip point
3475 cx.set_shared_state(indoc! {r"
3476 1 2 3
3477 4 5 6
3478 7 8 ˇ9
3479 "})
3480 .await;
3481 cx.simulate_shared_keystrokes("shift-h").await;
3482 cx.shared_state().await.assert_eq(indoc! {"
3483 1 2 ˇ3
3484 4 5 6
3485 7 8 9
3486 "});
3487
3488 cx.set_shared_state(indoc! {r"
3489 1 2 3
3490 4 5 6
3491 ˇ7 8 9
3492 "})
3493 .await;
3494 cx.simulate_shared_keystrokes("shift-h").await;
3495 cx.shared_state().await.assert_eq(indoc! {"
3496 ˇ1 2 3
3497 4 5 6
3498 7 8 9
3499 "});
3500
3501 cx.set_shared_state(indoc! {r"
3502 1 2 3
3503 4 5 ˇ6
3504 7 8 9"})
3505 .await;
3506 cx.simulate_shared_keystrokes("9 shift-h").await;
3507 cx.shared_state().await.assert_eq(indoc! {"
3508 1 2 3
3509 4 5 6
3510 7 8 ˇ9"});
3511 }
3512
3513 #[gpui::test]
3514 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3515 let mut cx = NeovimBackedTestContext::new(cx).await;
3516 let initial_state = indoc! {r"abˇc
3517 def
3518 paragraph
3519 the second
3520 third and
3521 final"};
3522
3523 cx.set_shared_state(initial_state).await;
3524 cx.simulate_shared_keystrokes("shift-m").await;
3525 cx.shared_state().await.assert_eq(indoc! {r"abc
3526 def
3527 paˇragraph
3528 the second
3529 third and
3530 final"});
3531
3532 cx.set_shared_state(indoc! {r"
3533 1 2 3
3534 4 5 6
3535 7 8 ˇ9
3536 "})
3537 .await;
3538 cx.simulate_shared_keystrokes("shift-m").await;
3539 cx.shared_state().await.assert_eq(indoc! {"
3540 1 2 3
3541 4 5 ˇ6
3542 7 8 9
3543 "});
3544 cx.set_shared_state(indoc! {r"
3545 1 2 3
3546 4 5 6
3547 ˇ7 8 9
3548 "})
3549 .await;
3550 cx.simulate_shared_keystrokes("shift-m").await;
3551 cx.shared_state().await.assert_eq(indoc! {"
3552 1 2 3
3553 ˇ4 5 6
3554 7 8 9
3555 "});
3556 cx.set_shared_state(indoc! {r"
3557 ˇ1 2 3
3558 4 5 6
3559 7 8 9
3560 "})
3561 .await;
3562 cx.simulate_shared_keystrokes("shift-m").await;
3563 cx.shared_state().await.assert_eq(indoc! {"
3564 1 2 3
3565 ˇ4 5 6
3566 7 8 9
3567 "});
3568 cx.set_shared_state(indoc! {r"
3569 1 2 3
3570 ˇ4 5 6
3571 7 8 9
3572 "})
3573 .await;
3574 cx.simulate_shared_keystrokes("shift-m").await;
3575 cx.shared_state().await.assert_eq(indoc! {"
3576 1 2 3
3577 ˇ4 5 6
3578 7 8 9
3579 "});
3580 cx.set_shared_state(indoc! {r"
3581 1 2 3
3582 4 5 ˇ6
3583 7 8 9
3584 "})
3585 .await;
3586 cx.simulate_shared_keystrokes("shift-m").await;
3587 cx.shared_state().await.assert_eq(indoc! {"
3588 1 2 3
3589 4 5 ˇ6
3590 7 8 9
3591 "});
3592 }
3593
3594 #[gpui::test]
3595 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3596 let mut cx = NeovimBackedTestContext::new(cx).await;
3597 let initial_state = indoc! {r"abc
3598 deˇf
3599 paragraph
3600 the second
3601 third and
3602 final"};
3603
3604 cx.set_shared_state(initial_state).await;
3605 cx.simulate_shared_keystrokes("shift-l").await;
3606 cx.shared_state().await.assert_eq(indoc! {r"abc
3607 def
3608 paragraph
3609 the second
3610 third and
3611 fiˇnal"});
3612
3613 cx.set_shared_state(indoc! {r"
3614 1 2 3
3615 4 5 ˇ6
3616 7 8 9
3617 "})
3618 .await;
3619 cx.simulate_shared_keystrokes("shift-l").await;
3620 cx.shared_state().await.assert_eq(indoc! {"
3621 1 2 3
3622 4 5 6
3623 7 8 9
3624 ˇ"});
3625
3626 cx.set_shared_state(indoc! {r"
3627 1 2 3
3628 ˇ4 5 6
3629 7 8 9
3630 "})
3631 .await;
3632 cx.simulate_shared_keystrokes("shift-l").await;
3633 cx.shared_state().await.assert_eq(indoc! {"
3634 1 2 3
3635 4 5 6
3636 7 8 9
3637 ˇ"});
3638
3639 cx.set_shared_state(indoc! {r"
3640 1 2 ˇ3
3641 4 5 6
3642 7 8 9
3643 "})
3644 .await;
3645 cx.simulate_shared_keystrokes("shift-l").await;
3646 cx.shared_state().await.assert_eq(indoc! {"
3647 1 2 3
3648 4 5 6
3649 7 8 9
3650 ˇ"});
3651
3652 cx.set_shared_state(indoc! {r"
3653 ˇ1 2 3
3654 4 5 6
3655 7 8 9
3656 "})
3657 .await;
3658 cx.simulate_shared_keystrokes("shift-l").await;
3659 cx.shared_state().await.assert_eq(indoc! {"
3660 1 2 3
3661 4 5 6
3662 7 8 9
3663 ˇ"});
3664
3665 cx.set_shared_state(indoc! {r"
3666 1 2 3
3667 4 5 ˇ6
3668 7 8 9
3669 "})
3670 .await;
3671 cx.simulate_shared_keystrokes("9 shift-l").await;
3672 cx.shared_state().await.assert_eq(indoc! {"
3673 1 2 ˇ3
3674 4 5 6
3675 7 8 9
3676 "});
3677 }
3678
3679 #[gpui::test]
3680 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3681 let mut cx = NeovimBackedTestContext::new(cx).await;
3682 cx.set_shared_state(indoc! {r"
3683 456 5ˇ67 678
3684 "})
3685 .await;
3686 cx.simulate_shared_keystrokes("g e").await;
3687 cx.shared_state().await.assert_eq(indoc! {"
3688 45ˇ6 567 678
3689 "});
3690
3691 // Test times
3692 cx.set_shared_state(indoc! {r"
3693 123 234 345
3694 456 5ˇ67 678
3695 "})
3696 .await;
3697 cx.simulate_shared_keystrokes("4 g e").await;
3698 cx.shared_state().await.assert_eq(indoc! {"
3699 12ˇ3 234 345
3700 456 567 678
3701 "});
3702
3703 // With punctuation
3704 cx.set_shared_state(indoc! {r"
3705 123 234 345
3706 4;5.6 5ˇ67 678
3707 789 890 901
3708 "})
3709 .await;
3710 cx.simulate_shared_keystrokes("g e").await;
3711 cx.shared_state().await.assert_eq(indoc! {"
3712 123 234 345
3713 4;5.ˇ6 567 678
3714 789 890 901
3715 "});
3716
3717 // With punctuation and count
3718 cx.set_shared_state(indoc! {r"
3719 123 234 345
3720 4;5.6 5ˇ67 678
3721 789 890 901
3722 "})
3723 .await;
3724 cx.simulate_shared_keystrokes("5 g e").await;
3725 cx.shared_state().await.assert_eq(indoc! {"
3726 123 234 345
3727 ˇ4;5.6 567 678
3728 789 890 901
3729 "});
3730
3731 // newlines
3732 cx.set_shared_state(indoc! {r"
3733 123 234 345
3734
3735 78ˇ9 890 901
3736 "})
3737 .await;
3738 cx.simulate_shared_keystrokes("g e").await;
3739 cx.shared_state().await.assert_eq(indoc! {"
3740 123 234 345
3741 ˇ
3742 789 890 901
3743 "});
3744 cx.simulate_shared_keystrokes("g e").await;
3745 cx.shared_state().await.assert_eq(indoc! {"
3746 123 234 34ˇ5
3747
3748 789 890 901
3749 "});
3750
3751 // With punctuation
3752 cx.set_shared_state(indoc! {r"
3753 123 234 345
3754 4;5.ˇ6 567 678
3755 789 890 901
3756 "})
3757 .await;
3758 cx.simulate_shared_keystrokes("g shift-e").await;
3759 cx.shared_state().await.assert_eq(indoc! {"
3760 123 234 34ˇ5
3761 4;5.6 567 678
3762 789 890 901
3763 "});
3764
3765 // With multi byte char
3766 cx.set_shared_state(indoc! {r"
3767 bar ˇó
3768 "})
3769 .await;
3770 cx.simulate_shared_keystrokes("g e").await;
3771 cx.shared_state().await.assert_eq(indoc! {"
3772 baˇr ó
3773 "});
3774 }
3775
3776 #[gpui::test]
3777 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3778 let mut cx = NeovimBackedTestContext::new(cx).await;
3779
3780 cx.set_shared_state(indoc! {"
3781 fn aˇ() {
3782 return
3783 }
3784 "})
3785 .await;
3786 cx.simulate_shared_keystrokes("v $ %").await;
3787 cx.shared_state().await.assert_eq(indoc! {"
3788 fn a«() {
3789 return
3790 }ˇ»
3791 "});
3792 }
3793
3794 #[gpui::test]
3795 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3796 let mut cx = VimTestContext::new(cx, true).await;
3797
3798 cx.set_state(
3799 indoc! {"
3800 struct Foo {
3801 ˇ
3802 }
3803 "},
3804 Mode::Normal,
3805 );
3806
3807 cx.update_editor(|editor, _window, cx| {
3808 let range = editor.selections.newest_anchor().range();
3809 let inlay_text = " field: int,\n field2: string\n field3: float";
3810 let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
3811 editor.splice_inlays(&[], vec![inlay], cx);
3812 });
3813
3814 cx.simulate_keystrokes("j");
3815 cx.assert_state(
3816 indoc! {"
3817 struct Foo {
3818
3819 ˇ}
3820 "},
3821 Mode::Normal,
3822 );
3823 }
3824
3825 #[gpui::test]
3826 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3827 let mut cx = VimTestContext::new(cx, true).await;
3828
3829 cx.set_state(
3830 indoc! {"
3831 ˇstruct Foo {
3832
3833 }
3834 "},
3835 Mode::Normal,
3836 );
3837 cx.update_editor(|editor, _window, cx| {
3838 let snapshot = editor.buffer().read(cx).snapshot(cx);
3839 let end_of_line =
3840 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3841 let inlay_text = " hint";
3842 let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
3843 editor.splice_inlays(&[], vec![inlay], cx);
3844 });
3845 cx.simulate_keystrokes("$");
3846 cx.assert_state(
3847 indoc! {"
3848 struct Foo ˇ{
3849
3850 }
3851 "},
3852 Mode::Normal,
3853 );
3854 }
3855
3856 #[gpui::test]
3857 async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
3858 let mut cx = VimTestContext::new(cx, true).await;
3859
3860 // Test the exact scenario from issue #29134
3861 cx.set_state(
3862 indoc! {"
3863 fn main() {
3864 let this_is_a_long_name = Vec::<u32>::new();
3865 let new_oneˇ = this_is_a_long_name
3866 .iter()
3867 .map(|i| i + 1)
3868 .map(|i| i * 2)
3869 .collect::<Vec<_>>();
3870 }
3871 "},
3872 Mode::Normal,
3873 );
3874
3875 // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
3876 cx.update_editor(|editor, _window, cx| {
3877 let snapshot = editor.buffer().read(cx).snapshot(cx);
3878 // The empty line is at line 3 (0-indexed)
3879 let line_start = snapshot.anchor_after(Point::new(3, 0));
3880 let inlay_text = ": Vec<u32>";
3881 let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
3882 editor.splice_inlays(&[], vec![inlay], cx);
3883 });
3884
3885 // Enter visual mode
3886 cx.simulate_keystrokes("v");
3887 cx.assert_state(
3888 indoc! {"
3889 fn main() {
3890 let this_is_a_long_name = Vec::<u32>::new();
3891 let new_one« ˇ»= this_is_a_long_name
3892 .iter()
3893 .map(|i| i + 1)
3894 .map(|i| i * 2)
3895 .collect::<Vec<_>>();
3896 }
3897 "},
3898 Mode::Visual,
3899 );
3900
3901 // Move down - should go to the beginning of line 4, not skip to line 5
3902 cx.simulate_keystrokes("j");
3903 cx.assert_state(
3904 indoc! {"
3905 fn main() {
3906 let this_is_a_long_name = Vec::<u32>::new();
3907 let new_one« = this_is_a_long_name
3908 ˇ» .iter()
3909 .map(|i| i + 1)
3910 .map(|i| i * 2)
3911 .collect::<Vec<_>>();
3912 }
3913 "},
3914 Mode::Visual,
3915 );
3916
3917 // Test with multiple movements
3918 cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
3919
3920 // Add type hint on the empty line
3921 cx.update_editor(|editor, _window, cx| {
3922 let snapshot = editor.buffer().read(cx).snapshot(cx);
3923 let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
3924 let inlay_text = ": i32";
3925 let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
3926 editor.splice_inlays(&[], vec![inlay], cx);
3927 });
3928
3929 // Enter visual mode and move down twice
3930 cx.simulate_keystrokes("v j j");
3931 cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
3932 }
3933
3934 #[gpui::test]
3935 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3936 let mut cx = NeovimBackedTestContext::new(cx).await;
3937 // Normal mode
3938 cx.set_shared_state(indoc! {"
3939 The ˇquick brown
3940 fox jumps over
3941 the lazy dog
3942 The quick brown
3943 fox jumps over
3944 the lazy dog
3945 The quick brown
3946 fox jumps over
3947 the lazy dog"})
3948 .await;
3949 cx.simulate_shared_keystrokes("2 0 %").await;
3950 cx.shared_state().await.assert_eq(indoc! {"
3951 The quick brown
3952 fox ˇjumps over
3953 the lazy dog
3954 The quick brown
3955 fox jumps over
3956 the lazy dog
3957 The quick brown
3958 fox jumps over
3959 the lazy dog"});
3960
3961 cx.simulate_shared_keystrokes("2 5 %").await;
3962 cx.shared_state().await.assert_eq(indoc! {"
3963 The quick brown
3964 fox jumps over
3965 the ˇlazy dog
3966 The quick brown
3967 fox jumps over
3968 the lazy dog
3969 The quick brown
3970 fox jumps over
3971 the lazy dog"});
3972
3973 cx.simulate_shared_keystrokes("7 5 %").await;
3974 cx.shared_state().await.assert_eq(indoc! {"
3975 The quick brown
3976 fox jumps over
3977 the lazy dog
3978 The quick brown
3979 fox jumps over
3980 the lazy dog
3981 The ˇquick brown
3982 fox jumps over
3983 the lazy dog"});
3984
3985 // Visual mode
3986 cx.set_shared_state(indoc! {"
3987 The ˇquick brown
3988 fox jumps over
3989 the lazy dog
3990 The quick brown
3991 fox jumps over
3992 the lazy dog
3993 The quick brown
3994 fox jumps over
3995 the lazy dog"})
3996 .await;
3997 cx.simulate_shared_keystrokes("v 5 0 %").await;
3998 cx.shared_state().await.assert_eq(indoc! {"
3999 The «quick brown
4000 fox jumps over
4001 the lazy dog
4002 The quick brown
4003 fox jˇ»umps over
4004 the lazy dog
4005 The quick brown
4006 fox jumps over
4007 the lazy dog"});
4008
4009 cx.set_shared_state(indoc! {"
4010 The ˇquick brown
4011 fox jumps over
4012 the lazy dog
4013 The quick brown
4014 fox jumps over
4015 the lazy dog
4016 The quick brown
4017 fox jumps over
4018 the lazy dog"})
4019 .await;
4020 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4021 cx.shared_state().await.assert_eq(indoc! {"
4022 The «quick brown
4023 fox jumps over
4024 the lazy dog
4025 The quick brown
4026 fox jumps over
4027 the lazy dog
4028 The quick brown
4029 fox jumps over
4030 the lˇ»azy dog"});
4031 }
4032
4033 #[gpui::test]
4034 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4035 let mut cx = NeovimBackedTestContext::new(cx).await;
4036
4037 cx.set_shared_state("ˇπππππ").await;
4038 cx.simulate_shared_keystrokes("3 space").await;
4039 cx.shared_state().await.assert_eq("πππˇππ");
4040 }
4041
4042 #[gpui::test]
4043 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4044 let mut cx = NeovimBackedTestContext::new(cx).await;
4045
4046 cx.set_shared_state(indoc! {"
4047 ππππˇπ
4048 πanotherline"})
4049 .await;
4050 cx.simulate_shared_keystrokes("4 space").await;
4051 cx.shared_state().await.assert_eq(indoc! {"
4052 πππππ
4053 πanˇotherline"});
4054 }
4055
4056 #[gpui::test]
4057 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4058 let mut cx = NeovimBackedTestContext::new(cx).await;
4059
4060 cx.set_shared_state(indoc! {"
4061 ππππ
4062 πanˇotherline"})
4063 .await;
4064 cx.simulate_shared_keystrokes("4 backspace").await;
4065 cx.shared_state().await.assert_eq(indoc! {"
4066 πππˇπ
4067 πanotherline"});
4068 }
4069
4070 #[gpui::test]
4071 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4072 let mut cx = VimTestContext::new(cx, true).await;
4073 cx.set_state(
4074 indoc! {
4075 "func empty(a string) bool {
4076 ˇif a == \"\" {
4077 return true
4078 }
4079 return false
4080 }"
4081 },
4082 Mode::Normal,
4083 );
4084 cx.simulate_keystrokes("[ -");
4085 cx.assert_state(
4086 indoc! {
4087 "ˇfunc empty(a string) bool {
4088 if a == \"\" {
4089 return true
4090 }
4091 return false
4092 }"
4093 },
4094 Mode::Normal,
4095 );
4096 cx.simulate_keystrokes("] =");
4097 cx.assert_state(
4098 indoc! {
4099 "func empty(a string) bool {
4100 if a == \"\" {
4101 return true
4102 }
4103 return false
4104 ˇ}"
4105 },
4106 Mode::Normal,
4107 );
4108 cx.simulate_keystrokes("[ +");
4109 cx.assert_state(
4110 indoc! {
4111 "func empty(a string) bool {
4112 if a == \"\" {
4113 return true
4114 }
4115 ˇreturn false
4116 }"
4117 },
4118 Mode::Normal,
4119 );
4120 cx.simulate_keystrokes("2 [ =");
4121 cx.assert_state(
4122 indoc! {
4123 "func empty(a string) bool {
4124 ˇif a == \"\" {
4125 return true
4126 }
4127 return false
4128 }"
4129 },
4130 Mode::Normal,
4131 );
4132 cx.simulate_keystrokes("] +");
4133 cx.assert_state(
4134 indoc! {
4135 "func empty(a string) bool {
4136 if a == \"\" {
4137 ˇreturn true
4138 }
4139 return false
4140 }"
4141 },
4142 Mode::Normal,
4143 );
4144 cx.simulate_keystrokes("] -");
4145 cx.assert_state(
4146 indoc! {
4147 "func empty(a string) bool {
4148 if a == \"\" {
4149 return true
4150 ˇ}
4151 return false
4152 }"
4153 },
4154 Mode::Normal,
4155 );
4156 }
4157
4158 #[gpui::test]
4159 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4160 let mut cx = NeovimBackedTestContext::new(cx).await;
4161 cx.set_shared_state("abˇc").await;
4162 cx.simulate_shared_keystrokes("delete").await;
4163 cx.shared_state().await.assert_eq("aˇb");
4164 }
4165
4166 #[gpui::test]
4167 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4168 let mut cx = NeovimBackedTestContext::new(cx).await;
4169
4170 cx.set_shared_state(indoc! {"
4171 ˇthe quick brown fox
4172 jumped over the lazy dog"})
4173 .await;
4174 cx.simulate_shared_keystrokes("d v 0").await;
4175 cx.shared_state().await.assert_eq(indoc! {"
4176 ˇhe quick brown fox
4177 jumped over the lazy dog"});
4178 assert!(!cx.cx.forced_motion());
4179
4180 cx.set_shared_state(indoc! {"
4181 the quick bˇrown fox
4182 jumped over the lazy dog"})
4183 .await;
4184 cx.simulate_shared_keystrokes("d v 0").await;
4185 cx.shared_state().await.assert_eq(indoc! {"
4186 ˇown fox
4187 jumped over the lazy dog"});
4188 assert!(!cx.cx.forced_motion());
4189
4190 cx.set_shared_state(indoc! {"
4191 the quick brown foˇx
4192 jumped over the lazy dog"})
4193 .await;
4194 cx.simulate_shared_keystrokes("d v 0").await;
4195 cx.shared_state().await.assert_eq(indoc! {"
4196 ˇ
4197 jumped over the lazy dog"});
4198 assert!(!cx.cx.forced_motion());
4199 }
4200
4201 #[gpui::test]
4202 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4203 let mut cx = NeovimBackedTestContext::new(cx).await;
4204
4205 cx.set_shared_state(indoc! {"
4206 ˇthe quick brown fox
4207 jumped over the lazy dog"})
4208 .await;
4209 cx.simulate_shared_keystrokes("d v g shift-m").await;
4210 cx.shared_state().await.assert_eq(indoc! {"
4211 ˇbrown fox
4212 jumped over the lazy dog"});
4213 assert!(!cx.cx.forced_motion());
4214
4215 cx.set_shared_state(indoc! {"
4216 the quick bˇrown fox
4217 jumped over the lazy dog"})
4218 .await;
4219 cx.simulate_shared_keystrokes("d v g shift-m").await;
4220 cx.shared_state().await.assert_eq(indoc! {"
4221 the quickˇown fox
4222 jumped over the lazy dog"});
4223 assert!(!cx.cx.forced_motion());
4224
4225 cx.set_shared_state(indoc! {"
4226 the quick brown foˇx
4227 jumped over the lazy dog"})
4228 .await;
4229 cx.simulate_shared_keystrokes("d v g shift-m").await;
4230 cx.shared_state().await.assert_eq(indoc! {"
4231 the quicˇk
4232 jumped over the lazy dog"});
4233 assert!(!cx.cx.forced_motion());
4234
4235 cx.set_shared_state(indoc! {"
4236 ˇthe quick brown fox
4237 jumped over the lazy dog"})
4238 .await;
4239 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4240 cx.shared_state().await.assert_eq(indoc! {"
4241 ˇ fox
4242 jumped over the lazy dog"});
4243 assert!(!cx.cx.forced_motion());
4244
4245 cx.set_shared_state(indoc! {"
4246 ˇthe quick brown fox
4247 jumped over the lazy dog"})
4248 .await;
4249 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4250 cx.shared_state().await.assert_eq(indoc! {"
4251 ˇuick brown fox
4252 jumped over the lazy dog"});
4253 assert!(!cx.cx.forced_motion());
4254 }
4255
4256 #[gpui::test]
4257 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4258 let mut cx = NeovimBackedTestContext::new(cx).await;
4259
4260 cx.set_shared_state(indoc! {"
4261 the quick brown foˇx
4262 jumped over the lazy dog"})
4263 .await;
4264 cx.simulate_shared_keystrokes("d v $").await;
4265 cx.shared_state().await.assert_eq(indoc! {"
4266 the quick brown foˇx
4267 jumped over the lazy dog"});
4268 assert!(!cx.cx.forced_motion());
4269
4270 cx.set_shared_state(indoc! {"
4271 ˇthe quick brown fox
4272 jumped over the lazy dog"})
4273 .await;
4274 cx.simulate_shared_keystrokes("d v $").await;
4275 cx.shared_state().await.assert_eq(indoc! {"
4276 ˇx
4277 jumped over the lazy dog"});
4278 assert!(!cx.cx.forced_motion());
4279 }
4280
4281 #[gpui::test]
4282 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4283 let mut cx = NeovimBackedTestContext::new(cx).await;
4284
4285 cx.set_shared_state(indoc! {"
4286 ˇthe quick brown fox
4287 jumped over the lazy dog"})
4288 .await;
4289 cx.simulate_shared_keystrokes("y v j p").await;
4290 cx.shared_state().await.assert_eq(indoc! {"
4291 the quick brown fox
4292 ˇthe quick brown fox
4293 jumped over the lazy dog"});
4294 assert!(!cx.cx.forced_motion());
4295
4296 cx.set_shared_state(indoc! {"
4297 the quick bˇrown fox
4298 jumped over the lazy dog"})
4299 .await;
4300 cx.simulate_shared_keystrokes("y v j p").await;
4301 cx.shared_state().await.assert_eq(indoc! {"
4302 the quick brˇrown fox
4303 jumped overown fox
4304 jumped over the lazy dog"});
4305 assert!(!cx.cx.forced_motion());
4306
4307 cx.set_shared_state(indoc! {"
4308 the quick brown foˇx
4309 jumped over the lazy dog"})
4310 .await;
4311 cx.simulate_shared_keystrokes("y v j p").await;
4312 cx.shared_state().await.assert_eq(indoc! {"
4313 the quick brown foxˇx
4314 jumped over the la
4315 jumped over the lazy dog"});
4316 assert!(!cx.cx.forced_motion());
4317
4318 cx.set_shared_state(indoc! {"
4319 the quick brown fox
4320 jˇumped over the lazy dog"})
4321 .await;
4322 cx.simulate_shared_keystrokes("y v k p").await;
4323 cx.shared_state().await.assert_eq(indoc! {"
4324 thˇhe quick brown fox
4325 je quick brown fox
4326 jumped over the lazy dog"});
4327 assert!(!cx.cx.forced_motion());
4328 }
4329
4330 #[gpui::test]
4331 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4332 let mut cx = NeovimBackedTestContext::new(cx).await;
4333
4334 cx.set_shared_state(indoc! {"
4335 ˇthe quick brown fox
4336 jumped over the lazy dog"})
4337 .await;
4338 cx.simulate_shared_keystrokes("d v e").await;
4339 cx.shared_state().await.assert_eq(indoc! {"
4340 ˇe quick brown fox
4341 jumped over the lazy dog"});
4342 assert!(!cx.cx.forced_motion());
4343
4344 cx.set_shared_state(indoc! {"
4345 the quick bˇrown fox
4346 jumped over the lazy dog"})
4347 .await;
4348 cx.simulate_shared_keystrokes("d v e").await;
4349 cx.shared_state().await.assert_eq(indoc! {"
4350 the quick bˇn fox
4351 jumped over the lazy dog"});
4352 assert!(!cx.cx.forced_motion());
4353
4354 cx.set_shared_state(indoc! {"
4355 the quick brown foˇx
4356 jumped over the lazy dog"})
4357 .await;
4358 cx.simulate_shared_keystrokes("d v e").await;
4359 cx.shared_state().await.assert_eq(indoc! {"
4360 the quick brown foˇd over the lazy dog"});
4361 assert!(!cx.cx.forced_motion());
4362 }
4363}