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