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