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