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