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