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