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