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