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