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 .scroll_top_display_point(map);
2875
2876 if first_visible_line.row() != DisplayRow(0)
2877 && text_layout_details.vertical_scroll_margin as usize > times
2878 {
2879 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2880 }
2881
2882 if let Some(visible_rows) = text_layout_details.visible_rows {
2883 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2884 let new_row = (first_visible_line.row().0 + (times as u32))
2885 .min(bottom_row)
2886 .min(map.max_point().row().0);
2887 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2888
2889 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2890 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2891 } else {
2892 let new_row =
2893 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2894 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2895
2896 let new_point = DisplayPoint::new(new_row, new_col);
2897 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2898 }
2899}
2900
2901fn window_middle(
2902 map: &DisplaySnapshot,
2903 point: DisplayPoint,
2904 text_layout_details: &TextLayoutDetails,
2905) -> (DisplayPoint, SelectionGoal) {
2906 if let Some(visible_rows) = text_layout_details.visible_rows {
2907 let first_visible_line = text_layout_details
2908 .scroll_anchor
2909 .scroll_top_display_point(map);
2910
2911 let max_visible_rows =
2912 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2913
2914 let new_row =
2915 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2916 let new_row = DisplayRow(new_row);
2917 let new_col = point.column().min(map.line_len(new_row));
2918 let new_point = DisplayPoint::new(new_row, new_col);
2919 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2920 } else {
2921 (point, SelectionGoal::None)
2922 }
2923}
2924
2925fn window_bottom(
2926 map: &DisplaySnapshot,
2927 point: DisplayPoint,
2928 text_layout_details: &TextLayoutDetails,
2929 mut times: usize,
2930) -> (DisplayPoint, SelectionGoal) {
2931 if let Some(visible_rows) = text_layout_details.visible_rows {
2932 let first_visible_line = text_layout_details
2933 .scroll_anchor
2934 .scroll_top_display_point(map);
2935 let bottom_row = first_visible_line.row().0
2936 + (visible_rows + text_layout_details.scroll_anchor.scroll_anchor.offset.y - 1.).floor()
2937 as u32;
2938 if bottom_row < map.max_point().row().0
2939 && text_layout_details.vertical_scroll_margin as usize > times
2940 {
2941 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2942 }
2943 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2944 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2945 {
2946 first_visible_line.row()
2947 } else {
2948 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2949 };
2950 let new_col = point.column().min(map.line_len(new_row));
2951 let new_point = DisplayPoint::new(new_row, new_col);
2952 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2953 } else {
2954 (point, SelectionGoal::None)
2955 }
2956}
2957
2958fn method_motion(
2959 map: &DisplaySnapshot,
2960 mut display_point: DisplayPoint,
2961 times: usize,
2962 direction: Direction,
2963 is_start: bool,
2964) -> DisplayPoint {
2965 let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else {
2966 return display_point;
2967 };
2968
2969 for _ in 0..times {
2970 let point = map.display_point_to_point(display_point, Bias::Left);
2971 let offset = point.to_offset(&map.buffer_snapshot()).0;
2972 let range = if direction == Direction::Prev {
2973 0..offset
2974 } else {
2975 offset..buffer.len()
2976 };
2977
2978 let possibilities = buffer
2979 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2980 .filter_map(|(range, object)| {
2981 if !matches!(object, language::TextObject::AroundFunction) {
2982 return None;
2983 }
2984
2985 let relevant = if is_start { range.start } else { range.end };
2986 if direction == Direction::Prev && relevant < offset {
2987 Some(relevant)
2988 } else if direction == Direction::Next && relevant > offset + 1 {
2989 Some(relevant)
2990 } else {
2991 None
2992 }
2993 });
2994
2995 let dest = if direction == Direction::Prev {
2996 possibilities.max().unwrap_or(offset)
2997 } else {
2998 possibilities.min().unwrap_or(offset)
2999 };
3000 let new_point = map.clip_point(MultiBufferOffset(dest).to_display_point(map), Bias::Left);
3001 if new_point == display_point {
3002 break;
3003 }
3004 display_point = new_point;
3005 }
3006 display_point
3007}
3008
3009fn comment_motion(
3010 map: &DisplaySnapshot,
3011 mut display_point: DisplayPoint,
3012 times: usize,
3013 direction: Direction,
3014) -> DisplayPoint {
3015 let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else {
3016 return display_point;
3017 };
3018
3019 for _ in 0..times {
3020 let point = map.display_point_to_point(display_point, Bias::Left);
3021 let offset = point.to_offset(&map.buffer_snapshot()).0;
3022 let range = if direction == Direction::Prev {
3023 0..offset
3024 } else {
3025 offset..buffer.len()
3026 };
3027
3028 let possibilities = buffer
3029 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
3030 .filter_map(|(range, object)| {
3031 if !matches!(object, language::TextObject::AroundComment) {
3032 return None;
3033 }
3034
3035 let relevant = if direction == Direction::Prev {
3036 range.start
3037 } else {
3038 range.end
3039 };
3040 if direction == Direction::Prev && relevant < offset {
3041 Some(relevant)
3042 } else if direction == Direction::Next && relevant > offset + 1 {
3043 Some(relevant)
3044 } else {
3045 None
3046 }
3047 });
3048
3049 let dest = if direction == Direction::Prev {
3050 possibilities.max().unwrap_or(offset)
3051 } else {
3052 possibilities.min().unwrap_or(offset)
3053 };
3054 let new_point = map.clip_point(MultiBufferOffset(dest).to_display_point(map), Bias::Left);
3055 if new_point == display_point {
3056 break;
3057 }
3058 display_point = new_point;
3059 }
3060
3061 display_point
3062}
3063
3064fn section_motion(
3065 map: &DisplaySnapshot,
3066 mut display_point: DisplayPoint,
3067 times: usize,
3068 direction: Direction,
3069 is_start: bool,
3070) -> DisplayPoint {
3071 if map.buffer_snapshot().as_singleton().is_some() {
3072 for _ in 0..times {
3073 let offset = map
3074 .display_point_to_point(display_point, Bias::Left)
3075 .to_offset(&map.buffer_snapshot());
3076 let range = if direction == Direction::Prev {
3077 MultiBufferOffset(0)..offset
3078 } else {
3079 offset..map.buffer_snapshot().len()
3080 };
3081
3082 // we set a max start depth here because we want a section to only be "top level"
3083 // similar to vim's default of '{' in the first column.
3084 // (and without it, ]] at the start of editor.rs is -very- slow)
3085 let mut possibilities = map
3086 .buffer_snapshot()
3087 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
3088 .filter(|(_, object)| {
3089 matches!(
3090 object,
3091 language::TextObject::AroundClass | language::TextObject::AroundFunction
3092 )
3093 })
3094 .collect::<Vec<_>>();
3095 possibilities.sort_by_key(|(range_a, _)| range_a.start);
3096 let mut prev_end = None;
3097 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
3098 if t == language::TextObject::AroundFunction
3099 && prev_end.is_some_and(|prev_end| prev_end > range.start)
3100 {
3101 return None;
3102 }
3103 prev_end = Some(range.end);
3104
3105 let relevant = if is_start { range.start } else { range.end };
3106 if direction == Direction::Prev && relevant < offset {
3107 Some(relevant)
3108 } else if direction == Direction::Next && relevant > offset + 1usize {
3109 Some(relevant)
3110 } else {
3111 None
3112 }
3113 });
3114
3115 let offset = if direction == Direction::Prev {
3116 possibilities.max().unwrap_or(MultiBufferOffset(0))
3117 } else {
3118 possibilities.min().unwrap_or(map.buffer_snapshot().len())
3119 };
3120
3121 let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3122 if new_point == display_point {
3123 break;
3124 }
3125 display_point = new_point;
3126 }
3127 return display_point;
3128 };
3129
3130 for _ in 0..times {
3131 let next_point = if is_start {
3132 movement::start_of_excerpt(map, display_point, direction)
3133 } else {
3134 movement::end_of_excerpt(map, display_point, direction)
3135 };
3136 if next_point == display_point {
3137 break;
3138 }
3139 display_point = next_point;
3140 }
3141
3142 display_point
3143}
3144
3145fn matches_indent_type(
3146 target_indent: &text::LineIndent,
3147 current_indent: &text::LineIndent,
3148 indent_type: IndentType,
3149) -> bool {
3150 match indent_type {
3151 IndentType::Lesser => {
3152 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3153 }
3154 IndentType::Greater => {
3155 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3156 }
3157 IndentType::Same => {
3158 target_indent.spaces == current_indent.spaces
3159 && target_indent.tabs == current_indent.tabs
3160 }
3161 }
3162}
3163
3164fn indent_motion(
3165 map: &DisplaySnapshot,
3166 mut display_point: DisplayPoint,
3167 times: usize,
3168 direction: Direction,
3169 indent_type: IndentType,
3170) -> DisplayPoint {
3171 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3172 let current_row = MultiBufferRow(buffer_point.row);
3173 let current_indent = map.line_indent_for_buffer_row(current_row);
3174 if current_indent.is_line_empty() {
3175 return display_point;
3176 }
3177 let max_row = map.max_point().to_point(map).row;
3178
3179 for _ in 0..times {
3180 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3181
3182 let target_row = match direction {
3183 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3184 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3185 !indent.is_line_empty()
3186 && matches_indent_type(&indent, ¤t_indent, indent_type)
3187 }),
3188 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3189 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3190 !indent.is_line_empty()
3191 && matches_indent_type(&indent, ¤t_indent, indent_type)
3192 }),
3193 }
3194 .unwrap_or(current_buffer_row);
3195
3196 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3197 let new_point = first_non_whitespace(map, false, new_point);
3198 if new_point == display_point {
3199 break;
3200 }
3201 display_point = new_point;
3202 }
3203 display_point
3204}
3205
3206#[cfg(test)]
3207mod test {
3208
3209 use crate::{
3210 motion::Matching,
3211 state::Mode,
3212 test::{NeovimBackedTestContext, VimTestContext},
3213 };
3214 use editor::Inlay;
3215 use gpui::KeyBinding;
3216 use indoc::indoc;
3217 use language::Point;
3218 use multi_buffer::MultiBufferRow;
3219
3220 #[gpui::test]
3221 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3222 let mut cx = NeovimBackedTestContext::new(cx).await;
3223
3224 let initial_state = indoc! {r"ˇabc
3225 def
3226
3227 paragraph
3228 the second
3229
3230
3231
3232 third and
3233 final"};
3234
3235 // goes down once
3236 cx.set_shared_state(initial_state).await;
3237 cx.simulate_shared_keystrokes("}").await;
3238 cx.shared_state().await.assert_eq(indoc! {r"abc
3239 def
3240 ˇ
3241 paragraph
3242 the second
3243
3244
3245
3246 third and
3247 final"});
3248
3249 // goes up once
3250 cx.simulate_shared_keystrokes("{").await;
3251 cx.shared_state().await.assert_eq(initial_state);
3252
3253 // goes down twice
3254 cx.simulate_shared_keystrokes("2 }").await;
3255 cx.shared_state().await.assert_eq(indoc! {r"abc
3256 def
3257
3258 paragraph
3259 the second
3260 ˇ
3261
3262
3263 third and
3264 final"});
3265
3266 // goes down over multiple blanks
3267 cx.simulate_shared_keystrokes("}").await;
3268 cx.shared_state().await.assert_eq(indoc! {r"abc
3269 def
3270
3271 paragraph
3272 the second
3273
3274
3275
3276 third and
3277 finaˇl"});
3278
3279 // goes up twice
3280 cx.simulate_shared_keystrokes("2 {").await;
3281 cx.shared_state().await.assert_eq(indoc! {r"abc
3282 def
3283 ˇ
3284 paragraph
3285 the second
3286
3287
3288
3289 third and
3290 final"});
3291 }
3292
3293 #[gpui::test]
3294 async fn test_paragraph_motion_with_whitespace_lines(cx: &mut gpui::TestAppContext) {
3295 let mut cx = NeovimBackedTestContext::new(cx).await;
3296
3297 // Test that whitespace-only lines are NOT treated as paragraph boundaries
3298 // Per vim's :help paragraph - only truly empty lines are boundaries
3299 // Line 2 has 4 spaces (whitespace-only), line 4 is truly empty
3300 cx.set_shared_state("ˇfirst\n \nstill first\n\nsecond")
3301 .await;
3302 cx.simulate_shared_keystrokes("}").await;
3303
3304 // Should skip whitespace-only line and stop at truly empty line
3305 let mut shared_state = cx.shared_state().await;
3306 shared_state.assert_eq("first\n \nstill first\nˇ\nsecond");
3307 shared_state.assert_matches();
3308
3309 // Should go back to original position
3310 cx.simulate_shared_keystrokes("{").await;
3311 let mut shared_state = cx.shared_state().await;
3312 shared_state.assert_eq("ˇfirst\n \nstill first\n\nsecond");
3313 shared_state.assert_matches();
3314 }
3315
3316 #[gpui::test]
3317 async fn test_matching(cx: &mut gpui::TestAppContext) {
3318 let mut cx = NeovimBackedTestContext::new(cx).await;
3319
3320 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3321 do(something(with<Types>.and_arrays[0, 2]))
3322 }"})
3323 .await;
3324 cx.simulate_shared_keystrokes("%").await;
3325 cx.shared_state()
3326 .await
3327 .assert_eq(indoc! {r"func (a stringˇ) {
3328 do(something(with<Types>.and_arrays[0, 2]))
3329 }"});
3330
3331 // test it works on the last character of the line
3332 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3333 do(something(with<Types>.and_arrays[0, 2]))
3334 }"})
3335 .await;
3336 cx.simulate_shared_keystrokes("%").await;
3337 cx.shared_state()
3338 .await
3339 .assert_eq(indoc! {r"func (a string) {
3340 do(something(with<Types>.and_arrays[0, 2]))
3341 ˇ}"});
3342
3343 // test it works on immediate nesting
3344 cx.set_shared_state("ˇ{()}").await;
3345 cx.simulate_shared_keystrokes("%").await;
3346 cx.shared_state().await.assert_eq("{()ˇ}");
3347 cx.simulate_shared_keystrokes("%").await;
3348 cx.shared_state().await.assert_eq("ˇ{()}");
3349
3350 // test it works on immediate nesting inside braces
3351 cx.set_shared_state("{\n ˇ{()}\n}").await;
3352 cx.simulate_shared_keystrokes("%").await;
3353 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3354
3355 // test it jumps to the next paren on a line
3356 cx.set_shared_state("func ˇboop() {\n}").await;
3357 cx.simulate_shared_keystrokes("%").await;
3358 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3359 }
3360
3361 #[gpui::test]
3362 async fn test_matching_quotes_disabled(cx: &mut gpui::TestAppContext) {
3363 let mut cx = NeovimBackedTestContext::new(cx).await;
3364
3365 // Bind % to Matching with match_quotes: false to match Neovim's behavior
3366 // (Neovim's % doesn't match quotes by default)
3367 cx.update(|_, cx| {
3368 cx.bind_keys([KeyBinding::new(
3369 "%",
3370 Matching {
3371 match_quotes: false,
3372 },
3373 None,
3374 )]);
3375 });
3376
3377 cx.set_shared_state("one {two 'thˇree' four}").await;
3378 cx.simulate_shared_keystrokes("%").await;
3379 cx.shared_state().await.assert_eq("one ˇ{two 'three' four}");
3380
3381 cx.set_shared_state("'hello wˇorld'").await;
3382 cx.simulate_shared_keystrokes("%").await;
3383 cx.shared_state().await.assert_eq("'hello wˇorld'");
3384
3385 cx.set_shared_state(r#"func ("teˇst") {}"#).await;
3386 cx.simulate_shared_keystrokes("%").await;
3387 cx.shared_state().await.assert_eq(r#"func ˇ("test") {}"#);
3388
3389 cx.set_shared_state("ˇ'hello'").await;
3390 cx.simulate_shared_keystrokes("%").await;
3391 cx.shared_state().await.assert_eq("ˇ'hello'");
3392
3393 cx.set_shared_state("'helloˇ'").await;
3394 cx.simulate_shared_keystrokes("%").await;
3395 cx.shared_state().await.assert_eq("'helloˇ'");
3396
3397 cx.set_shared_state(indoc! {r"func (a string) {
3398 do('somethiˇng'))
3399 }"})
3400 .await;
3401 cx.simulate_shared_keystrokes("%").await;
3402 cx.shared_state()
3403 .await
3404 .assert_eq(indoc! {r"func (a string) {
3405 doˇ('something'))
3406 }"});
3407 }
3408
3409 #[gpui::test]
3410 async fn test_matching_quotes_enabled(cx: &mut gpui::TestAppContext) {
3411 let mut cx = VimTestContext::new_markdown_with_rust(cx).await;
3412
3413 // Test default behavior (match_quotes: true as configured in keymap/vim.json)
3414 cx.set_state("one {two 'thˇree' four}", Mode::Normal);
3415 cx.simulate_keystrokes("%");
3416 cx.assert_state("one {two ˇ'three' four}", Mode::Normal);
3417
3418 cx.set_state("'hello wˇorld'", Mode::Normal);
3419 cx.simulate_keystrokes("%");
3420 cx.assert_state("ˇ'hello world'", Mode::Normal);
3421
3422 cx.set_state(r#"func ('teˇst') {}"#, Mode::Normal);
3423 cx.simulate_keystrokes("%");
3424 cx.assert_state(r#"func (ˇ'test') {}"#, Mode::Normal);
3425
3426 cx.set_state("ˇ'hello'", Mode::Normal);
3427 cx.simulate_keystrokes("%");
3428 cx.assert_state("'helloˇ'", Mode::Normal);
3429
3430 cx.set_state("'helloˇ'", Mode::Normal);
3431 cx.simulate_keystrokes("%");
3432 cx.assert_state("ˇ'hello'", Mode::Normal);
3433
3434 cx.set_state(
3435 indoc! {r"func (a string) {
3436 do('somethiˇng'))
3437 }"},
3438 Mode::Normal,
3439 );
3440 cx.simulate_keystrokes("%");
3441 cx.assert_state(
3442 indoc! {r"func (a string) {
3443 do(ˇ'something'))
3444 }"},
3445 Mode::Normal,
3446 );
3447 }
3448
3449 #[gpui::test]
3450 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3451 let mut cx = NeovimBackedTestContext::new(cx).await;
3452
3453 // test it works with curly braces
3454 cx.set_shared_state(indoc! {r"func (a string) {
3455 do(something(with<Types>.anˇd_arrays[0, 2]))
3456 }"})
3457 .await;
3458 cx.simulate_shared_keystrokes("] }").await;
3459 cx.shared_state()
3460 .await
3461 .assert_eq(indoc! {r"func (a string) {
3462 do(something(with<Types>.and_arrays[0, 2]))
3463 ˇ}"});
3464
3465 // test it works with brackets
3466 cx.set_shared_state(indoc! {r"func (a string) {
3467 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3468 }"})
3469 .await;
3470 cx.simulate_shared_keystrokes("] )").await;
3471 cx.shared_state()
3472 .await
3473 .assert_eq(indoc! {r"func (a string) {
3474 do(something(with<Types>.and_arrays[0, 2])ˇ)
3475 }"});
3476
3477 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3478 .await;
3479 cx.simulate_shared_keystrokes("] )").await;
3480 cx.shared_state()
3481 .await
3482 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3483
3484 // test it works on immediate nesting
3485 cx.set_shared_state("{ˇ {}{}}").await;
3486 cx.simulate_shared_keystrokes("] }").await;
3487 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3488 cx.set_shared_state("(ˇ ()())").await;
3489 cx.simulate_shared_keystrokes("] )").await;
3490 cx.shared_state().await.assert_eq("( ()()ˇ)");
3491
3492 // test it works on immediate nesting inside braces
3493 cx.set_shared_state("{\n ˇ {()}\n}").await;
3494 cx.simulate_shared_keystrokes("] }").await;
3495 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3496 cx.set_shared_state("(\n ˇ {()}\n)").await;
3497 cx.simulate_shared_keystrokes("] )").await;
3498 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3499 }
3500
3501 #[gpui::test]
3502 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3503 let mut cx = NeovimBackedTestContext::new(cx).await;
3504
3505 // test it works with curly braces
3506 cx.set_shared_state(indoc! {r"func (a string) {
3507 do(something(with<Types>.anˇd_arrays[0, 2]))
3508 }"})
3509 .await;
3510 cx.simulate_shared_keystrokes("[ {").await;
3511 cx.shared_state()
3512 .await
3513 .assert_eq(indoc! {r"func (a string) ˇ{
3514 do(something(with<Types>.and_arrays[0, 2]))
3515 }"});
3516
3517 // test it works with brackets
3518 cx.set_shared_state(indoc! {r"func (a string) {
3519 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3520 }"})
3521 .await;
3522 cx.simulate_shared_keystrokes("[ (").await;
3523 cx.shared_state()
3524 .await
3525 .assert_eq(indoc! {r"func (a string) {
3526 doˇ(something(with<Types>.and_arrays[0, 2]))
3527 }"});
3528
3529 // test it works on immediate nesting
3530 cx.set_shared_state("{{}{} ˇ }").await;
3531 cx.simulate_shared_keystrokes("[ {").await;
3532 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3533 cx.set_shared_state("(()() ˇ )").await;
3534 cx.simulate_shared_keystrokes("[ (").await;
3535 cx.shared_state().await.assert_eq("ˇ(()() )");
3536
3537 // test it works on immediate nesting inside braces
3538 cx.set_shared_state("{\n {()} ˇ\n}").await;
3539 cx.simulate_shared_keystrokes("[ {").await;
3540 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3541 cx.set_shared_state("(\n {()} ˇ\n)").await;
3542 cx.simulate_shared_keystrokes("[ (").await;
3543 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3544 }
3545
3546 #[gpui::test]
3547 async fn test_unmatched_forward_markdown(cx: &mut gpui::TestAppContext) {
3548 let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3549
3550 cx.neovim.exec("set filetype=markdown").await;
3551
3552 cx.set_shared_state(indoc! {r"
3553 ```rs
3554 impl Worktree {
3555 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3556 ˇ }
3557 }
3558 ```
3559 "})
3560 .await;
3561 cx.simulate_shared_keystrokes("] }").await;
3562 cx.shared_state().await.assert_eq(indoc! {r"
3563 ```rs
3564 impl Worktree {
3565 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3566 ˇ}
3567 }
3568 ```
3569 "});
3570
3571 cx.set_shared_state(indoc! {r"
3572 ```rs
3573 impl Worktree {
3574 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3575 } ˇ
3576 }
3577 ```
3578 "})
3579 .await;
3580 cx.simulate_shared_keystrokes("] }").await;
3581 cx.shared_state().await.assert_eq(indoc! {r"
3582 ```rs
3583 impl Worktree {
3584 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3585 } •
3586 ˇ}
3587 ```
3588 "});
3589 }
3590
3591 #[gpui::test]
3592 async fn test_unmatched_backward_markdown(cx: &mut gpui::TestAppContext) {
3593 let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3594
3595 cx.neovim.exec("set filetype=markdown").await;
3596
3597 cx.set_shared_state(indoc! {r"
3598 ```rs
3599 impl Worktree {
3600 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3601 ˇ }
3602 }
3603 ```
3604 "})
3605 .await;
3606 cx.simulate_shared_keystrokes("[ {").await;
3607 cx.shared_state().await.assert_eq(indoc! {r"
3608 ```rs
3609 impl Worktree {
3610 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
3611 }
3612 }
3613 ```
3614 "});
3615
3616 cx.set_shared_state(indoc! {r"
3617 ```rs
3618 impl Worktree {
3619 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3620 } ˇ
3621 }
3622 ```
3623 "})
3624 .await;
3625 cx.simulate_shared_keystrokes("[ {").await;
3626 cx.shared_state().await.assert_eq(indoc! {r"
3627 ```rs
3628 impl Worktree ˇ{
3629 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3630 } •
3631 }
3632 ```
3633 "});
3634 }
3635
3636 #[gpui::test]
3637 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3638 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3639
3640 cx.neovim.exec("set filetype=html").await;
3641
3642 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3643 cx.simulate_shared_keystrokes("%").await;
3644 cx.shared_state()
3645 .await
3646 .assert_eq(indoc! {r"<body><ˇ/body>"});
3647 cx.simulate_shared_keystrokes("%").await;
3648
3649 // test jumping backwards
3650 cx.shared_state()
3651 .await
3652 .assert_eq(indoc! {r"<ˇbody></body>"});
3653
3654 // test self-closing tags
3655 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3656 cx.simulate_shared_keystrokes("%").await;
3657 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3658
3659 // test tag with attributes
3660 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3661 </div>
3662 "})
3663 .await;
3664 cx.simulate_shared_keystrokes("%").await;
3665 cx.shared_state()
3666 .await
3667 .assert_eq(indoc! {r"<div class='test' id='main'>
3668 <ˇ/div>
3669 "});
3670
3671 // test multi-line self-closing tag
3672 cx.set_shared_state(indoc! {r#"<a>
3673 <br
3674 test = "test"
3675 /ˇ>
3676 </a>"#})
3677 .await;
3678 cx.simulate_shared_keystrokes("%").await;
3679 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3680 ˇ<br
3681 test = "test"
3682 />
3683 </a>"#});
3684
3685 // test nested closing tag
3686 cx.set_shared_state(indoc! {r#"<html>
3687 <bˇody>
3688 </body>
3689 </html>"#})
3690 .await;
3691 cx.simulate_shared_keystrokes("%").await;
3692 cx.shared_state().await.assert_eq(indoc! {r#"<html>
3693 <body>
3694 <ˇ/body>
3695 </html>"#});
3696 cx.simulate_shared_keystrokes("%").await;
3697 cx.shared_state().await.assert_eq(indoc! {r#"<html>
3698 <ˇbody>
3699 </body>
3700 </html>"#});
3701 }
3702
3703 #[gpui::test]
3704 async fn test_matching_tag_with_quotes(cx: &mut gpui::TestAppContext) {
3705 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3706 cx.update(|_, cx| {
3707 cx.bind_keys([KeyBinding::new(
3708 "%",
3709 Matching {
3710 match_quotes: false,
3711 },
3712 None,
3713 )]);
3714 });
3715
3716 cx.neovim.exec("set filetype=html").await;
3717 cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
3718 </div>
3719 "})
3720 .await;
3721 cx.simulate_shared_keystrokes("%").await;
3722 cx.shared_state()
3723 .await
3724 .assert_eq(indoc! {r"<div class='test' id='main'>
3725 <ˇ/div>
3726 "});
3727
3728 cx.update(|_, cx| {
3729 cx.bind_keys([KeyBinding::new("%", Matching { match_quotes: true }, None)]);
3730 });
3731
3732 cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
3733 </div>
3734 "})
3735 .await;
3736 cx.simulate_shared_keystrokes("%").await;
3737 cx.shared_state()
3738 .await
3739 .assert_eq(indoc! {r"<div class='test' id='main'>
3740 <ˇ/div>
3741 "});
3742 }
3743 #[gpui::test]
3744 async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3745 let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3746
3747 // test brackets within tags
3748 cx.set_shared_state(indoc! {r"function f() {
3749 return (
3750 <div rules={ˇ[{ a: 1 }]}>
3751 <h1>test</h1>
3752 </div>
3753 );
3754 }"})
3755 .await;
3756 cx.simulate_shared_keystrokes("%").await;
3757 cx.shared_state().await.assert_eq(indoc! {r"function f() {
3758 return (
3759 <div rules={[{ a: 1 }ˇ]}>
3760 <h1>test</h1>
3761 </div>
3762 );
3763 }"});
3764 }
3765
3766 #[gpui::test]
3767 async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
3768 let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
3769
3770 cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
3771 .await;
3772 cx.simulate_shared_keystrokes("%").await;
3773 cx.shared_state()
3774 .await
3775 .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
3776 cx.simulate_shared_keystrokes("%").await;
3777 cx.shared_state()
3778 .await
3779 .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
3780 }
3781
3782 #[gpui::test]
3783 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3784 let mut cx = NeovimBackedTestContext::new(cx).await;
3785
3786 // f and F
3787 cx.set_shared_state("ˇone two three four").await;
3788 cx.simulate_shared_keystrokes("f o").await;
3789 cx.shared_state().await.assert_eq("one twˇo three four");
3790 cx.simulate_shared_keystrokes(",").await;
3791 cx.shared_state().await.assert_eq("ˇone two three four");
3792 cx.simulate_shared_keystrokes("2 ;").await;
3793 cx.shared_state().await.assert_eq("one two three fˇour");
3794 cx.simulate_shared_keystrokes("shift-f e").await;
3795 cx.shared_state().await.assert_eq("one two threˇe four");
3796 cx.simulate_shared_keystrokes("2 ;").await;
3797 cx.shared_state().await.assert_eq("onˇe two three four");
3798 cx.simulate_shared_keystrokes(",").await;
3799 cx.shared_state().await.assert_eq("one two thrˇee four");
3800
3801 // t and T
3802 cx.set_shared_state("ˇone two three four").await;
3803 cx.simulate_shared_keystrokes("t o").await;
3804 cx.shared_state().await.assert_eq("one tˇwo three four");
3805 cx.simulate_shared_keystrokes(",").await;
3806 cx.shared_state().await.assert_eq("oˇne two three four");
3807 cx.simulate_shared_keystrokes("2 ;").await;
3808 cx.shared_state().await.assert_eq("one two three ˇfour");
3809 cx.simulate_shared_keystrokes("shift-t e").await;
3810 cx.shared_state().await.assert_eq("one two threeˇ four");
3811 cx.simulate_shared_keystrokes("3 ;").await;
3812 cx.shared_state().await.assert_eq("oneˇ two three four");
3813 cx.simulate_shared_keystrokes(",").await;
3814 cx.shared_state().await.assert_eq("one two thˇree four");
3815 }
3816
3817 #[gpui::test]
3818 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3819 let mut cx = NeovimBackedTestContext::new(cx).await;
3820 let initial_state = indoc! {r"something(ˇfoo)"};
3821 cx.set_shared_state(initial_state).await;
3822 cx.simulate_shared_keystrokes("}").await;
3823 cx.shared_state().await.assert_eq("something(fooˇ)");
3824 }
3825
3826 #[gpui::test]
3827 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3828 let mut cx = NeovimBackedTestContext::new(cx).await;
3829 cx.set_shared_state("ˇone\n two\nthree").await;
3830 cx.simulate_shared_keystrokes("enter").await;
3831 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3832 }
3833
3834 #[gpui::test]
3835 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3836 let mut cx = NeovimBackedTestContext::new(cx).await;
3837 cx.set_shared_state("ˇ one\n two \nthree").await;
3838 cx.simulate_shared_keystrokes("g _").await;
3839 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3840
3841 cx.set_shared_state("ˇ one \n two \nthree").await;
3842 cx.simulate_shared_keystrokes("g _").await;
3843 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3844 cx.simulate_shared_keystrokes("2 g _").await;
3845 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3846 }
3847
3848 #[gpui::test]
3849 async fn test_end_of_line_with_vertical_motion(cx: &mut gpui::TestAppContext) {
3850 let mut cx = NeovimBackedTestContext::new(cx).await;
3851
3852 // test $ followed by k maintains end-of-line position
3853 cx.set_shared_state(indoc! {"
3854 The quick brown
3855 fˇox
3856 jumps over the
3857 lazy dog
3858 "})
3859 .await;
3860 cx.simulate_shared_keystrokes("$ k").await;
3861 cx.shared_state().await.assert_eq(indoc! {"
3862 The quick browˇn
3863 fox
3864 jumps over the
3865 lazy dog
3866 "});
3867 cx.simulate_shared_keystrokes("j j").await;
3868 cx.shared_state().await.assert_eq(indoc! {"
3869 The quick brown
3870 fox
3871 jumps over thˇe
3872 lazy dog
3873 "});
3874
3875 // test horizontal movement resets the end-of-line behavior
3876 cx.set_shared_state(indoc! {"
3877 The quick brown fox
3878 jumps over the
3879 lazy ˇdog
3880 "})
3881 .await;
3882 cx.simulate_shared_keystrokes("$ k").await;
3883 cx.shared_state().await.assert_eq(indoc! {"
3884 The quick brown fox
3885 jumps over thˇe
3886 lazy dog
3887 "});
3888 cx.simulate_shared_keystrokes("b b").await;
3889 cx.shared_state().await.assert_eq(indoc! {"
3890 The quick brown fox
3891 jumps ˇover the
3892 lazy dog
3893 "});
3894 cx.simulate_shared_keystrokes("k").await;
3895 cx.shared_state().await.assert_eq(indoc! {"
3896 The quˇick brown fox
3897 jumps over the
3898 lazy dog
3899 "});
3900 }
3901
3902 #[gpui::test]
3903 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3904 let mut cx = NeovimBackedTestContext::new(cx).await;
3905 let initial_state = indoc! {r"abc
3906 def
3907 paragraph
3908 the second
3909 third ˇand
3910 final"};
3911
3912 cx.set_shared_state(initial_state).await;
3913 cx.simulate_shared_keystrokes("shift-h").await;
3914 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3915 def
3916 paragraph
3917 the second
3918 third and
3919 final"});
3920
3921 // clip point
3922 cx.set_shared_state(indoc! {r"
3923 1 2 3
3924 4 5 6
3925 7 8 ˇ9
3926 "})
3927 .await;
3928 cx.simulate_shared_keystrokes("shift-h").await;
3929 cx.shared_state().await.assert_eq(indoc! {"
3930 1 2 ˇ3
3931 4 5 6
3932 7 8 9
3933 "});
3934
3935 cx.set_shared_state(indoc! {r"
3936 1 2 3
3937 4 5 6
3938 ˇ7 8 9
3939 "})
3940 .await;
3941 cx.simulate_shared_keystrokes("shift-h").await;
3942 cx.shared_state().await.assert_eq(indoc! {"
3943 ˇ1 2 3
3944 4 5 6
3945 7 8 9
3946 "});
3947
3948 cx.set_shared_state(indoc! {r"
3949 1 2 3
3950 4 5 ˇ6
3951 7 8 9"})
3952 .await;
3953 cx.simulate_shared_keystrokes("9 shift-h").await;
3954 cx.shared_state().await.assert_eq(indoc! {"
3955 1 2 3
3956 4 5 6
3957 7 8 ˇ9"});
3958 }
3959
3960 #[gpui::test]
3961 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3962 let mut cx = NeovimBackedTestContext::new(cx).await;
3963 let initial_state = indoc! {r"abˇc
3964 def
3965 paragraph
3966 the second
3967 third and
3968 final"};
3969
3970 cx.set_shared_state(initial_state).await;
3971 cx.simulate_shared_keystrokes("shift-m").await;
3972 cx.shared_state().await.assert_eq(indoc! {r"abc
3973 def
3974 paˇragraph
3975 the second
3976 third and
3977 final"});
3978
3979 cx.set_shared_state(indoc! {r"
3980 1 2 3
3981 4 5 6
3982 7 8 ˇ9
3983 "})
3984 .await;
3985 cx.simulate_shared_keystrokes("shift-m").await;
3986 cx.shared_state().await.assert_eq(indoc! {"
3987 1 2 3
3988 4 5 ˇ6
3989 7 8 9
3990 "});
3991 cx.set_shared_state(indoc! {r"
3992 1 2 3
3993 4 5 6
3994 ˇ7 8 9
3995 "})
3996 .await;
3997 cx.simulate_shared_keystrokes("shift-m").await;
3998 cx.shared_state().await.assert_eq(indoc! {"
3999 1 2 3
4000 ˇ4 5 6
4001 7 8 9
4002 "});
4003 cx.set_shared_state(indoc! {r"
4004 ˇ1 2 3
4005 4 5 6
4006 7 8 9
4007 "})
4008 .await;
4009 cx.simulate_shared_keystrokes("shift-m").await;
4010 cx.shared_state().await.assert_eq(indoc! {"
4011 1 2 3
4012 ˇ4 5 6
4013 7 8 9
4014 "});
4015 cx.set_shared_state(indoc! {r"
4016 1 2 3
4017 ˇ4 5 6
4018 7 8 9
4019 "})
4020 .await;
4021 cx.simulate_shared_keystrokes("shift-m").await;
4022 cx.shared_state().await.assert_eq(indoc! {"
4023 1 2 3
4024 ˇ4 5 6
4025 7 8 9
4026 "});
4027 cx.set_shared_state(indoc! {r"
4028 1 2 3
4029 4 5 ˇ6
4030 7 8 9
4031 "})
4032 .await;
4033 cx.simulate_shared_keystrokes("shift-m").await;
4034 cx.shared_state().await.assert_eq(indoc! {"
4035 1 2 3
4036 4 5 ˇ6
4037 7 8 9
4038 "});
4039 }
4040
4041 #[gpui::test]
4042 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
4043 let mut cx = NeovimBackedTestContext::new(cx).await;
4044 let initial_state = indoc! {r"abc
4045 deˇf
4046 paragraph
4047 the second
4048 third and
4049 final"};
4050
4051 cx.set_shared_state(initial_state).await;
4052 cx.simulate_shared_keystrokes("shift-l").await;
4053 cx.shared_state().await.assert_eq(indoc! {r"abc
4054 def
4055 paragraph
4056 the second
4057 third and
4058 fiˇnal"});
4059
4060 cx.set_shared_state(indoc! {r"
4061 1 2 3
4062 4 5 ˇ6
4063 7 8 9
4064 "})
4065 .await;
4066 cx.simulate_shared_keystrokes("shift-l").await;
4067 cx.shared_state().await.assert_eq(indoc! {"
4068 1 2 3
4069 4 5 6
4070 7 8 9
4071 ˇ"});
4072
4073 cx.set_shared_state(indoc! {r"
4074 1 2 3
4075 ˇ4 5 6
4076 7 8 9
4077 "})
4078 .await;
4079 cx.simulate_shared_keystrokes("shift-l").await;
4080 cx.shared_state().await.assert_eq(indoc! {"
4081 1 2 3
4082 4 5 6
4083 7 8 9
4084 ˇ"});
4085
4086 cx.set_shared_state(indoc! {r"
4087 1 2 ˇ3
4088 4 5 6
4089 7 8 9
4090 "})
4091 .await;
4092 cx.simulate_shared_keystrokes("shift-l").await;
4093 cx.shared_state().await.assert_eq(indoc! {"
4094 1 2 3
4095 4 5 6
4096 7 8 9
4097 ˇ"});
4098
4099 cx.set_shared_state(indoc! {r"
4100 ˇ1 2 3
4101 4 5 6
4102 7 8 9
4103 "})
4104 .await;
4105 cx.simulate_shared_keystrokes("shift-l").await;
4106 cx.shared_state().await.assert_eq(indoc! {"
4107 1 2 3
4108 4 5 6
4109 7 8 9
4110 ˇ"});
4111
4112 cx.set_shared_state(indoc! {r"
4113 1 2 3
4114 4 5 ˇ6
4115 7 8 9
4116 "})
4117 .await;
4118 cx.simulate_shared_keystrokes("9 shift-l").await;
4119 cx.shared_state().await.assert_eq(indoc! {"
4120 1 2 ˇ3
4121 4 5 6
4122 7 8 9
4123 "});
4124 }
4125
4126 #[gpui::test]
4127 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
4128 let mut cx = NeovimBackedTestContext::new(cx).await;
4129 cx.set_shared_state(indoc! {r"
4130 456 5ˇ67 678
4131 "})
4132 .await;
4133 cx.simulate_shared_keystrokes("g e").await;
4134 cx.shared_state().await.assert_eq(indoc! {"
4135 45ˇ6 567 678
4136 "});
4137
4138 // Test times
4139 cx.set_shared_state(indoc! {r"
4140 123 234 345
4141 456 5ˇ67 678
4142 "})
4143 .await;
4144 cx.simulate_shared_keystrokes("4 g e").await;
4145 cx.shared_state().await.assert_eq(indoc! {"
4146 12ˇ3 234 345
4147 456 567 678
4148 "});
4149
4150 // With punctuation
4151 cx.set_shared_state(indoc! {r"
4152 123 234 345
4153 4;5.6 5ˇ67 678
4154 789 890 901
4155 "})
4156 .await;
4157 cx.simulate_shared_keystrokes("g e").await;
4158 cx.shared_state().await.assert_eq(indoc! {"
4159 123 234 345
4160 4;5.ˇ6 567 678
4161 789 890 901
4162 "});
4163
4164 // With punctuation and count
4165 cx.set_shared_state(indoc! {r"
4166 123 234 345
4167 4;5.6 5ˇ67 678
4168 789 890 901
4169 "})
4170 .await;
4171 cx.simulate_shared_keystrokes("5 g e").await;
4172 cx.shared_state().await.assert_eq(indoc! {"
4173 123 234 345
4174 ˇ4;5.6 567 678
4175 789 890 901
4176 "});
4177
4178 // newlines
4179 cx.set_shared_state(indoc! {r"
4180 123 234 345
4181
4182 78ˇ9 890 901
4183 "})
4184 .await;
4185 cx.simulate_shared_keystrokes("g e").await;
4186 cx.shared_state().await.assert_eq(indoc! {"
4187 123 234 345
4188 ˇ
4189 789 890 901
4190 "});
4191 cx.simulate_shared_keystrokes("g e").await;
4192 cx.shared_state().await.assert_eq(indoc! {"
4193 123 234 34ˇ5
4194
4195 789 890 901
4196 "});
4197
4198 // With punctuation
4199 cx.set_shared_state(indoc! {r"
4200 123 234 345
4201 4;5.ˇ6 567 678
4202 789 890 901
4203 "})
4204 .await;
4205 cx.simulate_shared_keystrokes("g shift-e").await;
4206 cx.shared_state().await.assert_eq(indoc! {"
4207 123 234 34ˇ5
4208 4;5.6 567 678
4209 789 890 901
4210 "});
4211
4212 // With multi byte char
4213 cx.set_shared_state(indoc! {r"
4214 bar ˇó
4215 "})
4216 .await;
4217 cx.simulate_shared_keystrokes("g e").await;
4218 cx.shared_state().await.assert_eq(indoc! {"
4219 baˇr ó
4220 "});
4221 }
4222
4223 #[gpui::test]
4224 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
4225 let mut cx = NeovimBackedTestContext::new(cx).await;
4226
4227 cx.set_shared_state(indoc! {"
4228 fn aˇ() {
4229 return
4230 }
4231 "})
4232 .await;
4233 cx.simulate_shared_keystrokes("v $ %").await;
4234 cx.shared_state().await.assert_eq(indoc! {"
4235 fn a«() {
4236 return
4237 }ˇ»
4238 "});
4239 }
4240
4241 #[gpui::test]
4242 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
4243 let mut cx = VimTestContext::new(cx, true).await;
4244
4245 cx.set_state(
4246 indoc! {"
4247 struct Foo {
4248 ˇ
4249 }
4250 "},
4251 Mode::Normal,
4252 );
4253
4254 cx.update_editor(|editor, _window, cx| {
4255 let range = editor.selections.newest_anchor().range();
4256 let inlay_text = " field: int,\n field2: string\n field3: float";
4257 let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
4258 editor.splice_inlays(&[], vec![inlay], cx);
4259 });
4260
4261 cx.simulate_keystrokes("j");
4262 cx.assert_state(
4263 indoc! {"
4264 struct Foo {
4265
4266 ˇ}
4267 "},
4268 Mode::Normal,
4269 );
4270 }
4271
4272 #[gpui::test]
4273 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
4274 let mut cx = VimTestContext::new(cx, true).await;
4275
4276 cx.set_state(
4277 indoc! {"
4278 ˇstruct Foo {
4279
4280 }
4281 "},
4282 Mode::Normal,
4283 );
4284 cx.update_editor(|editor, _window, cx| {
4285 let snapshot = editor.buffer().read(cx).snapshot(cx);
4286 let end_of_line =
4287 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
4288 let inlay_text = " hint";
4289 let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
4290 editor.splice_inlays(&[], vec![inlay], cx);
4291 });
4292 cx.simulate_keystrokes("$");
4293 cx.assert_state(
4294 indoc! {"
4295 struct Foo ˇ{
4296
4297 }
4298 "},
4299 Mode::Normal,
4300 );
4301 }
4302
4303 #[gpui::test]
4304 async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
4305 let mut cx = VimTestContext::new(cx, true).await;
4306
4307 // Test the exact scenario from issue #29134
4308 cx.set_state(
4309 indoc! {"
4310 fn main() {
4311 let this_is_a_long_name = Vec::<u32>::new();
4312 let new_oneˇ = this_is_a_long_name
4313 .iter()
4314 .map(|i| i + 1)
4315 .map(|i| i * 2)
4316 .collect::<Vec<_>>();
4317 }
4318 "},
4319 Mode::Normal,
4320 );
4321
4322 // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
4323 cx.update_editor(|editor, _window, cx| {
4324 let snapshot = editor.buffer().read(cx).snapshot(cx);
4325 // The empty line is at line 3 (0-indexed)
4326 let line_start = snapshot.anchor_after(Point::new(3, 0));
4327 let inlay_text = ": Vec<u32>";
4328 let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
4329 editor.splice_inlays(&[], vec![inlay], cx);
4330 });
4331
4332 // Enter visual mode
4333 cx.simulate_keystrokes("v");
4334 cx.assert_state(
4335 indoc! {"
4336 fn main() {
4337 let this_is_a_long_name = Vec::<u32>::new();
4338 let new_one« ˇ»= this_is_a_long_name
4339 .iter()
4340 .map(|i| i + 1)
4341 .map(|i| i * 2)
4342 .collect::<Vec<_>>();
4343 }
4344 "},
4345 Mode::Visual,
4346 );
4347
4348 // Move down - should go to the beginning of line 4, not skip to line 5
4349 cx.simulate_keystrokes("j");
4350 cx.assert_state(
4351 indoc! {"
4352 fn main() {
4353 let this_is_a_long_name = Vec::<u32>::new();
4354 let new_one« = this_is_a_long_name
4355 ˇ» .iter()
4356 .map(|i| i + 1)
4357 .map(|i| i * 2)
4358 .collect::<Vec<_>>();
4359 }
4360 "},
4361 Mode::Visual,
4362 );
4363
4364 // Test with multiple movements
4365 cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
4366
4367 // Add type hint on the empty line
4368 cx.update_editor(|editor, _window, cx| {
4369 let snapshot = editor.buffer().read(cx).snapshot(cx);
4370 let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
4371 let inlay_text = ": i32";
4372 let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
4373 editor.splice_inlays(&[], vec![inlay], cx);
4374 });
4375
4376 // Enter visual mode and move down twice
4377 cx.simulate_keystrokes("v j j");
4378 cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
4379 }
4380
4381 #[gpui::test]
4382 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
4383 let mut cx = NeovimBackedTestContext::new(cx).await;
4384 // Normal mode
4385 cx.set_shared_state(indoc! {"
4386 The ˇquick brown
4387 fox jumps over
4388 the lazy dog
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 .await;
4396 cx.simulate_shared_keystrokes("2 0 %").await;
4397 cx.shared_state().await.assert_eq(indoc! {"
4398 The quick brown
4399 fox ˇjumps over
4400 the lazy dog
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
4408 cx.simulate_shared_keystrokes("2 5 %").await;
4409 cx.shared_state().await.assert_eq(indoc! {"
4410 The quick brown
4411 fox jumps over
4412 the ˇlazy dog
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
4420 cx.simulate_shared_keystrokes("7 5 %").await;
4421 cx.shared_state().await.assert_eq(indoc! {"
4422 The quick brown
4423 fox jumps over
4424 the lazy dog
4425 The quick brown
4426 fox jumps over
4427 the lazy dog
4428 The ˇquick brown
4429 fox jumps over
4430 the lazy dog"});
4431
4432 // Visual mode
4433 cx.set_shared_state(indoc! {"
4434 The ˇquick brown
4435 fox jumps over
4436 the lazy dog
4437 The quick brown
4438 fox jumps over
4439 the lazy dog
4440 The quick brown
4441 fox jumps over
4442 the lazy dog"})
4443 .await;
4444 cx.simulate_shared_keystrokes("v 5 0 %").await;
4445 cx.shared_state().await.assert_eq(indoc! {"
4446 The «quick brown
4447 fox jumps over
4448 the lazy dog
4449 The quick brown
4450 fox jˇ»umps over
4451 the lazy dog
4452 The quick brown
4453 fox jumps over
4454 the lazy dog"});
4455
4456 cx.set_shared_state(indoc! {"
4457 The ˇquick brown
4458 fox jumps over
4459 the lazy dog
4460 The quick brown
4461 fox jumps over
4462 the lazy dog
4463 The quick brown
4464 fox jumps over
4465 the lazy dog"})
4466 .await;
4467 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4468 cx.shared_state().await.assert_eq(indoc! {"
4469 The «quick brown
4470 fox jumps over
4471 the lazy dog
4472 The quick brown
4473 fox jumps over
4474 the lazy dog
4475 The quick brown
4476 fox jumps over
4477 the lˇ»azy dog"});
4478 }
4479
4480 #[gpui::test]
4481 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4482 let mut cx = NeovimBackedTestContext::new(cx).await;
4483
4484 cx.set_shared_state("ˇπππππ").await;
4485 cx.simulate_shared_keystrokes("3 space").await;
4486 cx.shared_state().await.assert_eq("πππˇππ");
4487 }
4488
4489 #[gpui::test]
4490 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4491 let mut cx = NeovimBackedTestContext::new(cx).await;
4492
4493 cx.set_shared_state(indoc! {"
4494 ππππˇπ
4495 πanotherline"})
4496 .await;
4497 cx.simulate_shared_keystrokes("4 space").await;
4498 cx.shared_state().await.assert_eq(indoc! {"
4499 πππππ
4500 πanˇotherline"});
4501 }
4502
4503 #[gpui::test]
4504 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4505 let mut cx = NeovimBackedTestContext::new(cx).await;
4506
4507 cx.set_shared_state(indoc! {"
4508 ππππ
4509 πanˇotherline"})
4510 .await;
4511 cx.simulate_shared_keystrokes("4 backspace").await;
4512 cx.shared_state().await.assert_eq(indoc! {"
4513 πππˇπ
4514 πanotherline"});
4515 }
4516
4517 #[gpui::test]
4518 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4519 let mut cx = VimTestContext::new(cx, true).await;
4520 cx.set_state(
4521 indoc! {
4522 "func empty(a string) bool {
4523 ˇif a == \"\" {
4524 return true
4525 }
4526 return false
4527 }"
4528 },
4529 Mode::Normal,
4530 );
4531 cx.simulate_keystrokes("[ -");
4532 cx.assert_state(
4533 indoc! {
4534 "ˇfunc empty(a string) bool {
4535 if a == \"\" {
4536 return true
4537 }
4538 return false
4539 }"
4540 },
4541 Mode::Normal,
4542 );
4543 cx.simulate_keystrokes("] =");
4544 cx.assert_state(
4545 indoc! {
4546 "func empty(a string) bool {
4547 if a == \"\" {
4548 return true
4549 }
4550 return false
4551 ˇ}"
4552 },
4553 Mode::Normal,
4554 );
4555 cx.simulate_keystrokes("[ +");
4556 cx.assert_state(
4557 indoc! {
4558 "func empty(a string) bool {
4559 if a == \"\" {
4560 return true
4561 }
4562 ˇreturn false
4563 }"
4564 },
4565 Mode::Normal,
4566 );
4567 cx.simulate_keystrokes("2 [ =");
4568 cx.assert_state(
4569 indoc! {
4570 "func empty(a string) bool {
4571 ˇif a == \"\" {
4572 return true
4573 }
4574 return false
4575 }"
4576 },
4577 Mode::Normal,
4578 );
4579 cx.simulate_keystrokes("] +");
4580 cx.assert_state(
4581 indoc! {
4582 "func empty(a string) bool {
4583 if a == \"\" {
4584 ˇreturn true
4585 }
4586 return false
4587 }"
4588 },
4589 Mode::Normal,
4590 );
4591 cx.simulate_keystrokes("] -");
4592 cx.assert_state(
4593 indoc! {
4594 "func empty(a string) bool {
4595 if a == \"\" {
4596 return true
4597 ˇ}
4598 return false
4599 }"
4600 },
4601 Mode::Normal,
4602 );
4603 }
4604
4605 #[gpui::test]
4606 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4607 let mut cx = NeovimBackedTestContext::new(cx).await;
4608 cx.set_shared_state("abˇc").await;
4609 cx.simulate_shared_keystrokes("delete").await;
4610 cx.shared_state().await.assert_eq("aˇb");
4611 }
4612
4613 #[gpui::test]
4614 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4615 let mut cx = NeovimBackedTestContext::new(cx).await;
4616
4617 cx.set_shared_state(indoc! {"
4618 ˇthe quick brown fox
4619 jumped over the lazy dog"})
4620 .await;
4621 cx.simulate_shared_keystrokes("d v 0").await;
4622 cx.shared_state().await.assert_eq(indoc! {"
4623 ˇhe quick brown fox
4624 jumped over the lazy dog"});
4625 assert!(!cx.cx.forced_motion());
4626
4627 cx.set_shared_state(indoc! {"
4628 the quick bˇrown fox
4629 jumped over the lazy dog"})
4630 .await;
4631 cx.simulate_shared_keystrokes("d v 0").await;
4632 cx.shared_state().await.assert_eq(indoc! {"
4633 ˇown fox
4634 jumped over the lazy dog"});
4635 assert!(!cx.cx.forced_motion());
4636
4637 cx.set_shared_state(indoc! {"
4638 the quick brown foˇx
4639 jumped over the lazy dog"})
4640 .await;
4641 cx.simulate_shared_keystrokes("d v 0").await;
4642 cx.shared_state().await.assert_eq(indoc! {"
4643 ˇ
4644 jumped over the lazy dog"});
4645 assert!(!cx.cx.forced_motion());
4646 }
4647
4648 #[gpui::test]
4649 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4650 let mut cx = NeovimBackedTestContext::new(cx).await;
4651
4652 cx.set_shared_state(indoc! {"
4653 ˇthe quick brown fox
4654 jumped over the lazy dog"})
4655 .await;
4656 cx.simulate_shared_keystrokes("d v g shift-m").await;
4657 cx.shared_state().await.assert_eq(indoc! {"
4658 ˇbrown fox
4659 jumped over the lazy dog"});
4660 assert!(!cx.cx.forced_motion());
4661
4662 cx.set_shared_state(indoc! {"
4663 the quick bˇrown fox
4664 jumped over the lazy dog"})
4665 .await;
4666 cx.simulate_shared_keystrokes("d v g shift-m").await;
4667 cx.shared_state().await.assert_eq(indoc! {"
4668 the quickˇown fox
4669 jumped over the lazy dog"});
4670 assert!(!cx.cx.forced_motion());
4671
4672 cx.set_shared_state(indoc! {"
4673 the quick brown foˇx
4674 jumped over the lazy dog"})
4675 .await;
4676 cx.simulate_shared_keystrokes("d v g shift-m").await;
4677 cx.shared_state().await.assert_eq(indoc! {"
4678 the quicˇk
4679 jumped over the lazy dog"});
4680 assert!(!cx.cx.forced_motion());
4681
4682 cx.set_shared_state(indoc! {"
4683 ˇthe quick brown fox
4684 jumped over the lazy dog"})
4685 .await;
4686 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4687 cx.shared_state().await.assert_eq(indoc! {"
4688 ˇ fox
4689 jumped over the lazy dog"});
4690 assert!(!cx.cx.forced_motion());
4691
4692 cx.set_shared_state(indoc! {"
4693 ˇthe quick brown fox
4694 jumped over the lazy dog"})
4695 .await;
4696 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4697 cx.shared_state().await.assert_eq(indoc! {"
4698 ˇuick brown fox
4699 jumped over the lazy dog"});
4700 assert!(!cx.cx.forced_motion());
4701 }
4702
4703 #[gpui::test]
4704 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4705 let mut cx = NeovimBackedTestContext::new(cx).await;
4706
4707 cx.set_shared_state(indoc! {"
4708 the quick brown foˇx
4709 jumped over the lazy dog"})
4710 .await;
4711 cx.simulate_shared_keystrokes("d v $").await;
4712 cx.shared_state().await.assert_eq(indoc! {"
4713 the quick brown foˇx
4714 jumped over the lazy dog"});
4715 assert!(!cx.cx.forced_motion());
4716
4717 cx.set_shared_state(indoc! {"
4718 ˇthe quick brown fox
4719 jumped over the lazy dog"})
4720 .await;
4721 cx.simulate_shared_keystrokes("d v $").await;
4722 cx.shared_state().await.assert_eq(indoc! {"
4723 ˇx
4724 jumped over the lazy dog"});
4725 assert!(!cx.cx.forced_motion());
4726 }
4727
4728 #[gpui::test]
4729 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4730 let mut cx = NeovimBackedTestContext::new(cx).await;
4731
4732 cx.set_shared_state(indoc! {"
4733 ˇthe quick brown fox
4734 jumped over the lazy dog"})
4735 .await;
4736 cx.simulate_shared_keystrokes("y v j p").await;
4737 cx.shared_state().await.assert_eq(indoc! {"
4738 the quick brown fox
4739 ˇthe quick brown fox
4740 jumped over the lazy dog"});
4741 assert!(!cx.cx.forced_motion());
4742
4743 cx.set_shared_state(indoc! {"
4744 the quick bˇrown fox
4745 jumped over the lazy dog"})
4746 .await;
4747 cx.simulate_shared_keystrokes("y v j p").await;
4748 cx.shared_state().await.assert_eq(indoc! {"
4749 the quick brˇrown fox
4750 jumped overown fox
4751 jumped over the lazy dog"});
4752 assert!(!cx.cx.forced_motion());
4753
4754 cx.set_shared_state(indoc! {"
4755 the quick brown foˇx
4756 jumped over the lazy dog"})
4757 .await;
4758 cx.simulate_shared_keystrokes("y v j p").await;
4759 cx.shared_state().await.assert_eq(indoc! {"
4760 the quick brown foxˇx
4761 jumped over the la
4762 jumped over the lazy dog"});
4763 assert!(!cx.cx.forced_motion());
4764
4765 cx.set_shared_state(indoc! {"
4766 the quick brown fox
4767 jˇumped over the lazy dog"})
4768 .await;
4769 cx.simulate_shared_keystrokes("y v k p").await;
4770 cx.shared_state().await.assert_eq(indoc! {"
4771 thˇhe quick brown fox
4772 je quick brown fox
4773 jumped over the lazy dog"});
4774 assert!(!cx.cx.forced_motion());
4775 }
4776
4777 #[gpui::test]
4778 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4779 let mut cx = NeovimBackedTestContext::new(cx).await;
4780
4781 cx.set_shared_state(indoc! {"
4782 ˇthe quick brown fox
4783 jumped over the lazy dog"})
4784 .await;
4785 cx.simulate_shared_keystrokes("d v e").await;
4786 cx.shared_state().await.assert_eq(indoc! {"
4787 ˇe quick brown fox
4788 jumped over the lazy dog"});
4789 assert!(!cx.cx.forced_motion());
4790
4791 cx.set_shared_state(indoc! {"
4792 the quick bˇrown fox
4793 jumped over the lazy dog"})
4794 .await;
4795 cx.simulate_shared_keystrokes("d v e").await;
4796 cx.shared_state().await.assert_eq(indoc! {"
4797 the quick bˇn fox
4798 jumped over the lazy dog"});
4799 assert!(!cx.cx.forced_motion());
4800
4801 cx.set_shared_state(indoc! {"
4802 the quick brown foˇx
4803 jumped over the lazy dog"})
4804 .await;
4805 cx.simulate_shared_keystrokes("d v e").await;
4806 cx.shared_state().await.assert_eq(indoc! {"
4807 the quick brown foˇd over the lazy dog"});
4808 assert!(!cx.cx.forced_motion());
4809 }
4810
4811 #[gpui::test]
4812 async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
4813 let mut cx = VimTestContext::new(cx, true).await;
4814
4815 // Setup custom keybindings for subword motions so we can use the bindings
4816 // in `simulate_keystrokes`.
4817 cx.update(|_window, cx| {
4818 cx.bind_keys([KeyBinding::new(
4819 "w",
4820 super::NextSubwordStart {
4821 ignore_punctuation: false,
4822 },
4823 None,
4824 )]);
4825 });
4826
4827 cx.set_state("ˇfoo.bar", Mode::Normal);
4828 cx.simulate_keystrokes("w");
4829 cx.assert_state("foo.ˇbar", Mode::Normal);
4830
4831 cx.set_state("ˇfoo(bar)", Mode::Normal);
4832 cx.simulate_keystrokes("w");
4833 cx.assert_state("fooˇ(bar)", Mode::Normal);
4834 cx.simulate_keystrokes("w");
4835 cx.assert_state("foo(ˇbar)", Mode::Normal);
4836 cx.simulate_keystrokes("w");
4837 cx.assert_state("foo(barˇ)", Mode::Normal);
4838
4839 cx.set_state("ˇfoo_bar_baz", Mode::Normal);
4840 cx.simulate_keystrokes("w");
4841 cx.assert_state("foo_ˇbar_baz", Mode::Normal);
4842 cx.simulate_keystrokes("w");
4843 cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
4844
4845 cx.set_state("ˇfooBarBaz", Mode::Normal);
4846 cx.simulate_keystrokes("w");
4847 cx.assert_state("fooˇBarBaz", Mode::Normal);
4848 cx.simulate_keystrokes("w");
4849 cx.assert_state("fooBarˇBaz", Mode::Normal);
4850
4851 cx.set_state("ˇfoo;bar", Mode::Normal);
4852 cx.simulate_keystrokes("w");
4853 cx.assert_state("foo;ˇbar", Mode::Normal);
4854 }
4855
4856 #[gpui::test]
4857 async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
4858 let mut cx = VimTestContext::new(cx, true).await;
4859
4860 // Setup custom keybindings for subword motions so we can use the bindings
4861 // in `simulate_keystrokes`.
4862 cx.update(|_window, cx| {
4863 cx.bind_keys([KeyBinding::new(
4864 "e",
4865 super::NextSubwordEnd {
4866 ignore_punctuation: false,
4867 },
4868 None,
4869 )]);
4870 });
4871
4872 cx.set_state("ˇfoo.bar", Mode::Normal);
4873 cx.simulate_keystrokes("e");
4874 cx.assert_state("foˇo.bar", Mode::Normal);
4875 cx.simulate_keystrokes("e");
4876 cx.assert_state("fooˇ.bar", Mode::Normal);
4877 cx.simulate_keystrokes("e");
4878 cx.assert_state("foo.baˇr", Mode::Normal);
4879
4880 cx.set_state("ˇfoo(bar)", Mode::Normal);
4881 cx.simulate_keystrokes("e");
4882 cx.assert_state("foˇo(bar)", Mode::Normal);
4883 cx.simulate_keystrokes("e");
4884 cx.assert_state("fooˇ(bar)", Mode::Normal);
4885 cx.simulate_keystrokes("e");
4886 cx.assert_state("foo(baˇr)", Mode::Normal);
4887 cx.simulate_keystrokes("e");
4888 cx.assert_state("foo(barˇ)", Mode::Normal);
4889
4890 cx.set_state("ˇfoo_bar_baz", Mode::Normal);
4891 cx.simulate_keystrokes("e");
4892 cx.assert_state("foˇo_bar_baz", Mode::Normal);
4893 cx.simulate_keystrokes("e");
4894 cx.assert_state("foo_baˇr_baz", Mode::Normal);
4895 cx.simulate_keystrokes("e");
4896 cx.assert_state("foo_bar_baˇz", Mode::Normal);
4897
4898 cx.set_state("ˇfooBarBaz", Mode::Normal);
4899 cx.simulate_keystrokes("e");
4900 cx.set_state("foˇoBarBaz", Mode::Normal);
4901 cx.simulate_keystrokes("e");
4902 cx.set_state("fooBaˇrBaz", Mode::Normal);
4903 cx.simulate_keystrokes("e");
4904 cx.set_state("fooBarBaˇz", Mode::Normal);
4905
4906 cx.set_state("ˇfoo;bar", Mode::Normal);
4907 cx.simulate_keystrokes("e");
4908 cx.set_state("foˇo;bar", Mode::Normal);
4909 cx.simulate_keystrokes("e");
4910 cx.set_state("fooˇ;bar", Mode::Normal);
4911 cx.simulate_keystrokes("e");
4912 cx.set_state("foo;baˇr", Mode::Normal);
4913 }
4914
4915 #[gpui::test]
4916 async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
4917 let mut cx = VimTestContext::new(cx, true).await;
4918
4919 // Setup custom keybindings for subword motions so we can use the bindings
4920 // in `simulate_keystrokes`.
4921 cx.update(|_window, cx| {
4922 cx.bind_keys([KeyBinding::new(
4923 "b",
4924 super::PreviousSubwordStart {
4925 ignore_punctuation: false,
4926 },
4927 None,
4928 )]);
4929 });
4930
4931 cx.set_state("foo.barˇ", Mode::Normal);
4932 cx.simulate_keystrokes("b");
4933 cx.assert_state("foo.ˇbar", Mode::Normal);
4934 cx.simulate_keystrokes("b");
4935 cx.assert_state("fooˇ.bar", Mode::Normal);
4936 cx.simulate_keystrokes("b");
4937 cx.assert_state("ˇfoo.bar", Mode::Normal);
4938
4939 cx.set_state("foo(barˇ)", Mode::Normal);
4940 cx.simulate_keystrokes("b");
4941 cx.assert_state("foo(ˇbar)", Mode::Normal);
4942 cx.simulate_keystrokes("b");
4943 cx.assert_state("fooˇ(bar)", Mode::Normal);
4944 cx.simulate_keystrokes("b");
4945 cx.assert_state("ˇfoo(bar)", Mode::Normal);
4946
4947 cx.set_state("foo_bar_bazˇ", Mode::Normal);
4948 cx.simulate_keystrokes("b");
4949 cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
4950 cx.simulate_keystrokes("b");
4951 cx.assert_state("foo_ˇbar_baz", Mode::Normal);
4952 cx.simulate_keystrokes("b");
4953 cx.assert_state("ˇfoo_bar_baz", Mode::Normal);
4954
4955 cx.set_state("fooBarBazˇ", Mode::Normal);
4956 cx.simulate_keystrokes("b");
4957 cx.assert_state("fooBarˇBaz", Mode::Normal);
4958 cx.simulate_keystrokes("b");
4959 cx.assert_state("fooˇBarBaz", Mode::Normal);
4960 cx.simulate_keystrokes("b");
4961 cx.assert_state("ˇfooBarBaz", Mode::Normal);
4962
4963 cx.set_state("foo;barˇ", Mode::Normal);
4964 cx.simulate_keystrokes("b");
4965 cx.assert_state("foo;ˇbar", Mode::Normal);
4966 cx.simulate_keystrokes("b");
4967 cx.assert_state("ˇfoo;bar", Mode::Normal);
4968 }
4969
4970 #[gpui::test]
4971 async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
4972 let mut cx = VimTestContext::new(cx, true).await;
4973
4974 // Setup custom keybindings for subword motions so we can use the bindings
4975 // in `simulate_keystrokes`.
4976 cx.update(|_window, cx| {
4977 cx.bind_keys([KeyBinding::new(
4978 "g e",
4979 super::PreviousSubwordEnd {
4980 ignore_punctuation: false,
4981 },
4982 None,
4983 )]);
4984 });
4985
4986 cx.set_state("foo.baˇr", Mode::Normal);
4987 cx.simulate_keystrokes("g e");
4988 cx.assert_state("fooˇ.bar", Mode::Normal);
4989 cx.simulate_keystrokes("g e");
4990 cx.assert_state("foˇo.bar", Mode::Normal);
4991
4992 cx.set_state("foo(barˇ)", Mode::Normal);
4993 cx.simulate_keystrokes("g e");
4994 cx.assert_state("foo(baˇr)", Mode::Normal);
4995 cx.simulate_keystrokes("g e");
4996 cx.assert_state("fooˇ(bar)", Mode::Normal);
4997 cx.simulate_keystrokes("g e");
4998 cx.assert_state("foˇo(bar)", Mode::Normal);
4999
5000 cx.set_state("foo_bar_baˇz", Mode::Normal);
5001 cx.simulate_keystrokes("g e");
5002 cx.assert_state("foo_baˇr_baz", Mode::Normal);
5003 cx.simulate_keystrokes("g e");
5004 cx.assert_state("foˇo_bar_baz", Mode::Normal);
5005
5006 cx.set_state("fooBarBaˇz", Mode::Normal);
5007 cx.simulate_keystrokes("g e");
5008 cx.assert_state("fooBaˇrBaz", Mode::Normal);
5009 cx.simulate_keystrokes("g e");
5010 cx.assert_state("foˇoBarBaz", Mode::Normal);
5011
5012 cx.set_state("foo;baˇr", Mode::Normal);
5013 cx.simulate_keystrokes("g e");
5014 cx.assert_state("fooˇ;bar", Mode::Normal);
5015 cx.simulate_keystrokes("g e");
5016 cx.assert_state("foˇo;bar", Mode::Normal);
5017 }
5018}