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 use text::Rope;
3091
3092 #[gpui::test]
3093 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3094 let mut cx = NeovimBackedTestContext::new(cx).await;
3095
3096 let initial_state = indoc! {r"ˇabc
3097 def
3098
3099 paragraph
3100 the second
3101
3102
3103
3104 third and
3105 final"};
3106
3107 // goes down once
3108 cx.set_shared_state(initial_state).await;
3109 cx.simulate_shared_keystrokes("}").await;
3110 cx.shared_state().await.assert_eq(indoc! {r"abc
3111 def
3112 ˇ
3113 paragraph
3114 the second
3115
3116
3117
3118 third and
3119 final"});
3120
3121 // goes up once
3122 cx.simulate_shared_keystrokes("{").await;
3123 cx.shared_state().await.assert_eq(initial_state);
3124
3125 // goes down twice
3126 cx.simulate_shared_keystrokes("2 }").await;
3127 cx.shared_state().await.assert_eq(indoc! {r"abc
3128 def
3129
3130 paragraph
3131 the second
3132 ˇ
3133
3134
3135 third and
3136 final"});
3137
3138 // goes down over multiple blanks
3139 cx.simulate_shared_keystrokes("}").await;
3140 cx.shared_state().await.assert_eq(indoc! {r"abc
3141 def
3142
3143 paragraph
3144 the second
3145
3146
3147
3148 third and
3149 finaˇl"});
3150
3151 // goes up twice
3152 cx.simulate_shared_keystrokes("2 {").await;
3153 cx.shared_state().await.assert_eq(indoc! {r"abc
3154 def
3155 ˇ
3156 paragraph
3157 the second
3158
3159
3160
3161 third and
3162 final"});
3163 }
3164
3165 #[gpui::test]
3166 async fn test_matching(cx: &mut gpui::TestAppContext) {
3167 let mut cx = NeovimBackedTestContext::new(cx).await;
3168
3169 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3170 do(something(with<Types>.and_arrays[0, 2]))
3171 }"})
3172 .await;
3173 cx.simulate_shared_keystrokes("%").await;
3174 cx.shared_state()
3175 .await
3176 .assert_eq(indoc! {r"func (a stringˇ) {
3177 do(something(with<Types>.and_arrays[0, 2]))
3178 }"});
3179
3180 // test it works on the last character of the line
3181 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3182 do(something(with<Types>.and_arrays[0, 2]))
3183 }"})
3184 .await;
3185 cx.simulate_shared_keystrokes("%").await;
3186 cx.shared_state()
3187 .await
3188 .assert_eq(indoc! {r"func (a string) {
3189 do(something(with<Types>.and_arrays[0, 2]))
3190 ˇ}"});
3191
3192 // test it works on immediate nesting
3193 cx.set_shared_state("ˇ{()}").await;
3194 cx.simulate_shared_keystrokes("%").await;
3195 cx.shared_state().await.assert_eq("{()ˇ}");
3196 cx.simulate_shared_keystrokes("%").await;
3197 cx.shared_state().await.assert_eq("ˇ{()}");
3198
3199 // test it works on immediate nesting inside braces
3200 cx.set_shared_state("{\n ˇ{()}\n}").await;
3201 cx.simulate_shared_keystrokes("%").await;
3202 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3203
3204 // test it jumps to the next paren on a line
3205 cx.set_shared_state("func ˇboop() {\n}").await;
3206 cx.simulate_shared_keystrokes("%").await;
3207 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3208 }
3209
3210 #[gpui::test]
3211 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3212 let mut cx = NeovimBackedTestContext::new(cx).await;
3213
3214 // test it works with curly braces
3215 cx.set_shared_state(indoc! {r"func (a string) {
3216 do(something(with<Types>.anˇd_arrays[0, 2]))
3217 }"})
3218 .await;
3219 cx.simulate_shared_keystrokes("] }").await;
3220 cx.shared_state()
3221 .await
3222 .assert_eq(indoc! {r"func (a string) {
3223 do(something(with<Types>.and_arrays[0, 2]))
3224 ˇ}"});
3225
3226 // test it works with brackets
3227 cx.set_shared_state(indoc! {r"func (a string) {
3228 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3229 }"})
3230 .await;
3231 cx.simulate_shared_keystrokes("] )").await;
3232 cx.shared_state()
3233 .await
3234 .assert_eq(indoc! {r"func (a string) {
3235 do(something(with<Types>.and_arrays[0, 2])ˇ)
3236 }"});
3237
3238 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3239 .await;
3240 cx.simulate_shared_keystrokes("] )").await;
3241 cx.shared_state()
3242 .await
3243 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3244
3245 // test it works on immediate nesting
3246 cx.set_shared_state("{ˇ {}{}}").await;
3247 cx.simulate_shared_keystrokes("] }").await;
3248 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3249 cx.set_shared_state("(ˇ ()())").await;
3250 cx.simulate_shared_keystrokes("] )").await;
3251 cx.shared_state().await.assert_eq("( ()()ˇ)");
3252
3253 // test it works on immediate nesting inside braces
3254 cx.set_shared_state("{\n ˇ {()}\n}").await;
3255 cx.simulate_shared_keystrokes("] }").await;
3256 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3257 cx.set_shared_state("(\n ˇ {()}\n)").await;
3258 cx.simulate_shared_keystrokes("] )").await;
3259 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3260 }
3261
3262 #[gpui::test]
3263 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3264 let mut cx = NeovimBackedTestContext::new(cx).await;
3265
3266 // test it works with curly braces
3267 cx.set_shared_state(indoc! {r"func (a string) {
3268 do(something(with<Types>.anˇd_arrays[0, 2]))
3269 }"})
3270 .await;
3271 cx.simulate_shared_keystrokes("[ {").await;
3272 cx.shared_state()
3273 .await
3274 .assert_eq(indoc! {r"func (a string) ˇ{
3275 do(something(with<Types>.and_arrays[0, 2]))
3276 }"});
3277
3278 // test it works with brackets
3279 cx.set_shared_state(indoc! {r"func (a string) {
3280 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3281 }"})
3282 .await;
3283 cx.simulate_shared_keystrokes("[ (").await;
3284 cx.shared_state()
3285 .await
3286 .assert_eq(indoc! {r"func (a string) {
3287 doˇ(something(with<Types>.and_arrays[0, 2]))
3288 }"});
3289
3290 // test it works on immediate nesting
3291 cx.set_shared_state("{{}{} ˇ }").await;
3292 cx.simulate_shared_keystrokes("[ {").await;
3293 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3294 cx.set_shared_state("(()() ˇ )").await;
3295 cx.simulate_shared_keystrokes("[ (").await;
3296 cx.shared_state().await.assert_eq("ˇ(()() )");
3297
3298 // test it works on immediate nesting inside braces
3299 cx.set_shared_state("{\n {()} ˇ\n}").await;
3300 cx.simulate_shared_keystrokes("[ {").await;
3301 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3302 cx.set_shared_state("(\n {()} ˇ\n)").await;
3303 cx.simulate_shared_keystrokes("[ (").await;
3304 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3305 }
3306
3307 #[gpui::test]
3308 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3309 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3310
3311 cx.neovim.exec("set filetype=html").await;
3312
3313 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3314 cx.simulate_shared_keystrokes("%").await;
3315 cx.shared_state()
3316 .await
3317 .assert_eq(indoc! {r"<body><ˇ/body>"});
3318 cx.simulate_shared_keystrokes("%").await;
3319
3320 // test jumping backwards
3321 cx.shared_state()
3322 .await
3323 .assert_eq(indoc! {r"<ˇbody></body>"});
3324
3325 // test self-closing tags
3326 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3327 cx.simulate_shared_keystrokes("%").await;
3328 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3329
3330 // test tag with attributes
3331 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3332 </div>
3333 "})
3334 .await;
3335 cx.simulate_shared_keystrokes("%").await;
3336 cx.shared_state()
3337 .await
3338 .assert_eq(indoc! {r"<div class='test' id='main'>
3339 <ˇ/div>
3340 "});
3341
3342 // test multi-line self-closing tag
3343 cx.set_shared_state(indoc! {r#"<a>
3344 <br
3345 test = "test"
3346 /ˇ>
3347 </a>"#})
3348 .await;
3349 cx.simulate_shared_keystrokes("%").await;
3350 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3351 ˇ<br
3352 test = "test"
3353 />
3354 </a>"#});
3355 }
3356
3357 #[gpui::test]
3358 async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3359 let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3360
3361 // test brackets within tags
3362 cx.set_shared_state(indoc! {r"function f() {
3363 return (
3364 <div rules={ˇ[{ a: 1 }]}>
3365 <h1>test</h1>
3366 </div>
3367 );
3368 }"})
3369 .await;
3370 cx.simulate_shared_keystrokes("%").await;
3371 cx.shared_state().await.assert_eq(indoc! {r"function f() {
3372 return (
3373 <div rules={[{ a: 1 }ˇ]}>
3374 <h1>test</h1>
3375 </div>
3376 );
3377 }"});
3378 }
3379
3380 #[gpui::test]
3381 async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
3382 let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
3383
3384 cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
3385 .await;
3386 cx.simulate_shared_keystrokes("%").await;
3387 cx.shared_state()
3388 .await
3389 .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
3390 cx.simulate_shared_keystrokes("%").await;
3391 cx.shared_state()
3392 .await
3393 .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
3394 }
3395
3396 #[gpui::test]
3397 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3398 let mut cx = NeovimBackedTestContext::new(cx).await;
3399
3400 // f and F
3401 cx.set_shared_state("ˇone two three four").await;
3402 cx.simulate_shared_keystrokes("f o").await;
3403 cx.shared_state().await.assert_eq("one twˇo three four");
3404 cx.simulate_shared_keystrokes(",").await;
3405 cx.shared_state().await.assert_eq("ˇone two three four");
3406 cx.simulate_shared_keystrokes("2 ;").await;
3407 cx.shared_state().await.assert_eq("one two three fˇour");
3408 cx.simulate_shared_keystrokes("shift-f e").await;
3409 cx.shared_state().await.assert_eq("one two threˇe four");
3410 cx.simulate_shared_keystrokes("2 ;").await;
3411 cx.shared_state().await.assert_eq("onˇe two three four");
3412 cx.simulate_shared_keystrokes(",").await;
3413 cx.shared_state().await.assert_eq("one two thrˇee four");
3414
3415 // t and T
3416 cx.set_shared_state("ˇone two three four").await;
3417 cx.simulate_shared_keystrokes("t o").await;
3418 cx.shared_state().await.assert_eq("one tˇwo three four");
3419 cx.simulate_shared_keystrokes(",").await;
3420 cx.shared_state().await.assert_eq("oˇne two three four");
3421 cx.simulate_shared_keystrokes("2 ;").await;
3422 cx.shared_state().await.assert_eq("one two three ˇfour");
3423 cx.simulate_shared_keystrokes("shift-t e").await;
3424 cx.shared_state().await.assert_eq("one two threeˇ four");
3425 cx.simulate_shared_keystrokes("3 ;").await;
3426 cx.shared_state().await.assert_eq("oneˇ two three four");
3427 cx.simulate_shared_keystrokes(",").await;
3428 cx.shared_state().await.assert_eq("one two thˇree four");
3429 }
3430
3431 #[gpui::test]
3432 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3433 let mut cx = NeovimBackedTestContext::new(cx).await;
3434 let initial_state = indoc! {r"something(ˇfoo)"};
3435 cx.set_shared_state(initial_state).await;
3436 cx.simulate_shared_keystrokes("}").await;
3437 cx.shared_state().await.assert_eq("something(fooˇ)");
3438 }
3439
3440 #[gpui::test]
3441 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3442 let mut cx = NeovimBackedTestContext::new(cx).await;
3443 cx.set_shared_state("ˇone\n two\nthree").await;
3444 cx.simulate_shared_keystrokes("enter").await;
3445 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3446 }
3447
3448 #[gpui::test]
3449 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3450 let mut cx = NeovimBackedTestContext::new(cx).await;
3451 cx.set_shared_state("ˇ one\n two \nthree").await;
3452 cx.simulate_shared_keystrokes("g _").await;
3453 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3454
3455 cx.set_shared_state("ˇ one \n two \nthree").await;
3456 cx.simulate_shared_keystrokes("g _").await;
3457 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3458 cx.simulate_shared_keystrokes("2 g _").await;
3459 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3460 }
3461
3462 #[gpui::test]
3463 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3464 let mut cx = NeovimBackedTestContext::new(cx).await;
3465 let initial_state = indoc! {r"abc
3466 def
3467 paragraph
3468 the second
3469 third ˇand
3470 final"};
3471
3472 cx.set_shared_state(initial_state).await;
3473 cx.simulate_shared_keystrokes("shift-h").await;
3474 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3475 def
3476 paragraph
3477 the second
3478 third and
3479 final"});
3480
3481 // clip point
3482 cx.set_shared_state(indoc! {r"
3483 1 2 3
3484 4 5 6
3485 7 8 ˇ9
3486 "})
3487 .await;
3488 cx.simulate_shared_keystrokes("shift-h").await;
3489 cx.shared_state().await.assert_eq(indoc! {"
3490 1 2 ˇ3
3491 4 5 6
3492 7 8 9
3493 "});
3494
3495 cx.set_shared_state(indoc! {r"
3496 1 2 3
3497 4 5 6
3498 ˇ7 8 9
3499 "})
3500 .await;
3501 cx.simulate_shared_keystrokes("shift-h").await;
3502 cx.shared_state().await.assert_eq(indoc! {"
3503 ˇ1 2 3
3504 4 5 6
3505 7 8 9
3506 "});
3507
3508 cx.set_shared_state(indoc! {r"
3509 1 2 3
3510 4 5 ˇ6
3511 7 8 9"})
3512 .await;
3513 cx.simulate_shared_keystrokes("9 shift-h").await;
3514 cx.shared_state().await.assert_eq(indoc! {"
3515 1 2 3
3516 4 5 6
3517 7 8 ˇ9"});
3518 }
3519
3520 #[gpui::test]
3521 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3522 let mut cx = NeovimBackedTestContext::new(cx).await;
3523 let initial_state = indoc! {r"abˇc
3524 def
3525 paragraph
3526 the second
3527 third and
3528 final"};
3529
3530 cx.set_shared_state(initial_state).await;
3531 cx.simulate_shared_keystrokes("shift-m").await;
3532 cx.shared_state().await.assert_eq(indoc! {r"abc
3533 def
3534 paˇragraph
3535 the second
3536 third and
3537 final"});
3538
3539 cx.set_shared_state(indoc! {r"
3540 1 2 3
3541 4 5 6
3542 7 8 ˇ9
3543 "})
3544 .await;
3545 cx.simulate_shared_keystrokes("shift-m").await;
3546 cx.shared_state().await.assert_eq(indoc! {"
3547 1 2 3
3548 4 5 ˇ6
3549 7 8 9
3550 "});
3551 cx.set_shared_state(indoc! {r"
3552 1 2 3
3553 4 5 6
3554 ˇ7 8 9
3555 "})
3556 .await;
3557 cx.simulate_shared_keystrokes("shift-m").await;
3558 cx.shared_state().await.assert_eq(indoc! {"
3559 1 2 3
3560 ˇ4 5 6
3561 7 8 9
3562 "});
3563 cx.set_shared_state(indoc! {r"
3564 ˇ1 2 3
3565 4 5 6
3566 7 8 9
3567 "})
3568 .await;
3569 cx.simulate_shared_keystrokes("shift-m").await;
3570 cx.shared_state().await.assert_eq(indoc! {"
3571 1 2 3
3572 ˇ4 5 6
3573 7 8 9
3574 "});
3575 cx.set_shared_state(indoc! {r"
3576 1 2 3
3577 ˇ4 5 6
3578 7 8 9
3579 "})
3580 .await;
3581 cx.simulate_shared_keystrokes("shift-m").await;
3582 cx.shared_state().await.assert_eq(indoc! {"
3583 1 2 3
3584 ˇ4 5 6
3585 7 8 9
3586 "});
3587 cx.set_shared_state(indoc! {r"
3588 1 2 3
3589 4 5 ˇ6
3590 7 8 9
3591 "})
3592 .await;
3593 cx.simulate_shared_keystrokes("shift-m").await;
3594 cx.shared_state().await.assert_eq(indoc! {"
3595 1 2 3
3596 4 5 ˇ6
3597 7 8 9
3598 "});
3599 }
3600
3601 #[gpui::test]
3602 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3603 let mut cx = NeovimBackedTestContext::new(cx).await;
3604 let initial_state = indoc! {r"abc
3605 deˇf
3606 paragraph
3607 the second
3608 third and
3609 final"};
3610
3611 cx.set_shared_state(initial_state).await;
3612 cx.simulate_shared_keystrokes("shift-l").await;
3613 cx.shared_state().await.assert_eq(indoc! {r"abc
3614 def
3615 paragraph
3616 the second
3617 third and
3618 fiˇnal"});
3619
3620 cx.set_shared_state(indoc! {r"
3621 1 2 3
3622 4 5 ˇ6
3623 7 8 9
3624 "})
3625 .await;
3626 cx.simulate_shared_keystrokes("shift-l").await;
3627 cx.shared_state().await.assert_eq(indoc! {"
3628 1 2 3
3629 4 5 6
3630 7 8 9
3631 ˇ"});
3632
3633 cx.set_shared_state(indoc! {r"
3634 1 2 3
3635 ˇ4 5 6
3636 7 8 9
3637 "})
3638 .await;
3639 cx.simulate_shared_keystrokes("shift-l").await;
3640 cx.shared_state().await.assert_eq(indoc! {"
3641 1 2 3
3642 4 5 6
3643 7 8 9
3644 ˇ"});
3645
3646 cx.set_shared_state(indoc! {r"
3647 1 2 ˇ3
3648 4 5 6
3649 7 8 9
3650 "})
3651 .await;
3652 cx.simulate_shared_keystrokes("shift-l").await;
3653 cx.shared_state().await.assert_eq(indoc! {"
3654 1 2 3
3655 4 5 6
3656 7 8 9
3657 ˇ"});
3658
3659 cx.set_shared_state(indoc! {r"
3660 ˇ1 2 3
3661 4 5 6
3662 7 8 9
3663 "})
3664 .await;
3665 cx.simulate_shared_keystrokes("shift-l").await;
3666 cx.shared_state().await.assert_eq(indoc! {"
3667 1 2 3
3668 4 5 6
3669 7 8 9
3670 ˇ"});
3671
3672 cx.set_shared_state(indoc! {r"
3673 1 2 3
3674 4 5 ˇ6
3675 7 8 9
3676 "})
3677 .await;
3678 cx.simulate_shared_keystrokes("9 shift-l").await;
3679 cx.shared_state().await.assert_eq(indoc! {"
3680 1 2 ˇ3
3681 4 5 6
3682 7 8 9
3683 "});
3684 }
3685
3686 #[gpui::test]
3687 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3688 let mut cx = NeovimBackedTestContext::new(cx).await;
3689 cx.set_shared_state(indoc! {r"
3690 456 5ˇ67 678
3691 "})
3692 .await;
3693 cx.simulate_shared_keystrokes("g e").await;
3694 cx.shared_state().await.assert_eq(indoc! {"
3695 45ˇ6 567 678
3696 "});
3697
3698 // Test times
3699 cx.set_shared_state(indoc! {r"
3700 123 234 345
3701 456 5ˇ67 678
3702 "})
3703 .await;
3704 cx.simulate_shared_keystrokes("4 g e").await;
3705 cx.shared_state().await.assert_eq(indoc! {"
3706 12ˇ3 234 345
3707 456 567 678
3708 "});
3709
3710 // With punctuation
3711 cx.set_shared_state(indoc! {r"
3712 123 234 345
3713 4;5.6 5ˇ67 678
3714 789 890 901
3715 "})
3716 .await;
3717 cx.simulate_shared_keystrokes("g e").await;
3718 cx.shared_state().await.assert_eq(indoc! {"
3719 123 234 345
3720 4;5.ˇ6 567 678
3721 789 890 901
3722 "});
3723
3724 // With punctuation and count
3725 cx.set_shared_state(indoc! {r"
3726 123 234 345
3727 4;5.6 5ˇ67 678
3728 789 890 901
3729 "})
3730 .await;
3731 cx.simulate_shared_keystrokes("5 g e").await;
3732 cx.shared_state().await.assert_eq(indoc! {"
3733 123 234 345
3734 ˇ4;5.6 567 678
3735 789 890 901
3736 "});
3737
3738 // newlines
3739 cx.set_shared_state(indoc! {r"
3740 123 234 345
3741
3742 78ˇ9 890 901
3743 "})
3744 .await;
3745 cx.simulate_shared_keystrokes("g e").await;
3746 cx.shared_state().await.assert_eq(indoc! {"
3747 123 234 345
3748 ˇ
3749 789 890 901
3750 "});
3751 cx.simulate_shared_keystrokes("g e").await;
3752 cx.shared_state().await.assert_eq(indoc! {"
3753 123 234 34ˇ5
3754
3755 789 890 901
3756 "});
3757
3758 // With punctuation
3759 cx.set_shared_state(indoc! {r"
3760 123 234 345
3761 4;5.ˇ6 567 678
3762 789 890 901
3763 "})
3764 .await;
3765 cx.simulate_shared_keystrokes("g shift-e").await;
3766 cx.shared_state().await.assert_eq(indoc! {"
3767 123 234 34ˇ5
3768 4;5.6 567 678
3769 789 890 901
3770 "});
3771
3772 // With multi byte char
3773 cx.set_shared_state(indoc! {r"
3774 bar ˇó
3775 "})
3776 .await;
3777 cx.simulate_shared_keystrokes("g e").await;
3778 cx.shared_state().await.assert_eq(indoc! {"
3779 baˇr ó
3780 "});
3781 }
3782
3783 #[gpui::test]
3784 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3785 let mut cx = NeovimBackedTestContext::new(cx).await;
3786
3787 cx.set_shared_state(indoc! {"
3788 fn aˇ() {
3789 return
3790 }
3791 "})
3792 .await;
3793 cx.simulate_shared_keystrokes("v $ %").await;
3794 cx.shared_state().await.assert_eq(indoc! {"
3795 fn a«() {
3796 return
3797 }ˇ»
3798 "});
3799 }
3800
3801 #[gpui::test]
3802 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3803 let mut cx = VimTestContext::new(cx, true).await;
3804
3805 cx.set_state(
3806 indoc! {"
3807 struct Foo {
3808 ˇ
3809 }
3810 "},
3811 Mode::Normal,
3812 );
3813
3814 cx.update_editor(|editor, _window, cx| {
3815 let range = editor.selections.newest_anchor().range();
3816 let inlay_text = " field: int,\n field2: string\n field3: float";
3817 let inlay = Inlay::edit_prediction(1, range.start, Rope::from_str_small(inlay_text));
3818 editor.splice_inlays(&[], vec![inlay], cx);
3819 });
3820
3821 cx.simulate_keystrokes("j");
3822 cx.assert_state(
3823 indoc! {"
3824 struct Foo {
3825
3826 ˇ}
3827 "},
3828 Mode::Normal,
3829 );
3830 }
3831
3832 #[gpui::test]
3833 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3834 let mut cx = VimTestContext::new(cx, true).await;
3835
3836 cx.set_state(
3837 indoc! {"
3838 ˇstruct Foo {
3839
3840 }
3841 "},
3842 Mode::Normal,
3843 );
3844 cx.update_editor(|editor, _window, cx| {
3845 let snapshot = editor.buffer().read(cx).snapshot(cx);
3846 let end_of_line =
3847 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3848 let inlay_text = " hint";
3849 let inlay = Inlay::edit_prediction(1, end_of_line, Rope::from_str_small(inlay_text));
3850 editor.splice_inlays(&[], vec![inlay], cx);
3851 });
3852 cx.simulate_keystrokes("$");
3853 cx.assert_state(
3854 indoc! {"
3855 struct Foo ˇ{
3856
3857 }
3858 "},
3859 Mode::Normal,
3860 );
3861 }
3862
3863 #[gpui::test]
3864 async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
3865 let mut cx = VimTestContext::new(cx, true).await;
3866
3867 // Test the exact scenario from issue #29134
3868 cx.set_state(
3869 indoc! {"
3870 fn main() {
3871 let this_is_a_long_name = Vec::<u32>::new();
3872 let new_oneˇ = this_is_a_long_name
3873 .iter()
3874 .map(|i| i + 1)
3875 .map(|i| i * 2)
3876 .collect::<Vec<_>>();
3877 }
3878 "},
3879 Mode::Normal,
3880 );
3881
3882 // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
3883 cx.update_editor(|editor, _window, cx| {
3884 let snapshot = editor.buffer().read(cx).snapshot(cx);
3885 // The empty line is at line 3 (0-indexed)
3886 let line_start = snapshot.anchor_after(Point::new(3, 0));
3887 let inlay_text = ": Vec<u32>";
3888 let inlay = Inlay::edit_prediction(1, line_start, Rope::from_str_small(inlay_text));
3889 editor.splice_inlays(&[], vec![inlay], cx);
3890 });
3891
3892 // Enter visual mode
3893 cx.simulate_keystrokes("v");
3894 cx.assert_state(
3895 indoc! {"
3896 fn main() {
3897 let this_is_a_long_name = Vec::<u32>::new();
3898 let new_one« ˇ»= this_is_a_long_name
3899 .iter()
3900 .map(|i| i + 1)
3901 .map(|i| i * 2)
3902 .collect::<Vec<_>>();
3903 }
3904 "},
3905 Mode::Visual,
3906 );
3907
3908 // Move down - should go to the beginning of line 4, not skip to line 5
3909 cx.simulate_keystrokes("j");
3910 cx.assert_state(
3911 indoc! {"
3912 fn main() {
3913 let this_is_a_long_name = Vec::<u32>::new();
3914 let new_one« = this_is_a_long_name
3915 ˇ» .iter()
3916 .map(|i| i + 1)
3917 .map(|i| i * 2)
3918 .collect::<Vec<_>>();
3919 }
3920 "},
3921 Mode::Visual,
3922 );
3923
3924 // Test with multiple movements
3925 cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
3926
3927 // Add type hint on the empty line
3928 cx.update_editor(|editor, _window, cx| {
3929 let snapshot = editor.buffer().read(cx).snapshot(cx);
3930 let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
3931 let inlay_text = ": i32";
3932 let inlay =
3933 Inlay::edit_prediction(2, empty_line_start, Rope::from_str_small(inlay_text));
3934 editor.splice_inlays(&[], vec![inlay], cx);
3935 });
3936
3937 // Enter visual mode and move down twice
3938 cx.simulate_keystrokes("v j j");
3939 cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
3940 }
3941
3942 #[gpui::test]
3943 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3944 let mut cx = NeovimBackedTestContext::new(cx).await;
3945 // Normal mode
3946 cx.set_shared_state(indoc! {"
3947 The ˇquick brown
3948 fox jumps over
3949 the lazy dog
3950 The quick brown
3951 fox jumps over
3952 the lazy dog
3953 The quick brown
3954 fox jumps over
3955 the lazy dog"})
3956 .await;
3957 cx.simulate_shared_keystrokes("2 0 %").await;
3958 cx.shared_state().await.assert_eq(indoc! {"
3959 The quick brown
3960 fox ˇjumps over
3961 the lazy dog
3962 The quick brown
3963 fox jumps over
3964 the lazy dog
3965 The quick brown
3966 fox jumps over
3967 the lazy dog"});
3968
3969 cx.simulate_shared_keystrokes("2 5 %").await;
3970 cx.shared_state().await.assert_eq(indoc! {"
3971 The quick brown
3972 fox jumps over
3973 the ˇlazy dog
3974 The quick brown
3975 fox jumps over
3976 the lazy dog
3977 The quick brown
3978 fox jumps over
3979 the lazy dog"});
3980
3981 cx.simulate_shared_keystrokes("7 5 %").await;
3982 cx.shared_state().await.assert_eq(indoc! {"
3983 The quick brown
3984 fox jumps over
3985 the lazy dog
3986 The quick brown
3987 fox jumps over
3988 the lazy dog
3989 The ˇquick brown
3990 fox jumps over
3991 the lazy dog"});
3992
3993 // Visual mode
3994 cx.set_shared_state(indoc! {"
3995 The ˇquick brown
3996 fox jumps over
3997 the lazy dog
3998 The quick brown
3999 fox jumps over
4000 the lazy dog
4001 The quick brown
4002 fox jumps over
4003 the lazy dog"})
4004 .await;
4005 cx.simulate_shared_keystrokes("v 5 0 %").await;
4006 cx.shared_state().await.assert_eq(indoc! {"
4007 The «quick brown
4008 fox jumps over
4009 the lazy dog
4010 The quick brown
4011 fox jˇ»umps over
4012 the lazy dog
4013 The quick brown
4014 fox jumps over
4015 the lazy dog"});
4016
4017 cx.set_shared_state(indoc! {"
4018 The ˇquick brown
4019 fox jumps over
4020 the lazy dog
4021 The quick brown
4022 fox jumps over
4023 the lazy dog
4024 The quick brown
4025 fox jumps over
4026 the lazy dog"})
4027 .await;
4028 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4029 cx.shared_state().await.assert_eq(indoc! {"
4030 The «quick brown
4031 fox jumps over
4032 the lazy dog
4033 The quick brown
4034 fox jumps over
4035 the lazy dog
4036 The quick brown
4037 fox jumps over
4038 the lˇ»azy dog"});
4039 }
4040
4041 #[gpui::test]
4042 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4043 let mut cx = NeovimBackedTestContext::new(cx).await;
4044
4045 cx.set_shared_state("ˇπππππ").await;
4046 cx.simulate_shared_keystrokes("3 space").await;
4047 cx.shared_state().await.assert_eq("πππˇππ");
4048 }
4049
4050 #[gpui::test]
4051 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4052 let mut cx = NeovimBackedTestContext::new(cx).await;
4053
4054 cx.set_shared_state(indoc! {"
4055 ππππˇπ
4056 πanotherline"})
4057 .await;
4058 cx.simulate_shared_keystrokes("4 space").await;
4059 cx.shared_state().await.assert_eq(indoc! {"
4060 πππππ
4061 πanˇotherline"});
4062 }
4063
4064 #[gpui::test]
4065 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4066 let mut cx = NeovimBackedTestContext::new(cx).await;
4067
4068 cx.set_shared_state(indoc! {"
4069 ππππ
4070 πanˇotherline"})
4071 .await;
4072 cx.simulate_shared_keystrokes("4 backspace").await;
4073 cx.shared_state().await.assert_eq(indoc! {"
4074 πππˇπ
4075 πanotherline"});
4076 }
4077
4078 #[gpui::test]
4079 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4080 let mut cx = VimTestContext::new(cx, true).await;
4081 cx.set_state(
4082 indoc! {
4083 "func empty(a string) bool {
4084 ˇif a == \"\" {
4085 return true
4086 }
4087 return false
4088 }"
4089 },
4090 Mode::Normal,
4091 );
4092 cx.simulate_keystrokes("[ -");
4093 cx.assert_state(
4094 indoc! {
4095 "ˇfunc empty(a string) bool {
4096 if a == \"\" {
4097 return true
4098 }
4099 return false
4100 }"
4101 },
4102 Mode::Normal,
4103 );
4104 cx.simulate_keystrokes("] =");
4105 cx.assert_state(
4106 indoc! {
4107 "func empty(a string) bool {
4108 if a == \"\" {
4109 return true
4110 }
4111 return false
4112 ˇ}"
4113 },
4114 Mode::Normal,
4115 );
4116 cx.simulate_keystrokes("[ +");
4117 cx.assert_state(
4118 indoc! {
4119 "func empty(a string) bool {
4120 if a == \"\" {
4121 return true
4122 }
4123 ˇreturn false
4124 }"
4125 },
4126 Mode::Normal,
4127 );
4128 cx.simulate_keystrokes("2 [ =");
4129 cx.assert_state(
4130 indoc! {
4131 "func empty(a string) bool {
4132 ˇif a == \"\" {
4133 return true
4134 }
4135 return false
4136 }"
4137 },
4138 Mode::Normal,
4139 );
4140 cx.simulate_keystrokes("] +");
4141 cx.assert_state(
4142 indoc! {
4143 "func empty(a string) bool {
4144 if a == \"\" {
4145 ˇreturn true
4146 }
4147 return false
4148 }"
4149 },
4150 Mode::Normal,
4151 );
4152 cx.simulate_keystrokes("] -");
4153 cx.assert_state(
4154 indoc! {
4155 "func empty(a string) bool {
4156 if a == \"\" {
4157 return true
4158 ˇ}
4159 return false
4160 }"
4161 },
4162 Mode::Normal,
4163 );
4164 }
4165
4166 #[gpui::test]
4167 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4168 let mut cx = NeovimBackedTestContext::new(cx).await;
4169 cx.set_shared_state("abˇc").await;
4170 cx.simulate_shared_keystrokes("delete").await;
4171 cx.shared_state().await.assert_eq("aˇb");
4172 }
4173
4174 #[gpui::test]
4175 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4176 let mut cx = NeovimBackedTestContext::new(cx).await;
4177
4178 cx.set_shared_state(indoc! {"
4179 ˇthe quick brown fox
4180 jumped over the lazy dog"})
4181 .await;
4182 cx.simulate_shared_keystrokes("d v 0").await;
4183 cx.shared_state().await.assert_eq(indoc! {"
4184 ˇhe quick brown fox
4185 jumped over the lazy dog"});
4186 assert!(!cx.cx.forced_motion());
4187
4188 cx.set_shared_state(indoc! {"
4189 the quick bˇrown fox
4190 jumped over the lazy dog"})
4191 .await;
4192 cx.simulate_shared_keystrokes("d v 0").await;
4193 cx.shared_state().await.assert_eq(indoc! {"
4194 ˇown fox
4195 jumped over the lazy dog"});
4196 assert!(!cx.cx.forced_motion());
4197
4198 cx.set_shared_state(indoc! {"
4199 the quick brown foˇx
4200 jumped over the lazy dog"})
4201 .await;
4202 cx.simulate_shared_keystrokes("d v 0").await;
4203 cx.shared_state().await.assert_eq(indoc! {"
4204 ˇ
4205 jumped over the lazy dog"});
4206 assert!(!cx.cx.forced_motion());
4207 }
4208
4209 #[gpui::test]
4210 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4211 let mut cx = NeovimBackedTestContext::new(cx).await;
4212
4213 cx.set_shared_state(indoc! {"
4214 ˇthe quick brown fox
4215 jumped over the lazy dog"})
4216 .await;
4217 cx.simulate_shared_keystrokes("d v g shift-m").await;
4218 cx.shared_state().await.assert_eq(indoc! {"
4219 ˇbrown fox
4220 jumped over the lazy dog"});
4221 assert!(!cx.cx.forced_motion());
4222
4223 cx.set_shared_state(indoc! {"
4224 the quick bˇrown fox
4225 jumped over the lazy dog"})
4226 .await;
4227 cx.simulate_shared_keystrokes("d v g shift-m").await;
4228 cx.shared_state().await.assert_eq(indoc! {"
4229 the quickˇown fox
4230 jumped over the lazy dog"});
4231 assert!(!cx.cx.forced_motion());
4232
4233 cx.set_shared_state(indoc! {"
4234 the quick brown foˇx
4235 jumped over the lazy dog"})
4236 .await;
4237 cx.simulate_shared_keystrokes("d v g shift-m").await;
4238 cx.shared_state().await.assert_eq(indoc! {"
4239 the quicˇk
4240 jumped over the lazy dog"});
4241 assert!(!cx.cx.forced_motion());
4242
4243 cx.set_shared_state(indoc! {"
4244 ˇthe quick brown fox
4245 jumped over the lazy dog"})
4246 .await;
4247 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4248 cx.shared_state().await.assert_eq(indoc! {"
4249 ˇ fox
4250 jumped over the lazy dog"});
4251 assert!(!cx.cx.forced_motion());
4252
4253 cx.set_shared_state(indoc! {"
4254 ˇthe quick brown fox
4255 jumped over the lazy dog"})
4256 .await;
4257 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4258 cx.shared_state().await.assert_eq(indoc! {"
4259 ˇuick brown fox
4260 jumped over the lazy dog"});
4261 assert!(!cx.cx.forced_motion());
4262 }
4263
4264 #[gpui::test]
4265 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4266 let mut cx = NeovimBackedTestContext::new(cx).await;
4267
4268 cx.set_shared_state(indoc! {"
4269 the quick brown foˇx
4270 jumped over the lazy dog"})
4271 .await;
4272 cx.simulate_shared_keystrokes("d v $").await;
4273 cx.shared_state().await.assert_eq(indoc! {"
4274 the quick brown foˇx
4275 jumped over the lazy dog"});
4276 assert!(!cx.cx.forced_motion());
4277
4278 cx.set_shared_state(indoc! {"
4279 ˇthe quick brown fox
4280 jumped over the lazy dog"})
4281 .await;
4282 cx.simulate_shared_keystrokes("d v $").await;
4283 cx.shared_state().await.assert_eq(indoc! {"
4284 ˇx
4285 jumped over the lazy dog"});
4286 assert!(!cx.cx.forced_motion());
4287 }
4288
4289 #[gpui::test]
4290 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4291 let mut cx = NeovimBackedTestContext::new(cx).await;
4292
4293 cx.set_shared_state(indoc! {"
4294 ˇthe quick brown fox
4295 jumped over the lazy dog"})
4296 .await;
4297 cx.simulate_shared_keystrokes("y v j p").await;
4298 cx.shared_state().await.assert_eq(indoc! {"
4299 the quick brown fox
4300 ˇthe quick brown fox
4301 jumped over the lazy dog"});
4302 assert!(!cx.cx.forced_motion());
4303
4304 cx.set_shared_state(indoc! {"
4305 the quick bˇrown fox
4306 jumped over the lazy dog"})
4307 .await;
4308 cx.simulate_shared_keystrokes("y v j p").await;
4309 cx.shared_state().await.assert_eq(indoc! {"
4310 the quick brˇrown fox
4311 jumped overown fox
4312 jumped over the lazy dog"});
4313 assert!(!cx.cx.forced_motion());
4314
4315 cx.set_shared_state(indoc! {"
4316 the quick brown foˇx
4317 jumped over the lazy dog"})
4318 .await;
4319 cx.simulate_shared_keystrokes("y v j p").await;
4320 cx.shared_state().await.assert_eq(indoc! {"
4321 the quick brown foxˇx
4322 jumped over the la
4323 jumped over the lazy dog"});
4324 assert!(!cx.cx.forced_motion());
4325
4326 cx.set_shared_state(indoc! {"
4327 the quick brown fox
4328 jˇumped over the lazy dog"})
4329 .await;
4330 cx.simulate_shared_keystrokes("y v k p").await;
4331 cx.shared_state().await.assert_eq(indoc! {"
4332 thˇhe quick brown fox
4333 je quick brown fox
4334 jumped over the lazy dog"});
4335 assert!(!cx.cx.forced_motion());
4336 }
4337
4338 #[gpui::test]
4339 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4340 let mut cx = NeovimBackedTestContext::new(cx).await;
4341
4342 cx.set_shared_state(indoc! {"
4343 ˇthe quick brown fox
4344 jumped over the lazy dog"})
4345 .await;
4346 cx.simulate_shared_keystrokes("d v e").await;
4347 cx.shared_state().await.assert_eq(indoc! {"
4348 ˇe quick brown fox
4349 jumped over the lazy dog"});
4350 assert!(!cx.cx.forced_motion());
4351
4352 cx.set_shared_state(indoc! {"
4353 the quick bˇrown fox
4354 jumped over the lazy dog"})
4355 .await;
4356 cx.simulate_shared_keystrokes("d v e").await;
4357 cx.shared_state().await.assert_eq(indoc! {"
4358 the quick bˇn fox
4359 jumped over the lazy dog"});
4360 assert!(!cx.cx.forced_motion());
4361
4362 cx.set_shared_state(indoc! {"
4363 the quick brown foˇx
4364 jumped over the lazy dog"})
4365 .await;
4366 cx.simulate_shared_keystrokes("d v e").await;
4367 cx.shared_state().await.assert_eq(indoc! {"
4368 the quick brown foˇd over the lazy dog"});
4369 assert!(!cx.cx.forced_motion());
4370 }
4371}