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