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