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
1718fn next_end_impl(
1719 map: &DisplaySnapshot,
1720 mut point: DisplayPoint,
1721 times: usize,
1722 allow_cross_newline: bool,
1723 always_advance: bool,
1724 mut is_boundary: impl FnMut(char, char) -> bool,
1725) -> DisplayPoint {
1726 for _ in 0..times {
1727 let mut need_next_char = false;
1728 let new_point = if always_advance {
1729 next_char(map, point, allow_cross_newline)
1730 } else {
1731 point
1732 };
1733 let new_point = movement::find_boundary_exclusive(
1734 map,
1735 new_point,
1736 FindRange::MultiLine,
1737 |left, right| {
1738 let at_newline = right == '\n';
1739
1740 if !allow_cross_newline && at_newline {
1741 need_next_char = true;
1742 return true;
1743 }
1744
1745 is_boundary(left, right)
1746 },
1747 );
1748 let new_point = if need_next_char {
1749 next_char(map, new_point, true)
1750 } else {
1751 new_point
1752 };
1753 let new_point = map.clip_point(new_point, Bias::Left);
1754 if point == new_point {
1755 break;
1756 }
1757 point = new_point;
1758 }
1759 point
1760}
1761
1762pub(crate) fn next_word_end(
1763 map: &DisplaySnapshot,
1764 point: DisplayPoint,
1765 ignore_punctuation: bool,
1766 times: usize,
1767 allow_cross_newline: bool,
1768 always_advance: bool,
1769) -> DisplayPoint {
1770 let classifier = map
1771 .buffer_snapshot()
1772 .char_classifier_at(point.to_point(map))
1773 .ignore_punctuation(ignore_punctuation);
1774
1775 next_end_impl(
1776 map,
1777 point,
1778 times,
1779 allow_cross_newline,
1780 always_advance,
1781 |left, right| {
1782 let left_kind = classifier.kind(left);
1783 let right_kind = classifier.kind(right);
1784 left_kind != right_kind && left_kind != CharKind::Whitespace
1785 },
1786 )
1787}
1788
1789pub(crate) fn next_subword_end(
1790 map: &DisplaySnapshot,
1791 point: DisplayPoint,
1792 ignore_punctuation: bool,
1793 times: usize,
1794 allow_cross_newline: bool,
1795) -> DisplayPoint {
1796 let classifier = map
1797 .buffer_snapshot()
1798 .char_classifier_at(point.to_point(map))
1799 .ignore_punctuation(ignore_punctuation);
1800
1801 next_end_impl(
1802 map,
1803 point,
1804 times,
1805 allow_cross_newline,
1806 true,
1807 |left, right| {
1808 let left_kind = classifier.kind(left);
1809 let right_kind = classifier.kind(right);
1810 let is_stopping_punct = |c: char| ".\"'{}[]()<>".contains(c);
1811 let found_subword_end = is_subword_end(left, right, "_-");
1812 let is_word_end = (left_kind != right_kind)
1813 && (!left.is_ascii_punctuation() || is_stopping_punct(left));
1814
1815 !left.is_whitespace() && (is_word_end || found_subword_end)
1816 },
1817 )
1818}
1819
1820fn previous_word_start(
1821 map: &DisplaySnapshot,
1822 mut point: DisplayPoint,
1823 ignore_punctuation: bool,
1824 times: usize,
1825) -> DisplayPoint {
1826 let classifier = map
1827 .buffer_snapshot()
1828 .char_classifier_at(point.to_point(map))
1829 .ignore_punctuation(ignore_punctuation);
1830 for _ in 0..times {
1831 // This works even though find_preceding_boundary is called for every character in the line containing
1832 // cursor because the newline is checked only once.
1833 let new_point = movement::find_preceding_boundary_display_point(
1834 map,
1835 point,
1836 FindRange::MultiLine,
1837 |left, right| {
1838 let left_kind = classifier.kind(left);
1839 let right_kind = classifier.kind(right);
1840
1841 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1842 },
1843 );
1844 if point == new_point {
1845 break;
1846 }
1847 point = new_point;
1848 }
1849 point
1850}
1851
1852fn previous_word_end(
1853 map: &DisplaySnapshot,
1854 point: DisplayPoint,
1855 ignore_punctuation: bool,
1856 times: usize,
1857) -> DisplayPoint {
1858 let classifier = map
1859 .buffer_snapshot()
1860 .char_classifier_at(point.to_point(map))
1861 .ignore_punctuation(ignore_punctuation);
1862 let mut point = point.to_point(map);
1863
1864 if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row))
1865 && let Some(ch) = map.buffer_snapshot().chars_at(point).next()
1866 {
1867 point.column += ch.len_utf8() as u32;
1868 }
1869 for _ in 0..times {
1870 let new_point = movement::find_preceding_boundary_point(
1871 &map.buffer_snapshot(),
1872 point,
1873 FindRange::MultiLine,
1874 |left, right| {
1875 let left_kind = classifier.kind(left);
1876 let right_kind = classifier.kind(right);
1877 match (left_kind, right_kind) {
1878 (CharKind::Punctuation, CharKind::Whitespace)
1879 | (CharKind::Punctuation, CharKind::Word)
1880 | (CharKind::Word, CharKind::Whitespace)
1881 | (CharKind::Word, CharKind::Punctuation) => true,
1882 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1883 _ => false,
1884 }
1885 },
1886 );
1887 if new_point == point {
1888 break;
1889 }
1890 point = new_point;
1891 }
1892 movement::saturating_left(map, point.to_display_point(map))
1893}
1894
1895/// Checks if there's a subword boundary start between `left` and `right` characters.
1896/// This detects transitions like `_b` (separator to non-separator) or `aB` (lowercase to uppercase).
1897pub(crate) fn is_subword_start(left: char, right: char, separators: &str) -> bool {
1898 let is_separator = |c: char| separators.contains(c);
1899 (is_separator(left) && !is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
1900}
1901
1902/// Checks if there's a subword boundary end between `left` and `right` characters.
1903/// This detects transitions like `a_` (non-separator to separator) or `aB` (lowercase to uppercase).
1904pub(crate) fn is_subword_end(left: char, right: char, separators: &str) -> bool {
1905 let is_separator = |c: char| separators.contains(c);
1906 (!is_separator(left) && is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
1907}
1908
1909fn next_subword_start(
1910 map: &DisplaySnapshot,
1911 mut point: DisplayPoint,
1912 ignore_punctuation: bool,
1913 times: usize,
1914) -> DisplayPoint {
1915 let classifier = map
1916 .buffer_snapshot()
1917 .char_classifier_at(point.to_point(map))
1918 .ignore_punctuation(ignore_punctuation);
1919 for _ in 0..times {
1920 let mut crossed_newline = false;
1921 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1922 let left_kind = classifier.kind(left);
1923 let right_kind = classifier.kind(right);
1924 let at_newline = right == '\n';
1925 let is_stopping_punct = |c: char| "\"'{}[]()<>".contains(c);
1926 let found_subword_start = is_subword_start(left, right, "._-");
1927 let is_word_start = (left_kind != right_kind)
1928 && (!right.is_ascii_punctuation() || is_stopping_punct(right));
1929 let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
1930 || at_newline && crossed_newline
1931 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1932
1933 crossed_newline |= at_newline;
1934 found
1935 });
1936 if point == new_point {
1937 break;
1938 }
1939 point = new_point;
1940 }
1941 point
1942}
1943
1944fn previous_subword_start(
1945 map: &DisplaySnapshot,
1946 mut point: DisplayPoint,
1947 ignore_punctuation: bool,
1948 times: usize,
1949) -> DisplayPoint {
1950 let classifier = map
1951 .buffer_snapshot()
1952 .char_classifier_at(point.to_point(map))
1953 .ignore_punctuation(ignore_punctuation);
1954 for _ in 0..times {
1955 let mut crossed_newline = false;
1956 // This works even though find_preceding_boundary is called for every character in the line containing
1957 // cursor because the newline is checked only once.
1958 let new_point = movement::find_preceding_boundary_display_point(
1959 map,
1960 point,
1961 FindRange::MultiLine,
1962 |left, right| {
1963 let left_kind = classifier.kind(left);
1964 let right_kind = classifier.kind(right);
1965 let at_newline = right == '\n';
1966
1967 let is_stopping_punct = |c: char| ".\"'{}[]()<>".contains(c);
1968 let is_word_start = (left_kind != right_kind)
1969 && (is_stopping_punct(right) || !right.is_ascii_punctuation());
1970 let found_subword_start = is_subword_start(left, right, "._-");
1971
1972 let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
1973 || at_newline && crossed_newline
1974 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1975
1976 crossed_newline |= at_newline;
1977
1978 found
1979 },
1980 );
1981 if point == new_point {
1982 break;
1983 }
1984 point = new_point;
1985 }
1986 point
1987}
1988
1989fn previous_subword_end(
1990 map: &DisplaySnapshot,
1991 point: DisplayPoint,
1992 ignore_punctuation: bool,
1993 times: usize,
1994) -> DisplayPoint {
1995 let classifier = map
1996 .buffer_snapshot()
1997 .char_classifier_at(point.to_point(map))
1998 .ignore_punctuation(ignore_punctuation);
1999 let mut point = point.to_point(map);
2000
2001 if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row))
2002 && let Some(ch) = map.buffer_snapshot().chars_at(point).next()
2003 {
2004 point.column += ch.len_utf8() as u32;
2005 }
2006 for _ in 0..times {
2007 let new_point = movement::find_preceding_boundary_point(
2008 &map.buffer_snapshot(),
2009 point,
2010 FindRange::MultiLine,
2011 |left, right| {
2012 let left_kind = classifier.kind(left);
2013 let right_kind = classifier.kind(right);
2014
2015 let is_stopping_punct = |c: char| ".;\"'{}[]()<>".contains(c);
2016 let found_subword_end = is_subword_end(left, right, "_-");
2017
2018 if found_subword_end {
2019 return true;
2020 }
2021
2022 match (left_kind, right_kind) {
2023 (CharKind::Word, CharKind::Whitespace)
2024 | (CharKind::Word, CharKind::Punctuation) => true,
2025 (CharKind::Punctuation, _) if is_stopping_punct(left) => true,
2026 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2027 _ => false,
2028 }
2029 },
2030 );
2031 if new_point == point {
2032 break;
2033 }
2034 point = new_point;
2035 }
2036 movement::saturating_left(map, point.to_display_point(map))
2037}
2038
2039pub(crate) fn first_non_whitespace(
2040 map: &DisplaySnapshot,
2041 display_lines: bool,
2042 from: DisplayPoint,
2043) -> DisplayPoint {
2044 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2045 let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map));
2046 for (ch, offset) in map.buffer_chars_at(start_offset) {
2047 if ch == '\n' {
2048 return from;
2049 }
2050
2051 start_offset = offset;
2052
2053 if classifier.kind(ch) != CharKind::Whitespace {
2054 break;
2055 }
2056 }
2057
2058 start_offset.to_display_point(map)
2059}
2060
2061pub(crate) fn last_non_whitespace(
2062 map: &DisplaySnapshot,
2063 from: DisplayPoint,
2064 count: usize,
2065) -> DisplayPoint {
2066 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2067 let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map));
2068
2069 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2070 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next()
2071 && classifier.kind(ch) != CharKind::Whitespace
2072 {
2073 return end_of_line.to_display_point(map);
2074 }
2075
2076 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2077 if ch == '\n' {
2078 break;
2079 }
2080 end_of_line = offset;
2081 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2082 break;
2083 }
2084 }
2085
2086 end_of_line.to_display_point(map)
2087}
2088
2089pub(crate) fn start_of_line(
2090 map: &DisplaySnapshot,
2091 display_lines: bool,
2092 point: DisplayPoint,
2093) -> DisplayPoint {
2094 if display_lines {
2095 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2096 } else {
2097 map.prev_line_boundary(point.to_point(map)).1
2098 }
2099}
2100
2101pub(crate) fn middle_of_line(
2102 map: &DisplaySnapshot,
2103 display_lines: bool,
2104 point: DisplayPoint,
2105 times: Option<usize>,
2106) -> DisplayPoint {
2107 let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2108 times as f64 / 100.
2109 } else {
2110 0.5
2111 };
2112 if display_lines {
2113 map.clip_point(
2114 DisplayPoint::new(
2115 point.row(),
2116 (map.line_len(point.row()) as f64 * percent) as u32,
2117 ),
2118 Bias::Left,
2119 )
2120 } else {
2121 let mut buffer_point = point.to_point(map);
2122 buffer_point.column = (map
2123 .buffer_snapshot()
2124 .line_len(MultiBufferRow(buffer_point.row)) as f64
2125 * percent) as u32;
2126
2127 map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2128 }
2129}
2130
2131pub(crate) fn end_of_line(
2132 map: &DisplaySnapshot,
2133 display_lines: bool,
2134 mut point: DisplayPoint,
2135 times: usize,
2136) -> DisplayPoint {
2137 if times > 1 {
2138 point = map.start_of_relative_buffer_row(point, times as isize - 1);
2139 }
2140 if display_lines {
2141 map.clip_point(
2142 DisplayPoint::new(point.row(), map.line_len(point.row())),
2143 Bias::Left,
2144 )
2145 } else {
2146 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2147 }
2148}
2149
2150pub(crate) fn sentence_backwards(
2151 map: &DisplaySnapshot,
2152 point: DisplayPoint,
2153 mut times: usize,
2154) -> DisplayPoint {
2155 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot());
2156 let mut chars = map.reverse_buffer_chars_at(start).peekable();
2157
2158 let mut was_newline = map
2159 .buffer_chars_at(start)
2160 .next()
2161 .is_some_and(|(c, _)| c == '\n');
2162
2163 while let Some((ch, offset)) = chars.next() {
2164 let start_of_next_sentence = if was_newline && ch == '\n' {
2165 Some(offset + ch.len_utf8())
2166 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2167 Some(next_non_blank(map, offset + ch.len_utf8()))
2168 } else if ch == '.' || ch == '?' || ch == '!' {
2169 start_of_next_sentence(map, offset + ch.len_utf8())
2170 } else {
2171 None
2172 };
2173
2174 if let Some(start_of_next_sentence) = start_of_next_sentence {
2175 if start_of_next_sentence < start {
2176 times = times.saturating_sub(1);
2177 }
2178 if times == 0 || offset.0 == 0 {
2179 return map.clip_point(
2180 start_of_next_sentence
2181 .to_offset(&map.buffer_snapshot())
2182 .to_display_point(map),
2183 Bias::Left,
2184 );
2185 }
2186 }
2187 if was_newline {
2188 start = offset;
2189 }
2190 was_newline = ch == '\n';
2191 }
2192
2193 DisplayPoint::zero()
2194}
2195
2196pub(crate) fn sentence_forwards(
2197 map: &DisplaySnapshot,
2198 point: DisplayPoint,
2199 mut times: usize,
2200) -> DisplayPoint {
2201 let start = point.to_point(map).to_offset(&map.buffer_snapshot());
2202 let mut chars = map.buffer_chars_at(start).peekable();
2203
2204 let mut was_newline = map
2205 .reverse_buffer_chars_at(start)
2206 .next()
2207 .is_some_and(|(c, _)| c == '\n')
2208 && chars.peek().is_some_and(|(c, _)| *c == '\n');
2209
2210 while let Some((ch, offset)) = chars.next() {
2211 if was_newline && ch == '\n' {
2212 continue;
2213 }
2214 let start_of_next_sentence = if was_newline {
2215 Some(next_non_blank(map, offset))
2216 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2217 Some(next_non_blank(map, offset + ch.len_utf8()))
2218 } else if ch == '.' || ch == '?' || ch == '!' {
2219 start_of_next_sentence(map, offset + ch.len_utf8())
2220 } else {
2221 None
2222 };
2223
2224 if let Some(start_of_next_sentence) = start_of_next_sentence {
2225 times = times.saturating_sub(1);
2226 if times == 0 {
2227 return map.clip_point(
2228 start_of_next_sentence
2229 .to_offset(&map.buffer_snapshot())
2230 .to_display_point(map),
2231 Bias::Right,
2232 );
2233 }
2234 }
2235
2236 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2237 }
2238
2239 map.max_point()
2240}
2241
2242fn next_non_blank(map: &DisplaySnapshot, start: MultiBufferOffset) -> MultiBufferOffset {
2243 for (c, o) in map.buffer_chars_at(start) {
2244 if c == '\n' || !c.is_whitespace() {
2245 return o;
2246 }
2247 }
2248
2249 map.buffer_snapshot().len()
2250}
2251
2252// given the offset after a ., !, or ? find the start of the next sentence.
2253// if this is not a sentence boundary, returns None.
2254fn start_of_next_sentence(
2255 map: &DisplaySnapshot,
2256 end_of_sentence: MultiBufferOffset,
2257) -> Option<MultiBufferOffset> {
2258 let chars = map.buffer_chars_at(end_of_sentence);
2259 let mut seen_space = false;
2260
2261 for (char, offset) in chars {
2262 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2263 continue;
2264 }
2265
2266 if char == '\n' && seen_space {
2267 return Some(offset);
2268 } else if char.is_whitespace() {
2269 seen_space = true;
2270 } else if seen_space {
2271 return Some(offset);
2272 } else {
2273 return None;
2274 }
2275 }
2276
2277 Some(map.buffer_snapshot().len())
2278}
2279
2280fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2281 let point = map.display_point_to_point(display_point, Bias::Left);
2282 let Some(mut excerpt) = map.buffer_snapshot().excerpt_containing(point..point) else {
2283 return display_point;
2284 };
2285 let offset = excerpt.buffer().point_to_offset(
2286 excerpt
2287 .buffer()
2288 .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2289 );
2290 let buffer_range = excerpt.buffer_range();
2291 if offset >= buffer_range.start.0 && offset <= buffer_range.end.0 {
2292 let point = map
2293 .buffer_snapshot()
2294 .offset_to_point(excerpt.map_offset_from_buffer(BufferOffset(offset)));
2295 return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2296 }
2297 for (excerpt, buffer, range) in map.buffer_snapshot().excerpts() {
2298 let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
2299 ..language::ToOffset::to_offset(&range.context.end, buffer);
2300 if offset >= excerpt_range.start && offset <= excerpt_range.end {
2301 let text_anchor = buffer.anchor_after(offset);
2302 let anchor = Anchor::in_buffer(excerpt, text_anchor);
2303 return anchor.to_display_point(map);
2304 } else if offset <= excerpt_range.start {
2305 let anchor = Anchor::in_buffer(excerpt, range.context.start);
2306 return anchor.to_display_point(map);
2307 }
2308 }
2309
2310 map.clip_point(
2311 map.point_to_display_point(
2312 map.buffer_snapshot().clip_point(point, Bias::Left),
2313 Bias::Left,
2314 ),
2315 Bias::Left,
2316 )
2317}
2318
2319fn start_of_document(
2320 map: &DisplaySnapshot,
2321 display_point: DisplayPoint,
2322 maybe_times: Option<usize>,
2323) -> DisplayPoint {
2324 if let Some(times) = maybe_times {
2325 return go_to_line(map, display_point, times);
2326 }
2327
2328 let point = map.display_point_to_point(display_point, Bias::Left);
2329 let mut first_point = Point::zero();
2330 first_point.column = point.column;
2331
2332 map.clip_point(
2333 map.point_to_display_point(
2334 map.buffer_snapshot().clip_point(first_point, Bias::Left),
2335 Bias::Left,
2336 ),
2337 Bias::Left,
2338 )
2339}
2340
2341fn end_of_document(
2342 map: &DisplaySnapshot,
2343 display_point: DisplayPoint,
2344 maybe_times: Option<usize>,
2345) -> DisplayPoint {
2346 if let Some(times) = maybe_times {
2347 return go_to_line(map, display_point, times);
2348 };
2349 let point = map.display_point_to_point(display_point, Bias::Left);
2350 let mut last_point = map.buffer_snapshot().max_point();
2351 last_point.column = point.column;
2352
2353 map.clip_point(
2354 map.point_to_display_point(
2355 map.buffer_snapshot().clip_point(last_point, Bias::Left),
2356 Bias::Left,
2357 ),
2358 Bias::Left,
2359 )
2360}
2361
2362fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2363 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2364 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2365
2366 if head > outer.start && head < inner.start {
2367 let mut offset = inner.end.to_offset(map, Bias::Left);
2368 for c in map.buffer_snapshot().chars_at(offset) {
2369 if c == '/' || c == '\n' || c == '>' {
2370 return Some(offset.to_display_point(map));
2371 }
2372 offset += c.len_utf8();
2373 }
2374 } else {
2375 let mut offset = outer.start.to_offset(map, Bias::Left);
2376 for c in map.buffer_snapshot().chars_at(offset) {
2377 offset += c.len_utf8();
2378 if c == '<' || c == '\n' {
2379 return Some(offset.to_display_point(map));
2380 }
2381 }
2382 }
2383
2384 None
2385}
2386
2387const BRACKET_PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
2388
2389fn get_bracket_pair(ch: char) -> Option<(char, char, bool)> {
2390 for (open, close) in BRACKET_PAIRS {
2391 if ch == open {
2392 return Some((open, close, true));
2393 }
2394 if ch == close {
2395 return Some((open, close, false));
2396 }
2397 }
2398 None
2399}
2400
2401fn find_matching_bracket_text_based(
2402 map: &DisplaySnapshot,
2403 offset: MultiBufferOffset,
2404 line_range: Range<MultiBufferOffset>,
2405) -> Option<MultiBufferOffset> {
2406 let bracket_info = map
2407 .buffer_chars_at(offset)
2408 .take_while(|(_, char_offset)| *char_offset < line_range.end)
2409 .find_map(|(ch, char_offset)| get_bracket_pair(ch).map(|info| (info, char_offset)));
2410
2411 let (open, close, is_opening) = bracket_info?.0;
2412 let bracket_offset = bracket_info?.1;
2413
2414 let mut depth = 0i32;
2415 if is_opening {
2416 for (ch, char_offset) in map.buffer_chars_at(bracket_offset) {
2417 if ch == open {
2418 depth += 1;
2419 } else if ch == close {
2420 depth -= 1;
2421 if depth == 0 {
2422 return Some(char_offset);
2423 }
2424 }
2425 }
2426 } else {
2427 for (ch, char_offset) in map.reverse_buffer_chars_at(bracket_offset + close.len_utf8()) {
2428 if ch == close {
2429 depth += 1;
2430 } else if ch == open {
2431 depth -= 1;
2432 if depth == 0 {
2433 return Some(char_offset);
2434 }
2435 }
2436 }
2437 }
2438
2439 None
2440}
2441
2442fn matching(
2443 map: &DisplaySnapshot,
2444 display_point: DisplayPoint,
2445 match_quotes: bool,
2446) -> DisplayPoint {
2447 if !map.is_singleton() {
2448 return display_point;
2449 }
2450 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2451 let display_point = map.clip_at_line_end(display_point);
2452 let point = display_point.to_point(map);
2453 let offset = point.to_offset(&map.buffer_snapshot());
2454 let snapshot = map.buffer_snapshot();
2455
2456 // Ensure the range is contained by the current line.
2457 let mut line_end = map.next_line_boundary(point).0;
2458 if line_end == point {
2459 line_end = map.max_point().to_point(map);
2460 }
2461
2462 let is_quote_char = |ch: char| matches!(ch, '\'' | '"' | '`');
2463
2464 let make_range_filter = |require_on_bracket: bool| {
2465 move |buffer: &language::BufferSnapshot,
2466 opening_range: Range<BufferOffset>,
2467 closing_range: Range<BufferOffset>| {
2468 if !match_quotes
2469 && buffer
2470 .chars_at(opening_range.start)
2471 .next()
2472 .is_some_and(is_quote_char)
2473 {
2474 return false;
2475 }
2476
2477 if require_on_bracket {
2478 // Attempt to find the smallest enclosing bracket range that also contains
2479 // the offset, which only happens if the cursor is currently in a bracket.
2480 opening_range.contains(&BufferOffset(offset.0))
2481 || closing_range.contains(&BufferOffset(offset.0))
2482 } else {
2483 true
2484 }
2485 }
2486 };
2487
2488 let bracket_ranges = snapshot
2489 .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(true)))
2490 .or_else(|| {
2491 snapshot
2492 .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(false)))
2493 });
2494
2495 if let Some((opening_range, closing_range)) = bracket_ranges {
2496 let mut chars = map.buffer_snapshot().chars_at(offset);
2497 match chars.next() {
2498 Some('/') => {}
2499 _ => {
2500 if opening_range.contains(&offset) {
2501 return closing_range.start.to_display_point(map);
2502 } else if closing_range.contains(&offset) {
2503 return opening_range.start.to_display_point(map);
2504 }
2505 }
2506 }
2507 }
2508
2509 let line_range = map.prev_line_boundary(point).0..line_end;
2510 let visible_line_range =
2511 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2512 let line_range = line_range.start.to_offset(&map.buffer_snapshot())
2513 ..line_range.end.to_offset(&map.buffer_snapshot());
2514 let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range);
2515 if let Some(ranges) = ranges {
2516 let mut closest_pair_destination = None;
2517 let mut closest_distance = usize::MAX;
2518
2519 for (open_range, close_range) in ranges {
2520 if !match_quotes
2521 && map
2522 .buffer_snapshot()
2523 .chars_at(open_range.start)
2524 .next()
2525 .is_some_and(is_quote_char)
2526 {
2527 continue;
2528 }
2529
2530 if map.buffer_snapshot().chars_at(open_range.start).next() == Some('<') {
2531 if offset > open_range.start && offset < close_range.start {
2532 let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2533 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2534 return display_point;
2535 }
2536 if let Some(tag) = matching_tag(map, display_point) {
2537 return tag;
2538 }
2539 } else if close_range.contains(&offset) {
2540 return open_range.start.to_display_point(map);
2541 } else if open_range.contains(&offset) {
2542 return (close_range.end - 1).to_display_point(map);
2543 }
2544 }
2545
2546 if (open_range.contains(&offset) || open_range.start >= offset)
2547 && line_range.contains(&open_range.start)
2548 {
2549 let distance = open_range.start.saturating_sub(offset);
2550 if distance < closest_distance {
2551 closest_pair_destination = Some(close_range.start);
2552 closest_distance = distance;
2553 }
2554 }
2555
2556 if (close_range.contains(&offset) || close_range.start >= offset)
2557 && line_range.contains(&close_range.start)
2558 {
2559 let distance = close_range.start.saturating_sub(offset);
2560 if distance < closest_distance {
2561 closest_pair_destination = Some(open_range.start);
2562 closest_distance = distance;
2563 }
2564 }
2565
2566 continue;
2567 }
2568
2569 closest_pair_destination
2570 .map(|destination| destination.to_display_point(map))
2571 .unwrap_or_else(|| {
2572 find_matching_bracket_text_based(map, offset, line_range.clone())
2573 .map(|o| o.to_display_point(map))
2574 .unwrap_or(display_point)
2575 })
2576 } else {
2577 find_matching_bracket_text_based(map, offset, line_range)
2578 .map(|o| o.to_display_point(map))
2579 .unwrap_or(display_point)
2580 }
2581}
2582
2583// Go to {count} percentage in the file, on the first
2584// non-blank in the line linewise. To compute the new
2585// line number this formula is used:
2586// ({count} * number-of-lines + 99) / 100
2587//
2588// https://neovim.io/doc/user/motion.html#N%25
2589fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2590 let total_lines = map.buffer_snapshot().max_point().row + 1;
2591 let target_line = (count * total_lines as usize).div_ceil(100);
2592 let target_point = DisplayPoint::new(
2593 DisplayRow(target_line.saturating_sub(1) as u32),
2594 point.column(),
2595 );
2596 map.clip_point(target_point, Bias::Left)
2597}
2598
2599fn unmatched_forward(
2600 map: &DisplaySnapshot,
2601 mut display_point: DisplayPoint,
2602 char: char,
2603 times: usize,
2604) -> DisplayPoint {
2605 for _ in 0..times {
2606 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2607 let point = display_point.to_point(map);
2608 let offset = point.to_offset(&map.buffer_snapshot());
2609
2610 let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2611 let Some(ranges) = ranges else { break };
2612 let mut closest_closing_destination = None;
2613 let mut closest_distance = usize::MAX;
2614
2615 for (_, close_range) in ranges {
2616 if close_range.start > offset {
2617 let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2618 if Some(char) == chars.next() {
2619 let distance = close_range.start - offset;
2620 if distance < closest_distance {
2621 closest_closing_destination = Some(close_range.start);
2622 closest_distance = distance;
2623 continue;
2624 }
2625 }
2626 }
2627 }
2628
2629 let new_point = closest_closing_destination
2630 .map(|destination| destination.to_display_point(map))
2631 .unwrap_or(display_point);
2632 if new_point == display_point {
2633 break;
2634 }
2635 display_point = new_point;
2636 }
2637 display_point
2638}
2639
2640fn unmatched_backward(
2641 map: &DisplaySnapshot,
2642 mut display_point: DisplayPoint,
2643 char: char,
2644 times: usize,
2645) -> DisplayPoint {
2646 for _ in 0..times {
2647 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2648 let point = display_point.to_point(map);
2649 let offset = point.to_offset(&map.buffer_snapshot());
2650
2651 let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2652 let Some(ranges) = ranges else {
2653 break;
2654 };
2655
2656 let mut closest_starting_destination = None;
2657 let mut closest_distance = usize::MAX;
2658
2659 for (start_range, _) in ranges {
2660 if start_range.start < offset {
2661 let mut chars = map.buffer_snapshot().chars_at(start_range.start);
2662 if Some(char) == chars.next() {
2663 let distance = offset - start_range.start;
2664 if distance < closest_distance {
2665 closest_starting_destination = Some(start_range.start);
2666 closest_distance = distance;
2667 continue;
2668 }
2669 }
2670 }
2671 }
2672
2673 let new_point = closest_starting_destination
2674 .map(|destination| destination.to_display_point(map))
2675 .unwrap_or(display_point);
2676 if new_point == display_point {
2677 break;
2678 } else {
2679 display_point = new_point;
2680 }
2681 }
2682 display_point
2683}
2684
2685fn find_forward(
2686 map: &DisplaySnapshot,
2687 from: DisplayPoint,
2688 before: bool,
2689 target: char,
2690 times: usize,
2691 mode: FindRange,
2692 smartcase: bool,
2693) -> Option<DisplayPoint> {
2694 let mut to = from;
2695 let mut found = false;
2696
2697 for _ in 0..times {
2698 found = false;
2699 let new_to = find_boundary(map, to, mode, |_, right| {
2700 found = is_character_match(target, right, smartcase);
2701 found
2702 });
2703 if to == new_to {
2704 break;
2705 }
2706 to = new_to;
2707 }
2708
2709 if found {
2710 if before && to.column() > 0 {
2711 *to.column_mut() -= 1;
2712 Some(map.clip_point(to, Bias::Left))
2713 } else if before && to.row().0 > 0 {
2714 *to.row_mut() -= 1;
2715 *to.column_mut() = map.line(to.row()).len() as u32;
2716 Some(map.clip_point(to, Bias::Left))
2717 } else {
2718 Some(to)
2719 }
2720 } else {
2721 None
2722 }
2723}
2724
2725fn find_backward(
2726 map: &DisplaySnapshot,
2727 from: DisplayPoint,
2728 after: bool,
2729 target: char,
2730 times: usize,
2731 mode: FindRange,
2732 smartcase: bool,
2733) -> DisplayPoint {
2734 let mut to = from;
2735
2736 for _ in 0..times {
2737 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2738 is_character_match(target, right, smartcase)
2739 });
2740 if to == new_to {
2741 break;
2742 }
2743 to = new_to;
2744 }
2745
2746 let next = map.buffer_snapshot().chars_at(to.to_point(map)).next();
2747 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2748 if after {
2749 *to.column_mut() += 1;
2750 map.clip_point(to, Bias::Right)
2751 } else {
2752 to
2753 }
2754 } else {
2755 from
2756 }
2757}
2758
2759/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2760pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2761 if smartcase {
2762 if target.is_uppercase() {
2763 target == other
2764 } else {
2765 target == other.to_ascii_lowercase()
2766 }
2767 } else {
2768 target == other
2769 }
2770}
2771
2772fn sneak(
2773 map: &DisplaySnapshot,
2774 from: DisplayPoint,
2775 first_target: char,
2776 second_target: char,
2777 times: usize,
2778 smartcase: bool,
2779) -> Option<DisplayPoint> {
2780 let mut to = from;
2781 let mut found = false;
2782
2783 for _ in 0..times {
2784 found = false;
2785 let new_to = find_boundary(
2786 map,
2787 movement::right(map, to),
2788 FindRange::MultiLine,
2789 |left, right| {
2790 found = is_character_match(first_target, left, smartcase)
2791 && is_character_match(second_target, right, smartcase);
2792 found
2793 },
2794 );
2795 if to == new_to {
2796 break;
2797 }
2798 to = new_to;
2799 }
2800
2801 if found {
2802 Some(movement::left(map, to))
2803 } else {
2804 None
2805 }
2806}
2807
2808fn sneak_backward(
2809 map: &DisplaySnapshot,
2810 from: DisplayPoint,
2811 first_target: char,
2812 second_target: char,
2813 times: usize,
2814 smartcase: bool,
2815) -> Option<DisplayPoint> {
2816 let mut to = from;
2817 let mut found = false;
2818
2819 for _ in 0..times {
2820 found = false;
2821 let new_to =
2822 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2823 found = is_character_match(first_target, left, smartcase)
2824 && is_character_match(second_target, right, smartcase);
2825 found
2826 });
2827 if to == new_to {
2828 break;
2829 }
2830 to = new_to;
2831 }
2832
2833 if found {
2834 Some(movement::left(map, to))
2835 } else {
2836 None
2837 }
2838}
2839
2840fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2841 let correct_line = map.start_of_relative_buffer_row(point, times as isize);
2842 first_non_whitespace(map, false, correct_line)
2843}
2844
2845fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2846 let correct_line = map.start_of_relative_buffer_row(point, -(times as isize));
2847 first_non_whitespace(map, false, correct_line)
2848}
2849
2850fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2851 let correct_line = map.start_of_relative_buffer_row(point, 0);
2852 right(map, correct_line, times.saturating_sub(1))
2853}
2854
2855pub(crate) fn next_line_end(
2856 map: &DisplaySnapshot,
2857 mut point: DisplayPoint,
2858 times: usize,
2859) -> DisplayPoint {
2860 if times > 1 {
2861 point = map.start_of_relative_buffer_row(point, times as isize - 1);
2862 }
2863 end_of_line(map, false, point, 1)
2864}
2865
2866fn window_top(
2867 map: &DisplaySnapshot,
2868 point: DisplayPoint,
2869 text_layout_details: &TextLayoutDetails,
2870 mut times: usize,
2871) -> (DisplayPoint, SelectionGoal) {
2872 let first_visible_line = text_layout_details
2873 .scroll_anchor
2874 .anchor
2875 .to_display_point(map);
2876
2877 if first_visible_line.row() != DisplayRow(0)
2878 && text_layout_details.vertical_scroll_margin as usize > times
2879 {
2880 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2881 }
2882
2883 if let Some(visible_rows) = text_layout_details.visible_rows {
2884 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2885 let new_row = (first_visible_line.row().0 + (times as u32))
2886 .min(bottom_row)
2887 .min(map.max_point().row().0);
2888 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2889
2890 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2891 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2892 } else {
2893 let new_row =
2894 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2895 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2896
2897 let new_point = DisplayPoint::new(new_row, new_col);
2898 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2899 }
2900}
2901
2902fn window_middle(
2903 map: &DisplaySnapshot,
2904 point: DisplayPoint,
2905 text_layout_details: &TextLayoutDetails,
2906) -> (DisplayPoint, SelectionGoal) {
2907 if let Some(visible_rows) = text_layout_details.visible_rows {
2908 let first_visible_line = text_layout_details
2909 .scroll_anchor
2910 .anchor
2911 .to_display_point(map);
2912
2913 let max_visible_rows =
2914 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2915
2916 let new_row =
2917 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2918 let new_row = DisplayRow(new_row);
2919 let new_col = point.column().min(map.line_len(new_row));
2920 let new_point = DisplayPoint::new(new_row, new_col);
2921 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2922 } else {
2923 (point, SelectionGoal::None)
2924 }
2925}
2926
2927fn window_bottom(
2928 map: &DisplaySnapshot,
2929 point: DisplayPoint,
2930 text_layout_details: &TextLayoutDetails,
2931 mut times: usize,
2932) -> (DisplayPoint, SelectionGoal) {
2933 if let Some(visible_rows) = text_layout_details.visible_rows {
2934 let first_visible_line = text_layout_details
2935 .scroll_anchor
2936 .anchor
2937 .to_display_point(map);
2938 let bottom_row = first_visible_line.row().0
2939 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2940 if bottom_row < map.max_point().row().0
2941 && text_layout_details.vertical_scroll_margin as usize > times
2942 {
2943 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2944 }
2945 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2946 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2947 {
2948 first_visible_line.row()
2949 } else {
2950 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2951 };
2952 let new_col = point.column().min(map.line_len(new_row));
2953 let new_point = DisplayPoint::new(new_row, new_col);
2954 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2955 } else {
2956 (point, SelectionGoal::None)
2957 }
2958}
2959
2960fn method_motion(
2961 map: &DisplaySnapshot,
2962 mut display_point: DisplayPoint,
2963 times: usize,
2964 direction: Direction,
2965 is_start: bool,
2966) -> DisplayPoint {
2967 let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else {
2968 return display_point;
2969 };
2970
2971 for _ in 0..times {
2972 let point = map.display_point_to_point(display_point, Bias::Left);
2973 let offset = point.to_offset(&map.buffer_snapshot()).0;
2974 let range = if direction == Direction::Prev {
2975 0..offset
2976 } else {
2977 offset..buffer.len()
2978 };
2979
2980 let possibilities = buffer
2981 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2982 .filter_map(|(range, object)| {
2983 if !matches!(object, language::TextObject::AroundFunction) {
2984 return None;
2985 }
2986
2987 let relevant = if is_start { range.start } else { range.end };
2988 if direction == Direction::Prev && relevant < offset {
2989 Some(relevant)
2990 } else if direction == Direction::Next && relevant > offset + 1 {
2991 Some(relevant)
2992 } else {
2993 None
2994 }
2995 });
2996
2997 let dest = if direction == Direction::Prev {
2998 possibilities.max().unwrap_or(offset)
2999 } else {
3000 possibilities.min().unwrap_or(offset)
3001 };
3002 let new_point = map.clip_point(MultiBufferOffset(dest).to_display_point(map), Bias::Left);
3003 if new_point == display_point {
3004 break;
3005 }
3006 display_point = new_point;
3007 }
3008 display_point
3009}
3010
3011fn comment_motion(
3012 map: &DisplaySnapshot,
3013 mut display_point: DisplayPoint,
3014 times: usize,
3015 direction: Direction,
3016) -> DisplayPoint {
3017 let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else {
3018 return display_point;
3019 };
3020
3021 for _ in 0..times {
3022 let point = map.display_point_to_point(display_point, Bias::Left);
3023 let offset = point.to_offset(&map.buffer_snapshot()).0;
3024 let range = if direction == Direction::Prev {
3025 0..offset
3026 } else {
3027 offset..buffer.len()
3028 };
3029
3030 let possibilities = buffer
3031 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
3032 .filter_map(|(range, object)| {
3033 if !matches!(object, language::TextObject::AroundComment) {
3034 return None;
3035 }
3036
3037 let relevant = if direction == Direction::Prev {
3038 range.start
3039 } else {
3040 range.end
3041 };
3042 if direction == Direction::Prev && relevant < offset {
3043 Some(relevant)
3044 } else if direction == Direction::Next && relevant > offset + 1 {
3045 Some(relevant)
3046 } else {
3047 None
3048 }
3049 });
3050
3051 let dest = if direction == Direction::Prev {
3052 possibilities.max().unwrap_or(offset)
3053 } else {
3054 possibilities.min().unwrap_or(offset)
3055 };
3056 let new_point = map.clip_point(MultiBufferOffset(dest).to_display_point(map), Bias::Left);
3057 if new_point == display_point {
3058 break;
3059 }
3060 display_point = new_point;
3061 }
3062
3063 display_point
3064}
3065
3066fn section_motion(
3067 map: &DisplaySnapshot,
3068 mut display_point: DisplayPoint,
3069 times: usize,
3070 direction: Direction,
3071 is_start: bool,
3072) -> DisplayPoint {
3073 if map.buffer_snapshot().as_singleton().is_some() {
3074 for _ in 0..times {
3075 let offset = map
3076 .display_point_to_point(display_point, Bias::Left)
3077 .to_offset(&map.buffer_snapshot());
3078 let range = if direction == Direction::Prev {
3079 MultiBufferOffset(0)..offset
3080 } else {
3081 offset..map.buffer_snapshot().len()
3082 };
3083
3084 // we set a max start depth here because we want a section to only be "top level"
3085 // similar to vim's default of '{' in the first column.
3086 // (and without it, ]] at the start of editor.rs is -very- slow)
3087 let mut possibilities = map
3088 .buffer_snapshot()
3089 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
3090 .filter(|(_, object)| {
3091 matches!(
3092 object,
3093 language::TextObject::AroundClass | language::TextObject::AroundFunction
3094 )
3095 })
3096 .collect::<Vec<_>>();
3097 possibilities.sort_by_key(|(range_a, _)| range_a.start);
3098 let mut prev_end = None;
3099 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
3100 if t == language::TextObject::AroundFunction
3101 && prev_end.is_some_and(|prev_end| prev_end > range.start)
3102 {
3103 return None;
3104 }
3105 prev_end = Some(range.end);
3106
3107 let relevant = if is_start { range.start } else { range.end };
3108 if direction == Direction::Prev && relevant < offset {
3109 Some(relevant)
3110 } else if direction == Direction::Next && relevant > offset + 1usize {
3111 Some(relevant)
3112 } else {
3113 None
3114 }
3115 });
3116
3117 let offset = if direction == Direction::Prev {
3118 possibilities.max().unwrap_or(MultiBufferOffset(0))
3119 } else {
3120 possibilities.min().unwrap_or(map.buffer_snapshot().len())
3121 };
3122
3123 let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3124 if new_point == display_point {
3125 break;
3126 }
3127 display_point = new_point;
3128 }
3129 return display_point;
3130 };
3131
3132 for _ in 0..times {
3133 let next_point = if is_start {
3134 movement::start_of_excerpt(map, display_point, direction)
3135 } else {
3136 movement::end_of_excerpt(map, display_point, direction)
3137 };
3138 if next_point == display_point {
3139 break;
3140 }
3141 display_point = next_point;
3142 }
3143
3144 display_point
3145}
3146
3147fn matches_indent_type(
3148 target_indent: &text::LineIndent,
3149 current_indent: &text::LineIndent,
3150 indent_type: IndentType,
3151) -> bool {
3152 match indent_type {
3153 IndentType::Lesser => {
3154 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3155 }
3156 IndentType::Greater => {
3157 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3158 }
3159 IndentType::Same => {
3160 target_indent.spaces == current_indent.spaces
3161 && target_indent.tabs == current_indent.tabs
3162 }
3163 }
3164}
3165
3166fn indent_motion(
3167 map: &DisplaySnapshot,
3168 mut display_point: DisplayPoint,
3169 times: usize,
3170 direction: Direction,
3171 indent_type: IndentType,
3172) -> DisplayPoint {
3173 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3174 let current_row = MultiBufferRow(buffer_point.row);
3175 let current_indent = map.line_indent_for_buffer_row(current_row);
3176 if current_indent.is_line_empty() {
3177 return display_point;
3178 }
3179 let max_row = map.max_point().to_point(map).row;
3180
3181 for _ in 0..times {
3182 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3183
3184 let target_row = match direction {
3185 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3186 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3187 !indent.is_line_empty()
3188 && matches_indent_type(&indent, ¤t_indent, indent_type)
3189 }),
3190 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3191 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3192 !indent.is_line_empty()
3193 && matches_indent_type(&indent, ¤t_indent, indent_type)
3194 }),
3195 }
3196 .unwrap_or(current_buffer_row);
3197
3198 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3199 let new_point = first_non_whitespace(map, false, new_point);
3200 if new_point == display_point {
3201 break;
3202 }
3203 display_point = new_point;
3204 }
3205 display_point
3206}
3207
3208#[cfg(test)]
3209mod test {
3210
3211 use crate::{
3212 motion::Matching,
3213 state::Mode,
3214 test::{NeovimBackedTestContext, VimTestContext},
3215 };
3216 use editor::Inlay;
3217 use gpui::KeyBinding;
3218 use indoc::indoc;
3219 use language::Point;
3220 use multi_buffer::MultiBufferRow;
3221
3222 #[gpui::test]
3223 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3224 let mut cx = NeovimBackedTestContext::new(cx).await;
3225
3226 let initial_state = indoc! {r"ˇabc
3227 def
3228
3229 paragraph
3230 the second
3231
3232
3233
3234 third and
3235 final"};
3236
3237 // goes down once
3238 cx.set_shared_state(initial_state).await;
3239 cx.simulate_shared_keystrokes("}").await;
3240 cx.shared_state().await.assert_eq(indoc! {r"abc
3241 def
3242 ˇ
3243 paragraph
3244 the second
3245
3246
3247
3248 third and
3249 final"});
3250
3251 // goes up once
3252 cx.simulate_shared_keystrokes("{").await;
3253 cx.shared_state().await.assert_eq(initial_state);
3254
3255 // goes down twice
3256 cx.simulate_shared_keystrokes("2 }").await;
3257 cx.shared_state().await.assert_eq(indoc! {r"abc
3258 def
3259
3260 paragraph
3261 the second
3262 ˇ
3263
3264
3265 third and
3266 final"});
3267
3268 // goes down over multiple blanks
3269 cx.simulate_shared_keystrokes("}").await;
3270 cx.shared_state().await.assert_eq(indoc! {r"abc
3271 def
3272
3273 paragraph
3274 the second
3275
3276
3277
3278 third and
3279 finaˇl"});
3280
3281 // goes up twice
3282 cx.simulate_shared_keystrokes("2 {").await;
3283 cx.shared_state().await.assert_eq(indoc! {r"abc
3284 def
3285 ˇ
3286 paragraph
3287 the second
3288
3289
3290
3291 third and
3292 final"});
3293 }
3294
3295 #[gpui::test]
3296 async fn test_matching(cx: &mut gpui::TestAppContext) {
3297 let mut cx = NeovimBackedTestContext::new(cx).await;
3298
3299 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3300 do(something(with<Types>.and_arrays[0, 2]))
3301 }"})
3302 .await;
3303 cx.simulate_shared_keystrokes("%").await;
3304 cx.shared_state()
3305 .await
3306 .assert_eq(indoc! {r"func (a stringˇ) {
3307 do(something(with<Types>.and_arrays[0, 2]))
3308 }"});
3309
3310 // test it works on the last character of the line
3311 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3312 do(something(with<Types>.and_arrays[0, 2]))
3313 }"})
3314 .await;
3315 cx.simulate_shared_keystrokes("%").await;
3316 cx.shared_state()
3317 .await
3318 .assert_eq(indoc! {r"func (a string) {
3319 do(something(with<Types>.and_arrays[0, 2]))
3320 ˇ}"});
3321
3322 // test it works on immediate nesting
3323 cx.set_shared_state("ˇ{()}").await;
3324 cx.simulate_shared_keystrokes("%").await;
3325 cx.shared_state().await.assert_eq("{()ˇ}");
3326 cx.simulate_shared_keystrokes("%").await;
3327 cx.shared_state().await.assert_eq("ˇ{()}");
3328
3329 // test it works on immediate nesting inside braces
3330 cx.set_shared_state("{\n ˇ{()}\n}").await;
3331 cx.simulate_shared_keystrokes("%").await;
3332 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3333
3334 // test it jumps to the next paren on a line
3335 cx.set_shared_state("func ˇboop() {\n}").await;
3336 cx.simulate_shared_keystrokes("%").await;
3337 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3338 }
3339
3340 #[gpui::test]
3341 async fn test_matching_quotes_disabled(cx: &mut gpui::TestAppContext) {
3342 let mut cx = NeovimBackedTestContext::new(cx).await;
3343
3344 // Bind % to Matching with match_quotes: false to match Neovim's behavior
3345 // (Neovim's % doesn't match quotes by default)
3346 cx.update(|_, cx| {
3347 cx.bind_keys([KeyBinding::new(
3348 "%",
3349 Matching {
3350 match_quotes: false,
3351 },
3352 None,
3353 )]);
3354 });
3355
3356 cx.set_shared_state("one {two 'thˇree' four}").await;
3357 cx.simulate_shared_keystrokes("%").await;
3358 cx.shared_state().await.assert_eq("one ˇ{two 'three' four}");
3359
3360 cx.set_shared_state("'hello wˇorld'").await;
3361 cx.simulate_shared_keystrokes("%").await;
3362 cx.shared_state().await.assert_eq("'hello wˇorld'");
3363
3364 cx.set_shared_state(r#"func ("teˇst") {}"#).await;
3365 cx.simulate_shared_keystrokes("%").await;
3366 cx.shared_state().await.assert_eq(r#"func ˇ("test") {}"#);
3367
3368 cx.set_shared_state("ˇ'hello'").await;
3369 cx.simulate_shared_keystrokes("%").await;
3370 cx.shared_state().await.assert_eq("ˇ'hello'");
3371
3372 cx.set_shared_state("'helloˇ'").await;
3373 cx.simulate_shared_keystrokes("%").await;
3374 cx.shared_state().await.assert_eq("'helloˇ'");
3375
3376 cx.set_shared_state(indoc! {r"func (a string) {
3377 do('somethiˇng'))
3378 }"})
3379 .await;
3380 cx.simulate_shared_keystrokes("%").await;
3381 cx.shared_state()
3382 .await
3383 .assert_eq(indoc! {r"func (a string) {
3384 doˇ('something'))
3385 }"});
3386 }
3387
3388 #[gpui::test]
3389 async fn test_matching_quotes_enabled(cx: &mut gpui::TestAppContext) {
3390 let mut cx = VimTestContext::new_markdown_with_rust(cx).await;
3391
3392 // Test default behavior (match_quotes: true as configured in keymap/vim.json)
3393 cx.set_state("one {two 'thˇree' four}", Mode::Normal);
3394 cx.simulate_keystrokes("%");
3395 cx.assert_state("one {two ˇ'three' four}", Mode::Normal);
3396
3397 cx.set_state("'hello wˇorld'", Mode::Normal);
3398 cx.simulate_keystrokes("%");
3399 cx.assert_state("ˇ'hello world'", Mode::Normal);
3400
3401 cx.set_state(r#"func ('teˇst') {}"#, Mode::Normal);
3402 cx.simulate_keystrokes("%");
3403 cx.assert_state(r#"func (ˇ'test') {}"#, Mode::Normal);
3404
3405 cx.set_state("ˇ'hello'", Mode::Normal);
3406 cx.simulate_keystrokes("%");
3407 cx.assert_state("'helloˇ'", Mode::Normal);
3408
3409 cx.set_state("'helloˇ'", Mode::Normal);
3410 cx.simulate_keystrokes("%");
3411 cx.assert_state("ˇ'hello'", Mode::Normal);
3412
3413 cx.set_state(
3414 indoc! {r"func (a string) {
3415 do('somethiˇng'))
3416 }"},
3417 Mode::Normal,
3418 );
3419 cx.simulate_keystrokes("%");
3420 cx.assert_state(
3421 indoc! {r"func (a string) {
3422 do(ˇ'something'))
3423 }"},
3424 Mode::Normal,
3425 );
3426 }
3427
3428 #[gpui::test]
3429 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3430 let mut cx = NeovimBackedTestContext::new(cx).await;
3431
3432 // test it works with curly braces
3433 cx.set_shared_state(indoc! {r"func (a string) {
3434 do(something(with<Types>.anˇd_arrays[0, 2]))
3435 }"})
3436 .await;
3437 cx.simulate_shared_keystrokes("] }").await;
3438 cx.shared_state()
3439 .await
3440 .assert_eq(indoc! {r"func (a string) {
3441 do(something(with<Types>.and_arrays[0, 2]))
3442 ˇ}"});
3443
3444 // test it works with brackets
3445 cx.set_shared_state(indoc! {r"func (a string) {
3446 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3447 }"})
3448 .await;
3449 cx.simulate_shared_keystrokes("] )").await;
3450 cx.shared_state()
3451 .await
3452 .assert_eq(indoc! {r"func (a string) {
3453 do(something(with<Types>.and_arrays[0, 2])ˇ)
3454 }"});
3455
3456 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3457 .await;
3458 cx.simulate_shared_keystrokes("] )").await;
3459 cx.shared_state()
3460 .await
3461 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3462
3463 // test it works on immediate nesting
3464 cx.set_shared_state("{ˇ {}{}}").await;
3465 cx.simulate_shared_keystrokes("] }").await;
3466 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3467 cx.set_shared_state("(ˇ ()())").await;
3468 cx.simulate_shared_keystrokes("] )").await;
3469 cx.shared_state().await.assert_eq("( ()()ˇ)");
3470
3471 // test it works on immediate nesting inside braces
3472 cx.set_shared_state("{\n ˇ {()}\n}").await;
3473 cx.simulate_shared_keystrokes("] }").await;
3474 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3475 cx.set_shared_state("(\n ˇ {()}\n)").await;
3476 cx.simulate_shared_keystrokes("] )").await;
3477 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3478 }
3479
3480 #[gpui::test]
3481 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3482 let mut cx = NeovimBackedTestContext::new(cx).await;
3483
3484 // test it works with curly braces
3485 cx.set_shared_state(indoc! {r"func (a string) {
3486 do(something(with<Types>.anˇd_arrays[0, 2]))
3487 }"})
3488 .await;
3489 cx.simulate_shared_keystrokes("[ {").await;
3490 cx.shared_state()
3491 .await
3492 .assert_eq(indoc! {r"func (a string) ˇ{
3493 do(something(with<Types>.and_arrays[0, 2]))
3494 }"});
3495
3496 // test it works with brackets
3497 cx.set_shared_state(indoc! {r"func (a string) {
3498 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3499 }"})
3500 .await;
3501 cx.simulate_shared_keystrokes("[ (").await;
3502 cx.shared_state()
3503 .await
3504 .assert_eq(indoc! {r"func (a string) {
3505 doˇ(something(with<Types>.and_arrays[0, 2]))
3506 }"});
3507
3508 // test it works on immediate nesting
3509 cx.set_shared_state("{{}{} ˇ }").await;
3510 cx.simulate_shared_keystrokes("[ {").await;
3511 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3512 cx.set_shared_state("(()() ˇ )").await;
3513 cx.simulate_shared_keystrokes("[ (").await;
3514 cx.shared_state().await.assert_eq("ˇ(()() )");
3515
3516 // test it works on immediate nesting inside braces
3517 cx.set_shared_state("{\n {()} ˇ\n}").await;
3518 cx.simulate_shared_keystrokes("[ {").await;
3519 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3520 cx.set_shared_state("(\n {()} ˇ\n)").await;
3521 cx.simulate_shared_keystrokes("[ (").await;
3522 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3523 }
3524
3525 #[gpui::test]
3526 async fn test_unmatched_forward_markdown(cx: &mut gpui::TestAppContext) {
3527 let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3528
3529 cx.neovim.exec("set filetype=markdown").await;
3530
3531 cx.set_shared_state(indoc! {r"
3532 ```rs
3533 impl Worktree {
3534 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3535 ˇ }
3536 }
3537 ```
3538 "})
3539 .await;
3540 cx.simulate_shared_keystrokes("] }").await;
3541 cx.shared_state().await.assert_eq(indoc! {r"
3542 ```rs
3543 impl Worktree {
3544 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3545 ˇ}
3546 }
3547 ```
3548 "});
3549
3550 cx.set_shared_state(indoc! {r"
3551 ```rs
3552 impl Worktree {
3553 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3554 } ˇ
3555 }
3556 ```
3557 "})
3558 .await;
3559 cx.simulate_shared_keystrokes("] }").await;
3560 cx.shared_state().await.assert_eq(indoc! {r"
3561 ```rs
3562 impl Worktree {
3563 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3564 } •
3565 ˇ}
3566 ```
3567 "});
3568 }
3569
3570 #[gpui::test]
3571 async fn test_unmatched_backward_markdown(cx: &mut gpui::TestAppContext) {
3572 let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3573
3574 cx.neovim.exec("set filetype=markdown").await;
3575
3576 cx.set_shared_state(indoc! {r"
3577 ```rs
3578 impl Worktree {
3579 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3580 ˇ }
3581 }
3582 ```
3583 "})
3584 .await;
3585 cx.simulate_shared_keystrokes("[ {").await;
3586 cx.shared_state().await.assert_eq(indoc! {r"
3587 ```rs
3588 impl Worktree {
3589 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
3590 }
3591 }
3592 ```
3593 "});
3594
3595 cx.set_shared_state(indoc! {r"
3596 ```rs
3597 impl Worktree {
3598 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3599 } ˇ
3600 }
3601 ```
3602 "})
3603 .await;
3604 cx.simulate_shared_keystrokes("[ {").await;
3605 cx.shared_state().await.assert_eq(indoc! {r"
3606 ```rs
3607 impl Worktree ˇ{
3608 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3609 } •
3610 }
3611 ```
3612 "});
3613 }
3614
3615 #[gpui::test]
3616 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3617 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3618
3619 cx.neovim.exec("set filetype=html").await;
3620
3621 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3622 cx.simulate_shared_keystrokes("%").await;
3623 cx.shared_state()
3624 .await
3625 .assert_eq(indoc! {r"<body><ˇ/body>"});
3626 cx.simulate_shared_keystrokes("%").await;
3627
3628 // test jumping backwards
3629 cx.shared_state()
3630 .await
3631 .assert_eq(indoc! {r"<ˇbody></body>"});
3632
3633 // test self-closing tags
3634 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3635 cx.simulate_shared_keystrokes("%").await;
3636 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3637
3638 // test tag with attributes
3639 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3640 </div>
3641 "})
3642 .await;
3643 cx.simulate_shared_keystrokes("%").await;
3644 cx.shared_state()
3645 .await
3646 .assert_eq(indoc! {r"<div class='test' id='main'>
3647 <ˇ/div>
3648 "});
3649
3650 // test multi-line self-closing tag
3651 cx.set_shared_state(indoc! {r#"<a>
3652 <br
3653 test = "test"
3654 /ˇ>
3655 </a>"#})
3656 .await;
3657 cx.simulate_shared_keystrokes("%").await;
3658 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3659 ˇ<br
3660 test = "test"
3661 />
3662 </a>"#});
3663
3664 // test nested closing tag
3665 cx.set_shared_state(indoc! {r#"<html>
3666 <bˇody>
3667 </body>
3668 </html>"#})
3669 .await;
3670 cx.simulate_shared_keystrokes("%").await;
3671 cx.shared_state().await.assert_eq(indoc! {r#"<html>
3672 <body>
3673 <ˇ/body>
3674 </html>"#});
3675 cx.simulate_shared_keystrokes("%").await;
3676 cx.shared_state().await.assert_eq(indoc! {r#"<html>
3677 <ˇbody>
3678 </body>
3679 </html>"#});
3680 }
3681
3682 #[gpui::test]
3683 async fn test_matching_tag_with_quotes(cx: &mut gpui::TestAppContext) {
3684 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3685 cx.update(|_, cx| {
3686 cx.bind_keys([KeyBinding::new(
3687 "%",
3688 Matching {
3689 match_quotes: false,
3690 },
3691 None,
3692 )]);
3693 });
3694
3695 cx.neovim.exec("set filetype=html").await;
3696 cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
3697 </div>
3698 "})
3699 .await;
3700 cx.simulate_shared_keystrokes("%").await;
3701 cx.shared_state()
3702 .await
3703 .assert_eq(indoc! {r"<div class='test' id='main'>
3704 <ˇ/div>
3705 "});
3706
3707 cx.update(|_, cx| {
3708 cx.bind_keys([KeyBinding::new("%", Matching { match_quotes: true }, None)]);
3709 });
3710
3711 cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
3712 </div>
3713 "})
3714 .await;
3715 cx.simulate_shared_keystrokes("%").await;
3716 cx.shared_state()
3717 .await
3718 .assert_eq(indoc! {r"<div class='test' id='main'>
3719 <ˇ/div>
3720 "});
3721 }
3722 #[gpui::test]
3723 async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3724 let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3725
3726 // test brackets within tags
3727 cx.set_shared_state(indoc! {r"function f() {
3728 return (
3729 <div rules={ˇ[{ a: 1 }]}>
3730 <h1>test</h1>
3731 </div>
3732 );
3733 }"})
3734 .await;
3735 cx.simulate_shared_keystrokes("%").await;
3736 cx.shared_state().await.assert_eq(indoc! {r"function f() {
3737 return (
3738 <div rules={[{ a: 1 }ˇ]}>
3739 <h1>test</h1>
3740 </div>
3741 );
3742 }"});
3743 }
3744
3745 #[gpui::test]
3746 async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
3747 let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
3748
3749 cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
3750 .await;
3751 cx.simulate_shared_keystrokes("%").await;
3752 cx.shared_state()
3753 .await
3754 .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
3755 cx.simulate_shared_keystrokes("%").await;
3756 cx.shared_state()
3757 .await
3758 .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
3759 }
3760
3761 #[gpui::test]
3762 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3763 let mut cx = NeovimBackedTestContext::new(cx).await;
3764
3765 // f and F
3766 cx.set_shared_state("ˇone two three four").await;
3767 cx.simulate_shared_keystrokes("f o").await;
3768 cx.shared_state().await.assert_eq("one twˇo three four");
3769 cx.simulate_shared_keystrokes(",").await;
3770 cx.shared_state().await.assert_eq("ˇone two three four");
3771 cx.simulate_shared_keystrokes("2 ;").await;
3772 cx.shared_state().await.assert_eq("one two three fˇour");
3773 cx.simulate_shared_keystrokes("shift-f e").await;
3774 cx.shared_state().await.assert_eq("one two threˇe four");
3775 cx.simulate_shared_keystrokes("2 ;").await;
3776 cx.shared_state().await.assert_eq("onˇe two three four");
3777 cx.simulate_shared_keystrokes(",").await;
3778 cx.shared_state().await.assert_eq("one two thrˇee four");
3779
3780 // t and T
3781 cx.set_shared_state("ˇone two three four").await;
3782 cx.simulate_shared_keystrokes("t o").await;
3783 cx.shared_state().await.assert_eq("one tˇwo three four");
3784 cx.simulate_shared_keystrokes(",").await;
3785 cx.shared_state().await.assert_eq("oˇne two three four");
3786 cx.simulate_shared_keystrokes("2 ;").await;
3787 cx.shared_state().await.assert_eq("one two three ˇfour");
3788 cx.simulate_shared_keystrokes("shift-t e").await;
3789 cx.shared_state().await.assert_eq("one two threeˇ four");
3790 cx.simulate_shared_keystrokes("3 ;").await;
3791 cx.shared_state().await.assert_eq("oneˇ two three four");
3792 cx.simulate_shared_keystrokes(",").await;
3793 cx.shared_state().await.assert_eq("one two thˇree four");
3794 }
3795
3796 #[gpui::test]
3797 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3798 let mut cx = NeovimBackedTestContext::new(cx).await;
3799 let initial_state = indoc! {r"something(ˇfoo)"};
3800 cx.set_shared_state(initial_state).await;
3801 cx.simulate_shared_keystrokes("}").await;
3802 cx.shared_state().await.assert_eq("something(fooˇ)");
3803 }
3804
3805 #[gpui::test]
3806 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3807 let mut cx = NeovimBackedTestContext::new(cx).await;
3808 cx.set_shared_state("ˇone\n two\nthree").await;
3809 cx.simulate_shared_keystrokes("enter").await;
3810 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3811 }
3812
3813 #[gpui::test]
3814 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3815 let mut cx = NeovimBackedTestContext::new(cx).await;
3816 cx.set_shared_state("ˇ one\n two \nthree").await;
3817 cx.simulate_shared_keystrokes("g _").await;
3818 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3819
3820 cx.set_shared_state("ˇ one \n two \nthree").await;
3821 cx.simulate_shared_keystrokes("g _").await;
3822 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3823 cx.simulate_shared_keystrokes("2 g _").await;
3824 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3825 }
3826
3827 #[gpui::test]
3828 async fn test_end_of_line_with_vertical_motion(cx: &mut gpui::TestAppContext) {
3829 let mut cx = NeovimBackedTestContext::new(cx).await;
3830
3831 // test $ followed by k maintains end-of-line position
3832 cx.set_shared_state(indoc! {"
3833 The quick brown
3834 fˇox
3835 jumps over the
3836 lazy dog
3837 "})
3838 .await;
3839 cx.simulate_shared_keystrokes("$ k").await;
3840 cx.shared_state().await.assert_eq(indoc! {"
3841 The quick browˇn
3842 fox
3843 jumps over the
3844 lazy dog
3845 "});
3846 cx.simulate_shared_keystrokes("j j").await;
3847 cx.shared_state().await.assert_eq(indoc! {"
3848 The quick brown
3849 fox
3850 jumps over thˇe
3851 lazy dog
3852 "});
3853
3854 // test horizontal movement resets the end-of-line behavior
3855 cx.set_shared_state(indoc! {"
3856 The quick brown fox
3857 jumps over the
3858 lazy ˇdog
3859 "})
3860 .await;
3861 cx.simulate_shared_keystrokes("$ k").await;
3862 cx.shared_state().await.assert_eq(indoc! {"
3863 The quick brown fox
3864 jumps over thˇe
3865 lazy dog
3866 "});
3867 cx.simulate_shared_keystrokes("b b").await;
3868 cx.shared_state().await.assert_eq(indoc! {"
3869 The quick brown fox
3870 jumps ˇover the
3871 lazy dog
3872 "});
3873 cx.simulate_shared_keystrokes("k").await;
3874 cx.shared_state().await.assert_eq(indoc! {"
3875 The quˇick brown fox
3876 jumps over the
3877 lazy dog
3878 "});
3879 }
3880
3881 #[gpui::test]
3882 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3883 let mut cx = NeovimBackedTestContext::new(cx).await;
3884 let initial_state = indoc! {r"abc
3885 def
3886 paragraph
3887 the second
3888 third ˇand
3889 final"};
3890
3891 cx.set_shared_state(initial_state).await;
3892 cx.simulate_shared_keystrokes("shift-h").await;
3893 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3894 def
3895 paragraph
3896 the second
3897 third and
3898 final"});
3899
3900 // clip point
3901 cx.set_shared_state(indoc! {r"
3902 1 2 3
3903 4 5 6
3904 7 8 ˇ9
3905 "})
3906 .await;
3907 cx.simulate_shared_keystrokes("shift-h").await;
3908 cx.shared_state().await.assert_eq(indoc! {"
3909 1 2 ˇ3
3910 4 5 6
3911 7 8 9
3912 "});
3913
3914 cx.set_shared_state(indoc! {r"
3915 1 2 3
3916 4 5 6
3917 ˇ7 8 9
3918 "})
3919 .await;
3920 cx.simulate_shared_keystrokes("shift-h").await;
3921 cx.shared_state().await.assert_eq(indoc! {"
3922 ˇ1 2 3
3923 4 5 6
3924 7 8 9
3925 "});
3926
3927 cx.set_shared_state(indoc! {r"
3928 1 2 3
3929 4 5 ˇ6
3930 7 8 9"})
3931 .await;
3932 cx.simulate_shared_keystrokes("9 shift-h").await;
3933 cx.shared_state().await.assert_eq(indoc! {"
3934 1 2 3
3935 4 5 6
3936 7 8 ˇ9"});
3937 }
3938
3939 #[gpui::test]
3940 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3941 let mut cx = NeovimBackedTestContext::new(cx).await;
3942 let initial_state = indoc! {r"abˇc
3943 def
3944 paragraph
3945 the second
3946 third and
3947 final"};
3948
3949 cx.set_shared_state(initial_state).await;
3950 cx.simulate_shared_keystrokes("shift-m").await;
3951 cx.shared_state().await.assert_eq(indoc! {r"abc
3952 def
3953 paˇragraph
3954 the second
3955 third and
3956 final"});
3957
3958 cx.set_shared_state(indoc! {r"
3959 1 2 3
3960 4 5 6
3961 7 8 ˇ9
3962 "})
3963 .await;
3964 cx.simulate_shared_keystrokes("shift-m").await;
3965 cx.shared_state().await.assert_eq(indoc! {"
3966 1 2 3
3967 4 5 ˇ6
3968 7 8 9
3969 "});
3970 cx.set_shared_state(indoc! {r"
3971 1 2 3
3972 4 5 6
3973 ˇ7 8 9
3974 "})
3975 .await;
3976 cx.simulate_shared_keystrokes("shift-m").await;
3977 cx.shared_state().await.assert_eq(indoc! {"
3978 1 2 3
3979 ˇ4 5 6
3980 7 8 9
3981 "});
3982 cx.set_shared_state(indoc! {r"
3983 ˇ1 2 3
3984 4 5 6
3985 7 8 9
3986 "})
3987 .await;
3988 cx.simulate_shared_keystrokes("shift-m").await;
3989 cx.shared_state().await.assert_eq(indoc! {"
3990 1 2 3
3991 ˇ4 5 6
3992 7 8 9
3993 "});
3994 cx.set_shared_state(indoc! {r"
3995 1 2 3
3996 ˇ4 5 6
3997 7 8 9
3998 "})
3999 .await;
4000 cx.simulate_shared_keystrokes("shift-m").await;
4001 cx.shared_state().await.assert_eq(indoc! {"
4002 1 2 3
4003 ˇ4 5 6
4004 7 8 9
4005 "});
4006 cx.set_shared_state(indoc! {r"
4007 1 2 3
4008 4 5 ˇ6
4009 7 8 9
4010 "})
4011 .await;
4012 cx.simulate_shared_keystrokes("shift-m").await;
4013 cx.shared_state().await.assert_eq(indoc! {"
4014 1 2 3
4015 4 5 ˇ6
4016 7 8 9
4017 "});
4018 }
4019
4020 #[gpui::test]
4021 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
4022 let mut cx = NeovimBackedTestContext::new(cx).await;
4023 let initial_state = indoc! {r"abc
4024 deˇf
4025 paragraph
4026 the second
4027 third and
4028 final"};
4029
4030 cx.set_shared_state(initial_state).await;
4031 cx.simulate_shared_keystrokes("shift-l").await;
4032 cx.shared_state().await.assert_eq(indoc! {r"abc
4033 def
4034 paragraph
4035 the second
4036 third and
4037 fiˇnal"});
4038
4039 cx.set_shared_state(indoc! {r"
4040 1 2 3
4041 4 5 ˇ6
4042 7 8 9
4043 "})
4044 .await;
4045 cx.simulate_shared_keystrokes("shift-l").await;
4046 cx.shared_state().await.assert_eq(indoc! {"
4047 1 2 3
4048 4 5 6
4049 7 8 9
4050 ˇ"});
4051
4052 cx.set_shared_state(indoc! {r"
4053 1 2 3
4054 ˇ4 5 6
4055 7 8 9
4056 "})
4057 .await;
4058 cx.simulate_shared_keystrokes("shift-l").await;
4059 cx.shared_state().await.assert_eq(indoc! {"
4060 1 2 3
4061 4 5 6
4062 7 8 9
4063 ˇ"});
4064
4065 cx.set_shared_state(indoc! {r"
4066 1 2 ˇ3
4067 4 5 6
4068 7 8 9
4069 "})
4070 .await;
4071 cx.simulate_shared_keystrokes("shift-l").await;
4072 cx.shared_state().await.assert_eq(indoc! {"
4073 1 2 3
4074 4 5 6
4075 7 8 9
4076 ˇ"});
4077
4078 cx.set_shared_state(indoc! {r"
4079 ˇ1 2 3
4080 4 5 6
4081 7 8 9
4082 "})
4083 .await;
4084 cx.simulate_shared_keystrokes("shift-l").await;
4085 cx.shared_state().await.assert_eq(indoc! {"
4086 1 2 3
4087 4 5 6
4088 7 8 9
4089 ˇ"});
4090
4091 cx.set_shared_state(indoc! {r"
4092 1 2 3
4093 4 5 ˇ6
4094 7 8 9
4095 "})
4096 .await;
4097 cx.simulate_shared_keystrokes("9 shift-l").await;
4098 cx.shared_state().await.assert_eq(indoc! {"
4099 1 2 ˇ3
4100 4 5 6
4101 7 8 9
4102 "});
4103 }
4104
4105 #[gpui::test]
4106 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
4107 let mut cx = NeovimBackedTestContext::new(cx).await;
4108 cx.set_shared_state(indoc! {r"
4109 456 5ˇ67 678
4110 "})
4111 .await;
4112 cx.simulate_shared_keystrokes("g e").await;
4113 cx.shared_state().await.assert_eq(indoc! {"
4114 45ˇ6 567 678
4115 "});
4116
4117 // Test times
4118 cx.set_shared_state(indoc! {r"
4119 123 234 345
4120 456 5ˇ67 678
4121 "})
4122 .await;
4123 cx.simulate_shared_keystrokes("4 g e").await;
4124 cx.shared_state().await.assert_eq(indoc! {"
4125 12ˇ3 234 345
4126 456 567 678
4127 "});
4128
4129 // With punctuation
4130 cx.set_shared_state(indoc! {r"
4131 123 234 345
4132 4;5.6 5ˇ67 678
4133 789 890 901
4134 "})
4135 .await;
4136 cx.simulate_shared_keystrokes("g e").await;
4137 cx.shared_state().await.assert_eq(indoc! {"
4138 123 234 345
4139 4;5.ˇ6 567 678
4140 789 890 901
4141 "});
4142
4143 // With punctuation and count
4144 cx.set_shared_state(indoc! {r"
4145 123 234 345
4146 4;5.6 5ˇ67 678
4147 789 890 901
4148 "})
4149 .await;
4150 cx.simulate_shared_keystrokes("5 g e").await;
4151 cx.shared_state().await.assert_eq(indoc! {"
4152 123 234 345
4153 ˇ4;5.6 567 678
4154 789 890 901
4155 "});
4156
4157 // newlines
4158 cx.set_shared_state(indoc! {r"
4159 123 234 345
4160
4161 78ˇ9 890 901
4162 "})
4163 .await;
4164 cx.simulate_shared_keystrokes("g e").await;
4165 cx.shared_state().await.assert_eq(indoc! {"
4166 123 234 345
4167 ˇ
4168 789 890 901
4169 "});
4170 cx.simulate_shared_keystrokes("g e").await;
4171 cx.shared_state().await.assert_eq(indoc! {"
4172 123 234 34ˇ5
4173
4174 789 890 901
4175 "});
4176
4177 // With punctuation
4178 cx.set_shared_state(indoc! {r"
4179 123 234 345
4180 4;5.ˇ6 567 678
4181 789 890 901
4182 "})
4183 .await;
4184 cx.simulate_shared_keystrokes("g shift-e").await;
4185 cx.shared_state().await.assert_eq(indoc! {"
4186 123 234 34ˇ5
4187 4;5.6 567 678
4188 789 890 901
4189 "});
4190
4191 // With multi byte char
4192 cx.set_shared_state(indoc! {r"
4193 bar ˇó
4194 "})
4195 .await;
4196 cx.simulate_shared_keystrokes("g e").await;
4197 cx.shared_state().await.assert_eq(indoc! {"
4198 baˇr ó
4199 "});
4200 }
4201
4202 #[gpui::test]
4203 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
4204 let mut cx = NeovimBackedTestContext::new(cx).await;
4205
4206 cx.set_shared_state(indoc! {"
4207 fn aˇ() {
4208 return
4209 }
4210 "})
4211 .await;
4212 cx.simulate_shared_keystrokes("v $ %").await;
4213 cx.shared_state().await.assert_eq(indoc! {"
4214 fn a«() {
4215 return
4216 }ˇ»
4217 "});
4218 }
4219
4220 #[gpui::test]
4221 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
4222 let mut cx = VimTestContext::new(cx, true).await;
4223
4224 cx.set_state(
4225 indoc! {"
4226 struct Foo {
4227 ˇ
4228 }
4229 "},
4230 Mode::Normal,
4231 );
4232
4233 cx.update_editor(|editor, _window, cx| {
4234 let range = editor.selections.newest_anchor().range();
4235 let inlay_text = " field: int,\n field2: string\n field3: float";
4236 let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
4237 editor.splice_inlays(&[], vec![inlay], cx);
4238 });
4239
4240 cx.simulate_keystrokes("j");
4241 cx.assert_state(
4242 indoc! {"
4243 struct Foo {
4244
4245 ˇ}
4246 "},
4247 Mode::Normal,
4248 );
4249 }
4250
4251 #[gpui::test]
4252 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
4253 let mut cx = VimTestContext::new(cx, true).await;
4254
4255 cx.set_state(
4256 indoc! {"
4257 ˇstruct Foo {
4258
4259 }
4260 "},
4261 Mode::Normal,
4262 );
4263 cx.update_editor(|editor, _window, cx| {
4264 let snapshot = editor.buffer().read(cx).snapshot(cx);
4265 let end_of_line =
4266 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
4267 let inlay_text = " hint";
4268 let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
4269 editor.splice_inlays(&[], vec![inlay], cx);
4270 });
4271 cx.simulate_keystrokes("$");
4272 cx.assert_state(
4273 indoc! {"
4274 struct Foo ˇ{
4275
4276 }
4277 "},
4278 Mode::Normal,
4279 );
4280 }
4281
4282 #[gpui::test]
4283 async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
4284 let mut cx = VimTestContext::new(cx, true).await;
4285
4286 // Test the exact scenario from issue #29134
4287 cx.set_state(
4288 indoc! {"
4289 fn main() {
4290 let this_is_a_long_name = Vec::<u32>::new();
4291 let new_oneˇ = this_is_a_long_name
4292 .iter()
4293 .map(|i| i + 1)
4294 .map(|i| i * 2)
4295 .collect::<Vec<_>>();
4296 }
4297 "},
4298 Mode::Normal,
4299 );
4300
4301 // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
4302 cx.update_editor(|editor, _window, cx| {
4303 let snapshot = editor.buffer().read(cx).snapshot(cx);
4304 // The empty line is at line 3 (0-indexed)
4305 let line_start = snapshot.anchor_after(Point::new(3, 0));
4306 let inlay_text = ": Vec<u32>";
4307 let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
4308 editor.splice_inlays(&[], vec![inlay], cx);
4309 });
4310
4311 // Enter visual mode
4312 cx.simulate_keystrokes("v");
4313 cx.assert_state(
4314 indoc! {"
4315 fn main() {
4316 let this_is_a_long_name = Vec::<u32>::new();
4317 let new_one« ˇ»= this_is_a_long_name
4318 .iter()
4319 .map(|i| i + 1)
4320 .map(|i| i * 2)
4321 .collect::<Vec<_>>();
4322 }
4323 "},
4324 Mode::Visual,
4325 );
4326
4327 // Move down - should go to the beginning of line 4, not skip to line 5
4328 cx.simulate_keystrokes("j");
4329 cx.assert_state(
4330 indoc! {"
4331 fn main() {
4332 let this_is_a_long_name = Vec::<u32>::new();
4333 let new_one« = this_is_a_long_name
4334 ˇ» .iter()
4335 .map(|i| i + 1)
4336 .map(|i| i * 2)
4337 .collect::<Vec<_>>();
4338 }
4339 "},
4340 Mode::Visual,
4341 );
4342
4343 // Test with multiple movements
4344 cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
4345
4346 // Add type hint on the empty line
4347 cx.update_editor(|editor, _window, cx| {
4348 let snapshot = editor.buffer().read(cx).snapshot(cx);
4349 let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
4350 let inlay_text = ": i32";
4351 let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
4352 editor.splice_inlays(&[], vec![inlay], cx);
4353 });
4354
4355 // Enter visual mode and move down twice
4356 cx.simulate_keystrokes("v j j");
4357 cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
4358 }
4359
4360 #[gpui::test]
4361 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
4362 let mut cx = NeovimBackedTestContext::new(cx).await;
4363 // Normal mode
4364 cx.set_shared_state(indoc! {"
4365 The ˇquick brown
4366 fox jumps over
4367 the lazy dog
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 .await;
4375 cx.simulate_shared_keystrokes("2 0 %").await;
4376 cx.shared_state().await.assert_eq(indoc! {"
4377 The quick brown
4378 fox ˇjumps over
4379 the lazy dog
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
4387 cx.simulate_shared_keystrokes("2 5 %").await;
4388 cx.shared_state().await.assert_eq(indoc! {"
4389 The quick brown
4390 fox jumps over
4391 the ˇlazy dog
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
4399 cx.simulate_shared_keystrokes("7 5 %").await;
4400 cx.shared_state().await.assert_eq(indoc! {"
4401 The quick brown
4402 fox jumps over
4403 the lazy dog
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
4411 // Visual mode
4412 cx.set_shared_state(indoc! {"
4413 The ˇquick brown
4414 fox jumps over
4415 the lazy dog
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 .await;
4423 cx.simulate_shared_keystrokes("v 5 0 %").await;
4424 cx.shared_state().await.assert_eq(indoc! {"
4425 The «quick brown
4426 fox jumps over
4427 the lazy dog
4428 The quick brown
4429 fox jˇ»umps over
4430 the lazy dog
4431 The quick brown
4432 fox jumps over
4433 the lazy dog"});
4434
4435 cx.set_shared_state(indoc! {"
4436 The ˇquick brown
4437 fox jumps over
4438 the lazy dog
4439 The quick brown
4440 fox jumps over
4441 the lazy dog
4442 The quick brown
4443 fox jumps over
4444 the lazy dog"})
4445 .await;
4446 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4447 cx.shared_state().await.assert_eq(indoc! {"
4448 The «quick brown
4449 fox jumps over
4450 the lazy dog
4451 The quick brown
4452 fox jumps over
4453 the lazy dog
4454 The quick brown
4455 fox jumps over
4456 the lˇ»azy dog"});
4457 }
4458
4459 #[gpui::test]
4460 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4461 let mut cx = NeovimBackedTestContext::new(cx).await;
4462
4463 cx.set_shared_state("ˇπππππ").await;
4464 cx.simulate_shared_keystrokes("3 space").await;
4465 cx.shared_state().await.assert_eq("πππˇππ");
4466 }
4467
4468 #[gpui::test]
4469 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4470 let mut cx = NeovimBackedTestContext::new(cx).await;
4471
4472 cx.set_shared_state(indoc! {"
4473 ππππˇπ
4474 πanotherline"})
4475 .await;
4476 cx.simulate_shared_keystrokes("4 space").await;
4477 cx.shared_state().await.assert_eq(indoc! {"
4478 πππππ
4479 πanˇotherline"});
4480 }
4481
4482 #[gpui::test]
4483 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4484 let mut cx = NeovimBackedTestContext::new(cx).await;
4485
4486 cx.set_shared_state(indoc! {"
4487 ππππ
4488 πanˇotherline"})
4489 .await;
4490 cx.simulate_shared_keystrokes("4 backspace").await;
4491 cx.shared_state().await.assert_eq(indoc! {"
4492 πππˇπ
4493 πanotherline"});
4494 }
4495
4496 #[gpui::test]
4497 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4498 let mut cx = VimTestContext::new(cx, true).await;
4499 cx.set_state(
4500 indoc! {
4501 "func empty(a string) bool {
4502 ˇif a == \"\" {
4503 return true
4504 }
4505 return false
4506 }"
4507 },
4508 Mode::Normal,
4509 );
4510 cx.simulate_keystrokes("[ -");
4511 cx.assert_state(
4512 indoc! {
4513 "ˇfunc empty(a string) bool {
4514 if a == \"\" {
4515 return true
4516 }
4517 return false
4518 }"
4519 },
4520 Mode::Normal,
4521 );
4522 cx.simulate_keystrokes("] =");
4523 cx.assert_state(
4524 indoc! {
4525 "func empty(a string) bool {
4526 if a == \"\" {
4527 return true
4528 }
4529 return false
4530 ˇ}"
4531 },
4532 Mode::Normal,
4533 );
4534 cx.simulate_keystrokes("[ +");
4535 cx.assert_state(
4536 indoc! {
4537 "func empty(a string) bool {
4538 if a == \"\" {
4539 return true
4540 }
4541 ˇreturn false
4542 }"
4543 },
4544 Mode::Normal,
4545 );
4546 cx.simulate_keystrokes("2 [ =");
4547 cx.assert_state(
4548 indoc! {
4549 "func empty(a string) bool {
4550 ˇif a == \"\" {
4551 return true
4552 }
4553 return false
4554 }"
4555 },
4556 Mode::Normal,
4557 );
4558 cx.simulate_keystrokes("] +");
4559 cx.assert_state(
4560 indoc! {
4561 "func empty(a string) bool {
4562 if a == \"\" {
4563 ˇreturn true
4564 }
4565 return false
4566 }"
4567 },
4568 Mode::Normal,
4569 );
4570 cx.simulate_keystrokes("] -");
4571 cx.assert_state(
4572 indoc! {
4573 "func empty(a string) bool {
4574 if a == \"\" {
4575 return true
4576 ˇ}
4577 return false
4578 }"
4579 },
4580 Mode::Normal,
4581 );
4582 }
4583
4584 #[gpui::test]
4585 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4586 let mut cx = NeovimBackedTestContext::new(cx).await;
4587 cx.set_shared_state("abˇc").await;
4588 cx.simulate_shared_keystrokes("delete").await;
4589 cx.shared_state().await.assert_eq("aˇb");
4590 }
4591
4592 #[gpui::test]
4593 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4594 let mut cx = NeovimBackedTestContext::new(cx).await;
4595
4596 cx.set_shared_state(indoc! {"
4597 ˇthe quick brown fox
4598 jumped over the lazy dog"})
4599 .await;
4600 cx.simulate_shared_keystrokes("d v 0").await;
4601 cx.shared_state().await.assert_eq(indoc! {"
4602 ˇhe quick brown fox
4603 jumped over the lazy dog"});
4604 assert!(!cx.cx.forced_motion());
4605
4606 cx.set_shared_state(indoc! {"
4607 the quick bˇrown fox
4608 jumped over the lazy dog"})
4609 .await;
4610 cx.simulate_shared_keystrokes("d v 0").await;
4611 cx.shared_state().await.assert_eq(indoc! {"
4612 ˇown fox
4613 jumped over the lazy dog"});
4614 assert!(!cx.cx.forced_motion());
4615
4616 cx.set_shared_state(indoc! {"
4617 the quick brown foˇx
4618 jumped over the lazy dog"})
4619 .await;
4620 cx.simulate_shared_keystrokes("d v 0").await;
4621 cx.shared_state().await.assert_eq(indoc! {"
4622 ˇ
4623 jumped over the lazy dog"});
4624 assert!(!cx.cx.forced_motion());
4625 }
4626
4627 #[gpui::test]
4628 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4629 let mut cx = NeovimBackedTestContext::new(cx).await;
4630
4631 cx.set_shared_state(indoc! {"
4632 ˇthe quick brown fox
4633 jumped over the lazy dog"})
4634 .await;
4635 cx.simulate_shared_keystrokes("d v g shift-m").await;
4636 cx.shared_state().await.assert_eq(indoc! {"
4637 ˇbrown fox
4638 jumped over the lazy dog"});
4639 assert!(!cx.cx.forced_motion());
4640
4641 cx.set_shared_state(indoc! {"
4642 the quick bˇrown fox
4643 jumped over the lazy dog"})
4644 .await;
4645 cx.simulate_shared_keystrokes("d v g shift-m").await;
4646 cx.shared_state().await.assert_eq(indoc! {"
4647 the quickˇown fox
4648 jumped over the lazy dog"});
4649 assert!(!cx.cx.forced_motion());
4650
4651 cx.set_shared_state(indoc! {"
4652 the quick brown foˇx
4653 jumped over the lazy dog"})
4654 .await;
4655 cx.simulate_shared_keystrokes("d v g shift-m").await;
4656 cx.shared_state().await.assert_eq(indoc! {"
4657 the quicˇk
4658 jumped over the lazy dog"});
4659 assert!(!cx.cx.forced_motion());
4660
4661 cx.set_shared_state(indoc! {"
4662 ˇthe quick brown fox
4663 jumped over the lazy dog"})
4664 .await;
4665 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4666 cx.shared_state().await.assert_eq(indoc! {"
4667 ˇ fox
4668 jumped over the lazy dog"});
4669 assert!(!cx.cx.forced_motion());
4670
4671 cx.set_shared_state(indoc! {"
4672 ˇthe quick brown fox
4673 jumped over the lazy dog"})
4674 .await;
4675 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4676 cx.shared_state().await.assert_eq(indoc! {"
4677 ˇuick brown fox
4678 jumped over the lazy dog"});
4679 assert!(!cx.cx.forced_motion());
4680 }
4681
4682 #[gpui::test]
4683 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4684 let mut cx = NeovimBackedTestContext::new(cx).await;
4685
4686 cx.set_shared_state(indoc! {"
4687 the quick brown foˇx
4688 jumped over the lazy dog"})
4689 .await;
4690 cx.simulate_shared_keystrokes("d v $").await;
4691 cx.shared_state().await.assert_eq(indoc! {"
4692 the quick brown foˇx
4693 jumped over the lazy dog"});
4694 assert!(!cx.cx.forced_motion());
4695
4696 cx.set_shared_state(indoc! {"
4697 ˇthe quick brown fox
4698 jumped over the lazy dog"})
4699 .await;
4700 cx.simulate_shared_keystrokes("d v $").await;
4701 cx.shared_state().await.assert_eq(indoc! {"
4702 ˇx
4703 jumped over the lazy dog"});
4704 assert!(!cx.cx.forced_motion());
4705 }
4706
4707 #[gpui::test]
4708 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4709 let mut cx = NeovimBackedTestContext::new(cx).await;
4710
4711 cx.set_shared_state(indoc! {"
4712 ˇthe quick brown fox
4713 jumped over the lazy dog"})
4714 .await;
4715 cx.simulate_shared_keystrokes("y v j p").await;
4716 cx.shared_state().await.assert_eq(indoc! {"
4717 the quick brown fox
4718 ˇthe quick brown fox
4719 jumped over the lazy dog"});
4720 assert!(!cx.cx.forced_motion());
4721
4722 cx.set_shared_state(indoc! {"
4723 the quick bˇrown fox
4724 jumped over the lazy dog"})
4725 .await;
4726 cx.simulate_shared_keystrokes("y v j p").await;
4727 cx.shared_state().await.assert_eq(indoc! {"
4728 the quick brˇrown fox
4729 jumped overown fox
4730 jumped over the lazy dog"});
4731 assert!(!cx.cx.forced_motion());
4732
4733 cx.set_shared_state(indoc! {"
4734 the quick brown foˇx
4735 jumped over the lazy dog"})
4736 .await;
4737 cx.simulate_shared_keystrokes("y v j p").await;
4738 cx.shared_state().await.assert_eq(indoc! {"
4739 the quick brown foxˇx
4740 jumped over the la
4741 jumped over the lazy dog"});
4742 assert!(!cx.cx.forced_motion());
4743
4744 cx.set_shared_state(indoc! {"
4745 the quick brown fox
4746 jˇumped over the lazy dog"})
4747 .await;
4748 cx.simulate_shared_keystrokes("y v k p").await;
4749 cx.shared_state().await.assert_eq(indoc! {"
4750 thˇhe quick brown fox
4751 je quick brown fox
4752 jumped over the lazy dog"});
4753 assert!(!cx.cx.forced_motion());
4754 }
4755
4756 #[gpui::test]
4757 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4758 let mut cx = NeovimBackedTestContext::new(cx).await;
4759
4760 cx.set_shared_state(indoc! {"
4761 ˇthe quick brown fox
4762 jumped over the lazy dog"})
4763 .await;
4764 cx.simulate_shared_keystrokes("d v e").await;
4765 cx.shared_state().await.assert_eq(indoc! {"
4766 ˇe quick brown fox
4767 jumped over the lazy dog"});
4768 assert!(!cx.cx.forced_motion());
4769
4770 cx.set_shared_state(indoc! {"
4771 the quick bˇrown fox
4772 jumped over the lazy dog"})
4773 .await;
4774 cx.simulate_shared_keystrokes("d v e").await;
4775 cx.shared_state().await.assert_eq(indoc! {"
4776 the quick bˇn fox
4777 jumped over the lazy dog"});
4778 assert!(!cx.cx.forced_motion());
4779
4780 cx.set_shared_state(indoc! {"
4781 the quick brown foˇx
4782 jumped over the lazy dog"})
4783 .await;
4784 cx.simulate_shared_keystrokes("d v e").await;
4785 cx.shared_state().await.assert_eq(indoc! {"
4786 the quick brown foˇd over the lazy dog"});
4787 assert!(!cx.cx.forced_motion());
4788 }
4789
4790 #[gpui::test]
4791 async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
4792 let mut cx = VimTestContext::new(cx, true).await;
4793
4794 // Setup custom keybindings for subword motions so we can use the bindings
4795 // in `simulate_keystrokes`.
4796 cx.update(|_window, cx| {
4797 cx.bind_keys([KeyBinding::new(
4798 "w",
4799 super::NextSubwordStart {
4800 ignore_punctuation: false,
4801 },
4802 None,
4803 )]);
4804 });
4805
4806 cx.set_state("ˇfoo.bar", Mode::Normal);
4807 cx.simulate_keystrokes("w");
4808 cx.assert_state("foo.ˇbar", Mode::Normal);
4809
4810 cx.set_state("ˇfoo(bar)", Mode::Normal);
4811 cx.simulate_keystrokes("w");
4812 cx.assert_state("fooˇ(bar)", Mode::Normal);
4813 cx.simulate_keystrokes("w");
4814 cx.assert_state("foo(ˇbar)", Mode::Normal);
4815 cx.simulate_keystrokes("w");
4816 cx.assert_state("foo(barˇ)", Mode::Normal);
4817
4818 cx.set_state("ˇfoo_bar_baz", Mode::Normal);
4819 cx.simulate_keystrokes("w");
4820 cx.assert_state("foo_ˇbar_baz", Mode::Normal);
4821 cx.simulate_keystrokes("w");
4822 cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
4823
4824 cx.set_state("ˇfooBarBaz", Mode::Normal);
4825 cx.simulate_keystrokes("w");
4826 cx.assert_state("fooˇBarBaz", Mode::Normal);
4827 cx.simulate_keystrokes("w");
4828 cx.assert_state("fooBarˇBaz", Mode::Normal);
4829
4830 cx.set_state("ˇfoo;bar", Mode::Normal);
4831 cx.simulate_keystrokes("w");
4832 cx.assert_state("foo;ˇbar", Mode::Normal);
4833 }
4834
4835 #[gpui::test]
4836 async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
4837 let mut cx = VimTestContext::new(cx, true).await;
4838
4839 // Setup custom keybindings for subword motions so we can use the bindings
4840 // in `simulate_keystrokes`.
4841 cx.update(|_window, cx| {
4842 cx.bind_keys([KeyBinding::new(
4843 "e",
4844 super::NextSubwordEnd {
4845 ignore_punctuation: false,
4846 },
4847 None,
4848 )]);
4849 });
4850
4851 cx.set_state("ˇfoo.bar", Mode::Normal);
4852 cx.simulate_keystrokes("e");
4853 cx.assert_state("foˇo.bar", Mode::Normal);
4854 cx.simulate_keystrokes("e");
4855 cx.assert_state("fooˇ.bar", Mode::Normal);
4856 cx.simulate_keystrokes("e");
4857 cx.assert_state("foo.baˇr", Mode::Normal);
4858
4859 cx.set_state("ˇfoo(bar)", Mode::Normal);
4860 cx.simulate_keystrokes("e");
4861 cx.assert_state("foˇo(bar)", Mode::Normal);
4862 cx.simulate_keystrokes("e");
4863 cx.assert_state("fooˇ(bar)", Mode::Normal);
4864 cx.simulate_keystrokes("e");
4865 cx.assert_state("foo(baˇr)", Mode::Normal);
4866 cx.simulate_keystrokes("e");
4867 cx.assert_state("foo(barˇ)", Mode::Normal);
4868
4869 cx.set_state("ˇfoo_bar_baz", Mode::Normal);
4870 cx.simulate_keystrokes("e");
4871 cx.assert_state("foˇo_bar_baz", Mode::Normal);
4872 cx.simulate_keystrokes("e");
4873 cx.assert_state("foo_baˇr_baz", Mode::Normal);
4874 cx.simulate_keystrokes("e");
4875 cx.assert_state("foo_bar_baˇz", Mode::Normal);
4876
4877 cx.set_state("ˇfooBarBaz", Mode::Normal);
4878 cx.simulate_keystrokes("e");
4879 cx.set_state("foˇoBarBaz", Mode::Normal);
4880 cx.simulate_keystrokes("e");
4881 cx.set_state("fooBaˇrBaz", Mode::Normal);
4882 cx.simulate_keystrokes("e");
4883 cx.set_state("fooBarBaˇz", Mode::Normal);
4884
4885 cx.set_state("ˇfoo;bar", Mode::Normal);
4886 cx.simulate_keystrokes("e");
4887 cx.set_state("foˇo;bar", Mode::Normal);
4888 cx.simulate_keystrokes("e");
4889 cx.set_state("fooˇ;bar", Mode::Normal);
4890 cx.simulate_keystrokes("e");
4891 cx.set_state("foo;baˇr", Mode::Normal);
4892 }
4893
4894 #[gpui::test]
4895 async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
4896 let mut cx = VimTestContext::new(cx, true).await;
4897
4898 // Setup custom keybindings for subword motions so we can use the bindings
4899 // in `simulate_keystrokes`.
4900 cx.update(|_window, cx| {
4901 cx.bind_keys([KeyBinding::new(
4902 "b",
4903 super::PreviousSubwordStart {
4904 ignore_punctuation: false,
4905 },
4906 None,
4907 )]);
4908 });
4909
4910 cx.set_state("foo.barˇ", Mode::Normal);
4911 cx.simulate_keystrokes("b");
4912 cx.assert_state("foo.ˇbar", Mode::Normal);
4913 cx.simulate_keystrokes("b");
4914 cx.assert_state("fooˇ.bar", Mode::Normal);
4915 cx.simulate_keystrokes("b");
4916 cx.assert_state("ˇfoo.bar", Mode::Normal);
4917
4918 cx.set_state("foo(barˇ)", Mode::Normal);
4919 cx.simulate_keystrokes("b");
4920 cx.assert_state("foo(ˇbar)", Mode::Normal);
4921 cx.simulate_keystrokes("b");
4922 cx.assert_state("fooˇ(bar)", Mode::Normal);
4923 cx.simulate_keystrokes("b");
4924 cx.assert_state("ˇfoo(bar)", Mode::Normal);
4925
4926 cx.set_state("foo_bar_bazˇ", Mode::Normal);
4927 cx.simulate_keystrokes("b");
4928 cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
4929 cx.simulate_keystrokes("b");
4930 cx.assert_state("foo_ˇbar_baz", Mode::Normal);
4931 cx.simulate_keystrokes("b");
4932 cx.assert_state("ˇfoo_bar_baz", Mode::Normal);
4933
4934 cx.set_state("fooBarBazˇ", Mode::Normal);
4935 cx.simulate_keystrokes("b");
4936 cx.assert_state("fooBarˇBaz", Mode::Normal);
4937 cx.simulate_keystrokes("b");
4938 cx.assert_state("fooˇBarBaz", Mode::Normal);
4939 cx.simulate_keystrokes("b");
4940 cx.assert_state("ˇfooBarBaz", Mode::Normal);
4941
4942 cx.set_state("foo;barˇ", Mode::Normal);
4943 cx.simulate_keystrokes("b");
4944 cx.assert_state("foo;ˇbar", Mode::Normal);
4945 cx.simulate_keystrokes("b");
4946 cx.assert_state("ˇfoo;bar", Mode::Normal);
4947 }
4948
4949 #[gpui::test]
4950 async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
4951 let mut cx = VimTestContext::new(cx, true).await;
4952
4953 // Setup custom keybindings for subword motions so we can use the bindings
4954 // in `simulate_keystrokes`.
4955 cx.update(|_window, cx| {
4956 cx.bind_keys([KeyBinding::new(
4957 "g e",
4958 super::PreviousSubwordEnd {
4959 ignore_punctuation: false,
4960 },
4961 None,
4962 )]);
4963 });
4964
4965 cx.set_state("foo.baˇr", Mode::Normal);
4966 cx.simulate_keystrokes("g e");
4967 cx.assert_state("fooˇ.bar", Mode::Normal);
4968 cx.simulate_keystrokes("g e");
4969 cx.assert_state("foˇo.bar", Mode::Normal);
4970
4971 cx.set_state("foo(barˇ)", Mode::Normal);
4972 cx.simulate_keystrokes("g e");
4973 cx.assert_state("foo(baˇr)", Mode::Normal);
4974 cx.simulate_keystrokes("g e");
4975 cx.assert_state("fooˇ(bar)", Mode::Normal);
4976 cx.simulate_keystrokes("g e");
4977 cx.assert_state("foˇo(bar)", Mode::Normal);
4978
4979 cx.set_state("foo_bar_baˇz", Mode::Normal);
4980 cx.simulate_keystrokes("g e");
4981 cx.assert_state("foo_baˇr_baz", Mode::Normal);
4982 cx.simulate_keystrokes("g e");
4983 cx.assert_state("foˇo_bar_baz", Mode::Normal);
4984
4985 cx.set_state("fooBarBaˇz", Mode::Normal);
4986 cx.simulate_keystrokes("g e");
4987 cx.assert_state("fooBaˇrBaz", Mode::Normal);
4988 cx.simulate_keystrokes("g e");
4989 cx.assert_state("foˇoBarBaz", Mode::Normal);
4990
4991 cx.set_state("foo;baˇr", Mode::Normal);
4992 cx.simulate_keystrokes("g e");
4993 cx.assert_state("fooˇ;bar", Mode::Normal);
4994 cx.simulate_keystrokes("g e");
4995 cx.assert_state("foˇo;bar", Mode::Normal);
4996 }
4997}