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