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