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