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