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