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