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