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 let snapshot = map.buffer_snapshot();
2392
2393 // Ensure the range is contained by the current line.
2394 let mut line_end = map.next_line_boundary(point).0;
2395 if line_end == point {
2396 line_end = map.max_point().to_point(map);
2397 }
2398
2399 // Attempt to find the smallest enclosing bracket range that also contains
2400 // the offset, which only happens if the cursor is currently in a bracket.
2401 let range_filter = |_buffer: &language::BufferSnapshot,
2402 opening_range: Range<usize>,
2403 closing_range: Range<usize>| {
2404 opening_range.contains(&offset) || closing_range.contains(&offset)
2405 };
2406
2407 let bracket_ranges = snapshot
2408 .innermost_enclosing_bracket_ranges(offset..offset, Some(&range_filter))
2409 .or_else(|| snapshot.innermost_enclosing_bracket_ranges(offset..offset, None));
2410
2411 if let Some((opening_range, closing_range)) = bracket_ranges {
2412 if opening_range.contains(&offset) {
2413 return closing_range.start.to_display_point(map);
2414 } else if closing_range.contains(&offset) {
2415 return opening_range.start.to_display_point(map);
2416 }
2417 }
2418
2419 let line_range = map.prev_line_boundary(point).0..line_end;
2420 let visible_line_range =
2421 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2422 let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range);
2423 if let Some(ranges) = ranges {
2424 let line_range = line_range.start.to_offset(&map.buffer_snapshot())
2425 ..line_range.end.to_offset(&map.buffer_snapshot());
2426 let mut closest_pair_destination = None;
2427 let mut closest_distance = usize::MAX;
2428
2429 for (open_range, close_range) in ranges {
2430 if map.buffer_snapshot().chars_at(open_range.start).next() == Some('<') {
2431 if offset > open_range.start && offset < close_range.start {
2432 let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2433 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2434 return display_point;
2435 }
2436 if let Some(tag) = matching_tag(map, display_point) {
2437 return tag;
2438 }
2439 } else if close_range.contains(&offset) {
2440 return open_range.start.to_display_point(map);
2441 } else if open_range.contains(&offset) {
2442 return (close_range.end - 1).to_display_point(map);
2443 }
2444 }
2445
2446 if (open_range.contains(&offset) || open_range.start >= offset)
2447 && line_range.contains(&open_range.start)
2448 {
2449 let distance = open_range.start.saturating_sub(offset);
2450 if distance < closest_distance {
2451 closest_pair_destination = Some(close_range.start);
2452 closest_distance = distance;
2453 }
2454 }
2455
2456 if (close_range.contains(&offset) || close_range.start >= offset)
2457 && line_range.contains(&close_range.start)
2458 {
2459 let distance = close_range.start.saturating_sub(offset);
2460 if distance < closest_distance {
2461 closest_pair_destination = Some(open_range.start);
2462 closest_distance = distance;
2463 }
2464 }
2465
2466 continue;
2467 }
2468
2469 closest_pair_destination
2470 .map(|destination| destination.to_display_point(map))
2471 .unwrap_or(display_point)
2472 } else {
2473 display_point
2474 }
2475}
2476
2477// Go to {count} percentage in the file, on the first
2478// non-blank in the line linewise. To compute the new
2479// line number this formula is used:
2480// ({count} * number-of-lines + 99) / 100
2481//
2482// https://neovim.io/doc/user/motion.html#N%25
2483fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2484 let total_lines = map.buffer_snapshot().max_point().row + 1;
2485 let target_line = (count * total_lines as usize).div_ceil(100);
2486 let target_point = DisplayPoint::new(
2487 DisplayRow(target_line.saturating_sub(1) as u32),
2488 point.column(),
2489 );
2490 map.clip_point(target_point, Bias::Left)
2491}
2492
2493fn unmatched_forward(
2494 map: &DisplaySnapshot,
2495 mut display_point: DisplayPoint,
2496 char: char,
2497 times: usize,
2498) -> DisplayPoint {
2499 for _ in 0..times {
2500 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2501 let point = display_point.to_point(map);
2502 let offset = point.to_offset(&map.buffer_snapshot());
2503
2504 let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2505 let Some(ranges) = ranges else { break };
2506 let mut closest_closing_destination = None;
2507 let mut closest_distance = usize::MAX;
2508
2509 for (_, close_range) in ranges {
2510 if close_range.start > offset {
2511 let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2512 if Some(char) == chars.next() {
2513 let distance = close_range.start - offset;
2514 if distance < closest_distance {
2515 closest_closing_destination = Some(close_range.start);
2516 closest_distance = distance;
2517 continue;
2518 }
2519 }
2520 }
2521 }
2522
2523 let new_point = closest_closing_destination
2524 .map(|destination| destination.to_display_point(map))
2525 .unwrap_or(display_point);
2526 if new_point == display_point {
2527 break;
2528 }
2529 display_point = new_point;
2530 }
2531 display_point
2532}
2533
2534fn unmatched_backward(
2535 map: &DisplaySnapshot,
2536 mut display_point: DisplayPoint,
2537 char: char,
2538 times: usize,
2539) -> DisplayPoint {
2540 for _ in 0..times {
2541 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2542 let point = display_point.to_point(map);
2543 let offset = point.to_offset(&map.buffer_snapshot());
2544
2545 let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2546 let Some(ranges) = ranges else {
2547 break;
2548 };
2549
2550 let mut closest_starting_destination = None;
2551 let mut closest_distance = usize::MAX;
2552
2553 for (start_range, _) in ranges {
2554 if start_range.start < offset {
2555 let mut chars = map.buffer_snapshot().chars_at(start_range.start);
2556 if Some(char) == chars.next() {
2557 let distance = offset - start_range.start;
2558 if distance < closest_distance {
2559 closest_starting_destination = Some(start_range.start);
2560 closest_distance = distance;
2561 continue;
2562 }
2563 }
2564 }
2565 }
2566
2567 let new_point = closest_starting_destination
2568 .map(|destination| destination.to_display_point(map))
2569 .unwrap_or(display_point);
2570 if new_point == display_point {
2571 break;
2572 } else {
2573 display_point = new_point;
2574 }
2575 }
2576 display_point
2577}
2578
2579fn find_forward(
2580 map: &DisplaySnapshot,
2581 from: DisplayPoint,
2582 before: bool,
2583 target: char,
2584 times: usize,
2585 mode: FindRange,
2586 smartcase: bool,
2587) -> Option<DisplayPoint> {
2588 let mut to = from;
2589 let mut found = false;
2590
2591 for _ in 0..times {
2592 found = false;
2593 let new_to = find_boundary(map, to, mode, |_, right| {
2594 found = is_character_match(target, right, smartcase);
2595 found
2596 });
2597 if to == new_to {
2598 break;
2599 }
2600 to = new_to;
2601 }
2602
2603 if found {
2604 if before && to.column() > 0 {
2605 *to.column_mut() -= 1;
2606 Some(map.clip_point(to, Bias::Left))
2607 } else if before && to.row().0 > 0 {
2608 *to.row_mut() -= 1;
2609 *to.column_mut() = map.line(to.row()).len() as u32;
2610 Some(map.clip_point(to, Bias::Left))
2611 } else {
2612 Some(to)
2613 }
2614 } else {
2615 None
2616 }
2617}
2618
2619fn find_backward(
2620 map: &DisplaySnapshot,
2621 from: DisplayPoint,
2622 after: bool,
2623 target: char,
2624 times: usize,
2625 mode: FindRange,
2626 smartcase: bool,
2627) -> DisplayPoint {
2628 let mut to = from;
2629
2630 for _ in 0..times {
2631 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2632 is_character_match(target, right, smartcase)
2633 });
2634 if to == new_to {
2635 break;
2636 }
2637 to = new_to;
2638 }
2639
2640 let next = map.buffer_snapshot().chars_at(to.to_point(map)).next();
2641 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2642 if after {
2643 *to.column_mut() += 1;
2644 map.clip_point(to, Bias::Right)
2645 } else {
2646 to
2647 }
2648 } else {
2649 from
2650 }
2651}
2652
2653/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2654pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2655 if smartcase {
2656 if target.is_uppercase() {
2657 target == other
2658 } else {
2659 target == other.to_ascii_lowercase()
2660 }
2661 } else {
2662 target == other
2663 }
2664}
2665
2666fn sneak(
2667 map: &DisplaySnapshot,
2668 from: DisplayPoint,
2669 first_target: char,
2670 second_target: char,
2671 times: usize,
2672 smartcase: bool,
2673) -> Option<DisplayPoint> {
2674 let mut to = from;
2675 let mut found = false;
2676
2677 for _ in 0..times {
2678 found = false;
2679 let new_to = find_boundary(
2680 map,
2681 movement::right(map, to),
2682 FindRange::MultiLine,
2683 |left, right| {
2684 found = is_character_match(first_target, left, smartcase)
2685 && is_character_match(second_target, right, smartcase);
2686 found
2687 },
2688 );
2689 if to == new_to {
2690 break;
2691 }
2692 to = new_to;
2693 }
2694
2695 if found {
2696 Some(movement::left(map, to))
2697 } else {
2698 None
2699 }
2700}
2701
2702fn sneak_backward(
2703 map: &DisplaySnapshot,
2704 from: DisplayPoint,
2705 first_target: char,
2706 second_target: char,
2707 times: usize,
2708 smartcase: bool,
2709) -> Option<DisplayPoint> {
2710 let mut to = from;
2711 let mut found = false;
2712
2713 for _ in 0..times {
2714 found = false;
2715 let new_to =
2716 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2717 found = is_character_match(first_target, left, smartcase)
2718 && is_character_match(second_target, right, smartcase);
2719 found
2720 });
2721 if to == new_to {
2722 break;
2723 }
2724 to = new_to;
2725 }
2726
2727 if found {
2728 Some(movement::left(map, to))
2729 } else {
2730 None
2731 }
2732}
2733
2734fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2735 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2736 first_non_whitespace(map, false, correct_line)
2737}
2738
2739fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2740 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2741 first_non_whitespace(map, false, correct_line)
2742}
2743
2744fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2745 let correct_line = start_of_relative_buffer_row(map, point, 0);
2746 right(map, correct_line, times.saturating_sub(1))
2747}
2748
2749pub(crate) fn next_line_end(
2750 map: &DisplaySnapshot,
2751 mut point: DisplayPoint,
2752 times: usize,
2753) -> DisplayPoint {
2754 if times > 1 {
2755 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2756 }
2757 end_of_line(map, false, point, 1)
2758}
2759
2760fn window_top(
2761 map: &DisplaySnapshot,
2762 point: DisplayPoint,
2763 text_layout_details: &TextLayoutDetails,
2764 mut times: usize,
2765) -> (DisplayPoint, SelectionGoal) {
2766 let first_visible_line = text_layout_details
2767 .scroll_anchor
2768 .anchor
2769 .to_display_point(map);
2770
2771 if first_visible_line.row() != DisplayRow(0)
2772 && text_layout_details.vertical_scroll_margin as usize > times
2773 {
2774 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2775 }
2776
2777 if let Some(visible_rows) = text_layout_details.visible_rows {
2778 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2779 let new_row = (first_visible_line.row().0 + (times as u32))
2780 .min(bottom_row)
2781 .min(map.max_point().row().0);
2782 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2783
2784 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2785 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2786 } else {
2787 let new_row =
2788 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2789 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2790
2791 let new_point = DisplayPoint::new(new_row, new_col);
2792 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2793 }
2794}
2795
2796fn window_middle(
2797 map: &DisplaySnapshot,
2798 point: DisplayPoint,
2799 text_layout_details: &TextLayoutDetails,
2800) -> (DisplayPoint, SelectionGoal) {
2801 if let Some(visible_rows) = text_layout_details.visible_rows {
2802 let first_visible_line = text_layout_details
2803 .scroll_anchor
2804 .anchor
2805 .to_display_point(map);
2806
2807 let max_visible_rows =
2808 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2809
2810 let new_row =
2811 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2812 let new_row = DisplayRow(new_row);
2813 let new_col = point.column().min(map.line_len(new_row));
2814 let new_point = DisplayPoint::new(new_row, new_col);
2815 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2816 } else {
2817 (point, SelectionGoal::None)
2818 }
2819}
2820
2821fn window_bottom(
2822 map: &DisplaySnapshot,
2823 point: DisplayPoint,
2824 text_layout_details: &TextLayoutDetails,
2825 mut times: usize,
2826) -> (DisplayPoint, SelectionGoal) {
2827 if let Some(visible_rows) = text_layout_details.visible_rows {
2828 let first_visible_line = text_layout_details
2829 .scroll_anchor
2830 .anchor
2831 .to_display_point(map);
2832 let bottom_row = first_visible_line.row().0
2833 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2834 if bottom_row < map.max_point().row().0
2835 && text_layout_details.vertical_scroll_margin as usize > times
2836 {
2837 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2838 }
2839 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2840 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2841 {
2842 first_visible_line.row()
2843 } else {
2844 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2845 };
2846 let new_col = point.column().min(map.line_len(new_row));
2847 let new_point = DisplayPoint::new(new_row, new_col);
2848 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2849 } else {
2850 (point, SelectionGoal::None)
2851 }
2852}
2853
2854fn method_motion(
2855 map: &DisplaySnapshot,
2856 mut display_point: DisplayPoint,
2857 times: usize,
2858 direction: Direction,
2859 is_start: bool,
2860) -> DisplayPoint {
2861 let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else {
2862 return display_point;
2863 };
2864
2865 for _ in 0..times {
2866 let point = map.display_point_to_point(display_point, Bias::Left);
2867 let offset = point.to_offset(&map.buffer_snapshot());
2868 let range = if direction == Direction::Prev {
2869 0..offset
2870 } else {
2871 offset..buffer.len()
2872 };
2873
2874 let possibilities = buffer
2875 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2876 .filter_map(|(range, object)| {
2877 if !matches!(object, language::TextObject::AroundFunction) {
2878 return None;
2879 }
2880
2881 let relevant = if is_start { range.start } else { range.end };
2882 if direction == Direction::Prev && relevant < offset {
2883 Some(relevant)
2884 } else if direction == Direction::Next && relevant > offset + 1 {
2885 Some(relevant)
2886 } else {
2887 None
2888 }
2889 });
2890
2891 let dest = if direction == Direction::Prev {
2892 possibilities.max().unwrap_or(offset)
2893 } else {
2894 possibilities.min().unwrap_or(offset)
2895 };
2896 let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2897 if new_point == display_point {
2898 break;
2899 }
2900 display_point = new_point;
2901 }
2902 display_point
2903}
2904
2905fn comment_motion(
2906 map: &DisplaySnapshot,
2907 mut display_point: DisplayPoint,
2908 times: usize,
2909 direction: Direction,
2910) -> DisplayPoint {
2911 let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else {
2912 return display_point;
2913 };
2914
2915 for _ in 0..times {
2916 let point = map.display_point_to_point(display_point, Bias::Left);
2917 let offset = point.to_offset(&map.buffer_snapshot());
2918 let range = if direction == Direction::Prev {
2919 0..offset
2920 } else {
2921 offset..buffer.len()
2922 };
2923
2924 let possibilities = buffer
2925 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2926 .filter_map(|(range, object)| {
2927 if !matches!(object, language::TextObject::AroundComment) {
2928 return None;
2929 }
2930
2931 let relevant = if direction == Direction::Prev {
2932 range.start
2933 } else {
2934 range.end
2935 };
2936 if direction == Direction::Prev && relevant < offset {
2937 Some(relevant)
2938 } else if direction == Direction::Next && relevant > offset + 1 {
2939 Some(relevant)
2940 } else {
2941 None
2942 }
2943 });
2944
2945 let dest = if direction == Direction::Prev {
2946 possibilities.max().unwrap_or(offset)
2947 } else {
2948 possibilities.min().unwrap_or(offset)
2949 };
2950 let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2951 if new_point == display_point {
2952 break;
2953 }
2954 display_point = new_point;
2955 }
2956
2957 display_point
2958}
2959
2960fn section_motion(
2961 map: &DisplaySnapshot,
2962 mut display_point: DisplayPoint,
2963 times: usize,
2964 direction: Direction,
2965 is_start: bool,
2966) -> DisplayPoint {
2967 if map.buffer_snapshot().as_singleton().is_some() {
2968 for _ in 0..times {
2969 let offset = map
2970 .display_point_to_point(display_point, Bias::Left)
2971 .to_offset(&map.buffer_snapshot());
2972 let range = if direction == Direction::Prev {
2973 0..offset
2974 } else {
2975 offset..map.buffer_snapshot().len()
2976 };
2977
2978 // we set a max start depth here because we want a section to only be "top level"
2979 // similar to vim's default of '{' in the first column.
2980 // (and without it, ]] at the start of editor.rs is -very- slow)
2981 let mut possibilities = map
2982 .buffer_snapshot()
2983 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2984 .filter(|(_, object)| {
2985 matches!(
2986 object,
2987 language::TextObject::AroundClass | language::TextObject::AroundFunction
2988 )
2989 })
2990 .collect::<Vec<_>>();
2991 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2992 let mut prev_end = None;
2993 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2994 if t == language::TextObject::AroundFunction
2995 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2996 {
2997 return None;
2998 }
2999 prev_end = Some(range.end);
3000
3001 let relevant = if is_start { range.start } else { range.end };
3002 if direction == Direction::Prev && relevant < offset {
3003 Some(relevant)
3004 } else if direction == Direction::Next && relevant > offset + 1 {
3005 Some(relevant)
3006 } else {
3007 None
3008 }
3009 });
3010
3011 let offset = if direction == Direction::Prev {
3012 possibilities.max().unwrap_or(0)
3013 } else {
3014 possibilities.min().unwrap_or(map.buffer_snapshot().len())
3015 };
3016
3017 let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3018 if new_point == display_point {
3019 break;
3020 }
3021 display_point = new_point;
3022 }
3023 return display_point;
3024 };
3025
3026 for _ in 0..times {
3027 let next_point = if is_start {
3028 movement::start_of_excerpt(map, display_point, direction)
3029 } else {
3030 movement::end_of_excerpt(map, display_point, direction)
3031 };
3032 if next_point == display_point {
3033 break;
3034 }
3035 display_point = next_point;
3036 }
3037
3038 display_point
3039}
3040
3041fn matches_indent_type(
3042 target_indent: &text::LineIndent,
3043 current_indent: &text::LineIndent,
3044 indent_type: IndentType,
3045) -> bool {
3046 match indent_type {
3047 IndentType::Lesser => {
3048 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3049 }
3050 IndentType::Greater => {
3051 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3052 }
3053 IndentType::Same => {
3054 target_indent.spaces == current_indent.spaces
3055 && target_indent.tabs == current_indent.tabs
3056 }
3057 }
3058}
3059
3060fn indent_motion(
3061 map: &DisplaySnapshot,
3062 mut display_point: DisplayPoint,
3063 times: usize,
3064 direction: Direction,
3065 indent_type: IndentType,
3066) -> DisplayPoint {
3067 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3068 let current_row = MultiBufferRow(buffer_point.row);
3069 let current_indent = map.line_indent_for_buffer_row(current_row);
3070 if current_indent.is_line_empty() {
3071 return display_point;
3072 }
3073 let max_row = map.max_point().to_point(map).row;
3074
3075 for _ in 0..times {
3076 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3077
3078 let target_row = match direction {
3079 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3080 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3081 !indent.is_line_empty()
3082 && matches_indent_type(&indent, ¤t_indent, indent_type)
3083 }),
3084 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3085 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3086 !indent.is_line_empty()
3087 && matches_indent_type(&indent, ¤t_indent, indent_type)
3088 }),
3089 }
3090 .unwrap_or(current_buffer_row);
3091
3092 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3093 let new_point = first_non_whitespace(map, false, new_point);
3094 if new_point == display_point {
3095 break;
3096 }
3097 display_point = new_point;
3098 }
3099 display_point
3100}
3101
3102#[cfg(test)]
3103mod test {
3104
3105 use crate::{
3106 state::Mode,
3107 test::{NeovimBackedTestContext, VimTestContext},
3108 };
3109 use editor::display_map::Inlay;
3110 use indoc::indoc;
3111 use language::Point;
3112 use multi_buffer::MultiBufferRow;
3113
3114 #[gpui::test]
3115 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3116 let mut cx = NeovimBackedTestContext::new(cx).await;
3117
3118 let initial_state = indoc! {r"ˇabc
3119 def
3120
3121 paragraph
3122 the second
3123
3124
3125
3126 third and
3127 final"};
3128
3129 // goes down once
3130 cx.set_shared_state(initial_state).await;
3131 cx.simulate_shared_keystrokes("}").await;
3132 cx.shared_state().await.assert_eq(indoc! {r"abc
3133 def
3134 ˇ
3135 paragraph
3136 the second
3137
3138
3139
3140 third and
3141 final"});
3142
3143 // goes up once
3144 cx.simulate_shared_keystrokes("{").await;
3145 cx.shared_state().await.assert_eq(initial_state);
3146
3147 // goes down twice
3148 cx.simulate_shared_keystrokes("2 }").await;
3149 cx.shared_state().await.assert_eq(indoc! {r"abc
3150 def
3151
3152 paragraph
3153 the second
3154 ˇ
3155
3156
3157 third and
3158 final"});
3159
3160 // goes down over multiple blanks
3161 cx.simulate_shared_keystrokes("}").await;
3162 cx.shared_state().await.assert_eq(indoc! {r"abc
3163 def
3164
3165 paragraph
3166 the second
3167
3168
3169
3170 third and
3171 finaˇl"});
3172
3173 // goes up twice
3174 cx.simulate_shared_keystrokes("2 {").await;
3175 cx.shared_state().await.assert_eq(indoc! {r"abc
3176 def
3177 ˇ
3178 paragraph
3179 the second
3180
3181
3182
3183 third and
3184 final"});
3185 }
3186
3187 #[gpui::test]
3188 async fn test_matching(cx: &mut gpui::TestAppContext) {
3189 let mut cx = NeovimBackedTestContext::new(cx).await;
3190
3191 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3192 do(something(with<Types>.and_arrays[0, 2]))
3193 }"})
3194 .await;
3195 cx.simulate_shared_keystrokes("%").await;
3196 cx.shared_state()
3197 .await
3198 .assert_eq(indoc! {r"func (a stringˇ) {
3199 do(something(with<Types>.and_arrays[0, 2]))
3200 }"});
3201
3202 // test it works on the last character of the line
3203 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3204 do(something(with<Types>.and_arrays[0, 2]))
3205 }"})
3206 .await;
3207 cx.simulate_shared_keystrokes("%").await;
3208 cx.shared_state()
3209 .await
3210 .assert_eq(indoc! {r"func (a string) {
3211 do(something(with<Types>.and_arrays[0, 2]))
3212 ˇ}"});
3213
3214 // test it works on immediate nesting
3215 cx.set_shared_state("ˇ{()}").await;
3216 cx.simulate_shared_keystrokes("%").await;
3217 cx.shared_state().await.assert_eq("{()ˇ}");
3218 cx.simulate_shared_keystrokes("%").await;
3219 cx.shared_state().await.assert_eq("ˇ{()}");
3220
3221 // test it works on immediate nesting inside braces
3222 cx.set_shared_state("{\n ˇ{()}\n}").await;
3223 cx.simulate_shared_keystrokes("%").await;
3224 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3225
3226 // test it jumps to the next paren on a line
3227 cx.set_shared_state("func ˇboop() {\n}").await;
3228 cx.simulate_shared_keystrokes("%").await;
3229 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3230 }
3231
3232 #[gpui::test]
3233 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3234 let mut cx = NeovimBackedTestContext::new(cx).await;
3235
3236 // test it works with curly braces
3237 cx.set_shared_state(indoc! {r"func (a string) {
3238 do(something(with<Types>.anˇd_arrays[0, 2]))
3239 }"})
3240 .await;
3241 cx.simulate_shared_keystrokes("] }").await;
3242 cx.shared_state()
3243 .await
3244 .assert_eq(indoc! {r"func (a string) {
3245 do(something(with<Types>.and_arrays[0, 2]))
3246 ˇ}"});
3247
3248 // test it works with brackets
3249 cx.set_shared_state(indoc! {r"func (a string) {
3250 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3251 }"})
3252 .await;
3253 cx.simulate_shared_keystrokes("] )").await;
3254 cx.shared_state()
3255 .await
3256 .assert_eq(indoc! {r"func (a string) {
3257 do(something(with<Types>.and_arrays[0, 2])ˇ)
3258 }"});
3259
3260 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3261 .await;
3262 cx.simulate_shared_keystrokes("] )").await;
3263 cx.shared_state()
3264 .await
3265 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3266
3267 // test it works on immediate nesting
3268 cx.set_shared_state("{ˇ {}{}}").await;
3269 cx.simulate_shared_keystrokes("] }").await;
3270 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3271 cx.set_shared_state("(ˇ ()())").await;
3272 cx.simulate_shared_keystrokes("] )").await;
3273 cx.shared_state().await.assert_eq("( ()()ˇ)");
3274
3275 // test it works on immediate nesting inside braces
3276 cx.set_shared_state("{\n ˇ {()}\n}").await;
3277 cx.simulate_shared_keystrokes("] }").await;
3278 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3279 cx.set_shared_state("(\n ˇ {()}\n)").await;
3280 cx.simulate_shared_keystrokes("] )").await;
3281 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3282 }
3283
3284 #[gpui::test]
3285 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3286 let mut cx = NeovimBackedTestContext::new(cx).await;
3287
3288 // test it works with curly braces
3289 cx.set_shared_state(indoc! {r"func (a string) {
3290 do(something(with<Types>.anˇd_arrays[0, 2]))
3291 }"})
3292 .await;
3293 cx.simulate_shared_keystrokes("[ {").await;
3294 cx.shared_state()
3295 .await
3296 .assert_eq(indoc! {r"func (a string) ˇ{
3297 do(something(with<Types>.and_arrays[0, 2]))
3298 }"});
3299
3300 // test it works with brackets
3301 cx.set_shared_state(indoc! {r"func (a string) {
3302 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3303 }"})
3304 .await;
3305 cx.simulate_shared_keystrokes("[ (").await;
3306 cx.shared_state()
3307 .await
3308 .assert_eq(indoc! {r"func (a string) {
3309 doˇ(something(with<Types>.and_arrays[0, 2]))
3310 }"});
3311
3312 // test it works on immediate nesting
3313 cx.set_shared_state("{{}{} ˇ }").await;
3314 cx.simulate_shared_keystrokes("[ {").await;
3315 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3316 cx.set_shared_state("(()() ˇ )").await;
3317 cx.simulate_shared_keystrokes("[ (").await;
3318 cx.shared_state().await.assert_eq("ˇ(()() )");
3319
3320 // test it works on immediate nesting inside braces
3321 cx.set_shared_state("{\n {()} ˇ\n}").await;
3322 cx.simulate_shared_keystrokes("[ {").await;
3323 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3324 cx.set_shared_state("(\n {()} ˇ\n)").await;
3325 cx.simulate_shared_keystrokes("[ (").await;
3326 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3327 }
3328
3329 #[gpui::test]
3330 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3331 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3332
3333 cx.neovim.exec("set filetype=html").await;
3334
3335 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3336 cx.simulate_shared_keystrokes("%").await;
3337 cx.shared_state()
3338 .await
3339 .assert_eq(indoc! {r"<body><ˇ/body>"});
3340 cx.simulate_shared_keystrokes("%").await;
3341
3342 // test jumping backwards
3343 cx.shared_state()
3344 .await
3345 .assert_eq(indoc! {r"<ˇbody></body>"});
3346
3347 // test self-closing tags
3348 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3349 cx.simulate_shared_keystrokes("%").await;
3350 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3351
3352 // test tag with attributes
3353 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3354 </div>
3355 "})
3356 .await;
3357 cx.simulate_shared_keystrokes("%").await;
3358 cx.shared_state()
3359 .await
3360 .assert_eq(indoc! {r"<div class='test' id='main'>
3361 <ˇ/div>
3362 "});
3363
3364 // test multi-line self-closing tag
3365 cx.set_shared_state(indoc! {r#"<a>
3366 <br
3367 test = "test"
3368 /ˇ>
3369 </a>"#})
3370 .await;
3371 cx.simulate_shared_keystrokes("%").await;
3372 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3373 ˇ<br
3374 test = "test"
3375 />
3376 </a>"#});
3377 }
3378
3379 #[gpui::test]
3380 async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3381 let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3382
3383 // test brackets within tags
3384 cx.set_shared_state(indoc! {r"function f() {
3385 return (
3386 <div rules={ˇ[{ a: 1 }]}>
3387 <h1>test</h1>
3388 </div>
3389 );
3390 }"})
3391 .await;
3392 cx.simulate_shared_keystrokes("%").await;
3393 cx.shared_state().await.assert_eq(indoc! {r"function f() {
3394 return (
3395 <div rules={[{ a: 1 }ˇ]}>
3396 <h1>test</h1>
3397 </div>
3398 );
3399 }"});
3400 }
3401
3402 #[gpui::test]
3403 async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
3404 let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
3405
3406 cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
3407 .await;
3408 cx.simulate_shared_keystrokes("%").await;
3409 cx.shared_state()
3410 .await
3411 .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
3412 cx.simulate_shared_keystrokes("%").await;
3413 cx.shared_state()
3414 .await
3415 .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
3416 }
3417
3418 #[gpui::test]
3419 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3420 let mut cx = NeovimBackedTestContext::new(cx).await;
3421
3422 // f and F
3423 cx.set_shared_state("ˇone two three four").await;
3424 cx.simulate_shared_keystrokes("f o").await;
3425 cx.shared_state().await.assert_eq("one twˇo three four");
3426 cx.simulate_shared_keystrokes(",").await;
3427 cx.shared_state().await.assert_eq("ˇone two three four");
3428 cx.simulate_shared_keystrokes("2 ;").await;
3429 cx.shared_state().await.assert_eq("one two three fˇour");
3430 cx.simulate_shared_keystrokes("shift-f e").await;
3431 cx.shared_state().await.assert_eq("one two threˇe four");
3432 cx.simulate_shared_keystrokes("2 ;").await;
3433 cx.shared_state().await.assert_eq("onˇe two three four");
3434 cx.simulate_shared_keystrokes(",").await;
3435 cx.shared_state().await.assert_eq("one two thrˇee four");
3436
3437 // t and T
3438 cx.set_shared_state("ˇone two three four").await;
3439 cx.simulate_shared_keystrokes("t o").await;
3440 cx.shared_state().await.assert_eq("one tˇwo three four");
3441 cx.simulate_shared_keystrokes(",").await;
3442 cx.shared_state().await.assert_eq("oˇne two three four");
3443 cx.simulate_shared_keystrokes("2 ;").await;
3444 cx.shared_state().await.assert_eq("one two three ˇfour");
3445 cx.simulate_shared_keystrokes("shift-t e").await;
3446 cx.shared_state().await.assert_eq("one two threeˇ four");
3447 cx.simulate_shared_keystrokes("3 ;").await;
3448 cx.shared_state().await.assert_eq("oneˇ two three four");
3449 cx.simulate_shared_keystrokes(",").await;
3450 cx.shared_state().await.assert_eq("one two thˇree four");
3451 }
3452
3453 #[gpui::test]
3454 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3455 let mut cx = NeovimBackedTestContext::new(cx).await;
3456 let initial_state = indoc! {r"something(ˇfoo)"};
3457 cx.set_shared_state(initial_state).await;
3458 cx.simulate_shared_keystrokes("}").await;
3459 cx.shared_state().await.assert_eq("something(fooˇ)");
3460 }
3461
3462 #[gpui::test]
3463 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3464 let mut cx = NeovimBackedTestContext::new(cx).await;
3465 cx.set_shared_state("ˇone\n two\nthree").await;
3466 cx.simulate_shared_keystrokes("enter").await;
3467 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3468 }
3469
3470 #[gpui::test]
3471 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3472 let mut cx = NeovimBackedTestContext::new(cx).await;
3473 cx.set_shared_state("ˇ one\n two \nthree").await;
3474 cx.simulate_shared_keystrokes("g _").await;
3475 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3476
3477 cx.set_shared_state("ˇ one \n two \nthree").await;
3478 cx.simulate_shared_keystrokes("g _").await;
3479 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3480 cx.simulate_shared_keystrokes("2 g _").await;
3481 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3482 }
3483
3484 #[gpui::test]
3485 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3486 let mut cx = NeovimBackedTestContext::new(cx).await;
3487 let initial_state = indoc! {r"abc
3488 def
3489 paragraph
3490 the second
3491 third ˇand
3492 final"};
3493
3494 cx.set_shared_state(initial_state).await;
3495 cx.simulate_shared_keystrokes("shift-h").await;
3496 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3497 def
3498 paragraph
3499 the second
3500 third and
3501 final"});
3502
3503 // clip point
3504 cx.set_shared_state(indoc! {r"
3505 1 2 3
3506 4 5 6
3507 7 8 ˇ9
3508 "})
3509 .await;
3510 cx.simulate_shared_keystrokes("shift-h").await;
3511 cx.shared_state().await.assert_eq(indoc! {"
3512 1 2 ˇ3
3513 4 5 6
3514 7 8 9
3515 "});
3516
3517 cx.set_shared_state(indoc! {r"
3518 1 2 3
3519 4 5 6
3520 ˇ7 8 9
3521 "})
3522 .await;
3523 cx.simulate_shared_keystrokes("shift-h").await;
3524 cx.shared_state().await.assert_eq(indoc! {"
3525 ˇ1 2 3
3526 4 5 6
3527 7 8 9
3528 "});
3529
3530 cx.set_shared_state(indoc! {r"
3531 1 2 3
3532 4 5 ˇ6
3533 7 8 9"})
3534 .await;
3535 cx.simulate_shared_keystrokes("9 shift-h").await;
3536 cx.shared_state().await.assert_eq(indoc! {"
3537 1 2 3
3538 4 5 6
3539 7 8 ˇ9"});
3540 }
3541
3542 #[gpui::test]
3543 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3544 let mut cx = NeovimBackedTestContext::new(cx).await;
3545 let initial_state = indoc! {r"abˇc
3546 def
3547 paragraph
3548 the second
3549 third and
3550 final"};
3551
3552 cx.set_shared_state(initial_state).await;
3553 cx.simulate_shared_keystrokes("shift-m").await;
3554 cx.shared_state().await.assert_eq(indoc! {r"abc
3555 def
3556 paˇragraph
3557 the second
3558 third and
3559 final"});
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 cx.set_shared_state(indoc! {r"
3598 1 2 3
3599 ˇ4 5 6
3600 7 8 9
3601 "})
3602 .await;
3603 cx.simulate_shared_keystrokes("shift-m").await;
3604 cx.shared_state().await.assert_eq(indoc! {"
3605 1 2 3
3606 ˇ4 5 6
3607 7 8 9
3608 "});
3609 cx.set_shared_state(indoc! {r"
3610 1 2 3
3611 4 5 ˇ6
3612 7 8 9
3613 "})
3614 .await;
3615 cx.simulate_shared_keystrokes("shift-m").await;
3616 cx.shared_state().await.assert_eq(indoc! {"
3617 1 2 3
3618 4 5 ˇ6
3619 7 8 9
3620 "});
3621 }
3622
3623 #[gpui::test]
3624 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3625 let mut cx = NeovimBackedTestContext::new(cx).await;
3626 let initial_state = indoc! {r"abc
3627 deˇf
3628 paragraph
3629 the second
3630 third and
3631 final"};
3632
3633 cx.set_shared_state(initial_state).await;
3634 cx.simulate_shared_keystrokes("shift-l").await;
3635 cx.shared_state().await.assert_eq(indoc! {r"abc
3636 def
3637 paragraph
3638 the second
3639 third and
3640 fiˇnal"});
3641
3642 cx.set_shared_state(indoc! {r"
3643 1 2 3
3644 4 5 ˇ6
3645 7 8 9
3646 "})
3647 .await;
3648 cx.simulate_shared_keystrokes("shift-l").await;
3649 cx.shared_state().await.assert_eq(indoc! {"
3650 1 2 3
3651 4 5 6
3652 7 8 9
3653 ˇ"});
3654
3655 cx.set_shared_state(indoc! {r"
3656 1 2 3
3657 ˇ4 5 6
3658 7 8 9
3659 "})
3660 .await;
3661 cx.simulate_shared_keystrokes("shift-l").await;
3662 cx.shared_state().await.assert_eq(indoc! {"
3663 1 2 3
3664 4 5 6
3665 7 8 9
3666 ˇ"});
3667
3668 cx.set_shared_state(indoc! {r"
3669 1 2 ˇ3
3670 4 5 6
3671 7 8 9
3672 "})
3673 .await;
3674 cx.simulate_shared_keystrokes("shift-l").await;
3675 cx.shared_state().await.assert_eq(indoc! {"
3676 1 2 3
3677 4 5 6
3678 7 8 9
3679 ˇ"});
3680
3681 cx.set_shared_state(indoc! {r"
3682 ˇ1 2 3
3683 4 5 6
3684 7 8 9
3685 "})
3686 .await;
3687 cx.simulate_shared_keystrokes("shift-l").await;
3688 cx.shared_state().await.assert_eq(indoc! {"
3689 1 2 3
3690 4 5 6
3691 7 8 9
3692 ˇ"});
3693
3694 cx.set_shared_state(indoc! {r"
3695 1 2 3
3696 4 5 ˇ6
3697 7 8 9
3698 "})
3699 .await;
3700 cx.simulate_shared_keystrokes("9 shift-l").await;
3701 cx.shared_state().await.assert_eq(indoc! {"
3702 1 2 ˇ3
3703 4 5 6
3704 7 8 9
3705 "});
3706 }
3707
3708 #[gpui::test]
3709 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3710 let mut cx = NeovimBackedTestContext::new(cx).await;
3711 cx.set_shared_state(indoc! {r"
3712 456 5ˇ67 678
3713 "})
3714 .await;
3715 cx.simulate_shared_keystrokes("g e").await;
3716 cx.shared_state().await.assert_eq(indoc! {"
3717 45ˇ6 567 678
3718 "});
3719
3720 // Test times
3721 cx.set_shared_state(indoc! {r"
3722 123 234 345
3723 456 5ˇ67 678
3724 "})
3725 .await;
3726 cx.simulate_shared_keystrokes("4 g e").await;
3727 cx.shared_state().await.assert_eq(indoc! {"
3728 12ˇ3 234 345
3729 456 567 678
3730 "});
3731
3732 // With punctuation
3733 cx.set_shared_state(indoc! {r"
3734 123 234 345
3735 4;5.6 5ˇ67 678
3736 789 890 901
3737 "})
3738 .await;
3739 cx.simulate_shared_keystrokes("g e").await;
3740 cx.shared_state().await.assert_eq(indoc! {"
3741 123 234 345
3742 4;5.ˇ6 567 678
3743 789 890 901
3744 "});
3745
3746 // With punctuation and count
3747 cx.set_shared_state(indoc! {r"
3748 123 234 345
3749 4;5.6 5ˇ67 678
3750 789 890 901
3751 "})
3752 .await;
3753 cx.simulate_shared_keystrokes("5 g e").await;
3754 cx.shared_state().await.assert_eq(indoc! {"
3755 123 234 345
3756 ˇ4;5.6 567 678
3757 789 890 901
3758 "});
3759
3760 // newlines
3761 cx.set_shared_state(indoc! {r"
3762 123 234 345
3763
3764 78ˇ9 890 901
3765 "})
3766 .await;
3767 cx.simulate_shared_keystrokes("g e").await;
3768 cx.shared_state().await.assert_eq(indoc! {"
3769 123 234 345
3770 ˇ
3771 789 890 901
3772 "});
3773 cx.simulate_shared_keystrokes("g e").await;
3774 cx.shared_state().await.assert_eq(indoc! {"
3775 123 234 34ˇ5
3776
3777 789 890 901
3778 "});
3779
3780 // With punctuation
3781 cx.set_shared_state(indoc! {r"
3782 123 234 345
3783 4;5.ˇ6 567 678
3784 789 890 901
3785 "})
3786 .await;
3787 cx.simulate_shared_keystrokes("g shift-e").await;
3788 cx.shared_state().await.assert_eq(indoc! {"
3789 123 234 34ˇ5
3790 4;5.6 567 678
3791 789 890 901
3792 "});
3793
3794 // With multi byte char
3795 cx.set_shared_state(indoc! {r"
3796 bar ˇó
3797 "})
3798 .await;
3799 cx.simulate_shared_keystrokes("g e").await;
3800 cx.shared_state().await.assert_eq(indoc! {"
3801 baˇr ó
3802 "});
3803 }
3804
3805 #[gpui::test]
3806 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3807 let mut cx = NeovimBackedTestContext::new(cx).await;
3808
3809 cx.set_shared_state(indoc! {"
3810 fn aˇ() {
3811 return
3812 }
3813 "})
3814 .await;
3815 cx.simulate_shared_keystrokes("v $ %").await;
3816 cx.shared_state().await.assert_eq(indoc! {"
3817 fn a«() {
3818 return
3819 }ˇ»
3820 "});
3821 }
3822
3823 #[gpui::test]
3824 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3825 let mut cx = VimTestContext::new(cx, true).await;
3826
3827 cx.set_state(
3828 indoc! {"
3829 struct Foo {
3830 ˇ
3831 }
3832 "},
3833 Mode::Normal,
3834 );
3835
3836 cx.update_editor(|editor, _window, cx| {
3837 let range = editor.selections.newest_anchor().range();
3838 let inlay_text = " field: int,\n field2: string\n field3: float";
3839 let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
3840 editor.splice_inlays(&[], vec![inlay], cx);
3841 });
3842
3843 cx.simulate_keystrokes("j");
3844 cx.assert_state(
3845 indoc! {"
3846 struct Foo {
3847
3848 ˇ}
3849 "},
3850 Mode::Normal,
3851 );
3852 }
3853
3854 #[gpui::test]
3855 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3856 let mut cx = VimTestContext::new(cx, true).await;
3857
3858 cx.set_state(
3859 indoc! {"
3860 ˇstruct Foo {
3861
3862 }
3863 "},
3864 Mode::Normal,
3865 );
3866 cx.update_editor(|editor, _window, cx| {
3867 let snapshot = editor.buffer().read(cx).snapshot(cx);
3868 let end_of_line =
3869 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3870 let inlay_text = " hint";
3871 let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
3872 editor.splice_inlays(&[], vec![inlay], cx);
3873 });
3874 cx.simulate_keystrokes("$");
3875 cx.assert_state(
3876 indoc! {"
3877 struct Foo ˇ{
3878
3879 }
3880 "},
3881 Mode::Normal,
3882 );
3883 }
3884
3885 #[gpui::test]
3886 async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
3887 let mut cx = VimTestContext::new(cx, true).await;
3888
3889 // Test the exact scenario from issue #29134
3890 cx.set_state(
3891 indoc! {"
3892 fn main() {
3893 let this_is_a_long_name = Vec::<u32>::new();
3894 let new_oneˇ = this_is_a_long_name
3895 .iter()
3896 .map(|i| i + 1)
3897 .map(|i| i * 2)
3898 .collect::<Vec<_>>();
3899 }
3900 "},
3901 Mode::Normal,
3902 );
3903
3904 // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
3905 cx.update_editor(|editor, _window, cx| {
3906 let snapshot = editor.buffer().read(cx).snapshot(cx);
3907 // The empty line is at line 3 (0-indexed)
3908 let line_start = snapshot.anchor_after(Point::new(3, 0));
3909 let inlay_text = ": Vec<u32>";
3910 let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
3911 editor.splice_inlays(&[], vec![inlay], cx);
3912 });
3913
3914 // Enter visual mode
3915 cx.simulate_keystrokes("v");
3916 cx.assert_state(
3917 indoc! {"
3918 fn main() {
3919 let this_is_a_long_name = Vec::<u32>::new();
3920 let new_one« ˇ»= this_is_a_long_name
3921 .iter()
3922 .map(|i| i + 1)
3923 .map(|i| i * 2)
3924 .collect::<Vec<_>>();
3925 }
3926 "},
3927 Mode::Visual,
3928 );
3929
3930 // Move down - should go to the beginning of line 4, not skip to line 5
3931 cx.simulate_keystrokes("j");
3932 cx.assert_state(
3933 indoc! {"
3934 fn main() {
3935 let this_is_a_long_name = Vec::<u32>::new();
3936 let new_one« = this_is_a_long_name
3937 ˇ» .iter()
3938 .map(|i| i + 1)
3939 .map(|i| i * 2)
3940 .collect::<Vec<_>>();
3941 }
3942 "},
3943 Mode::Visual,
3944 );
3945
3946 // Test with multiple movements
3947 cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
3948
3949 // Add type hint on the empty line
3950 cx.update_editor(|editor, _window, cx| {
3951 let snapshot = editor.buffer().read(cx).snapshot(cx);
3952 let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
3953 let inlay_text = ": i32";
3954 let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
3955 editor.splice_inlays(&[], vec![inlay], cx);
3956 });
3957
3958 // Enter visual mode and move down twice
3959 cx.simulate_keystrokes("v j j");
3960 cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
3961 }
3962
3963 #[gpui::test]
3964 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3965 let mut cx = NeovimBackedTestContext::new(cx).await;
3966 // Normal mode
3967 cx.set_shared_state(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 .await;
3978 cx.simulate_shared_keystrokes("2 0 %").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 cx.simulate_shared_keystrokes("2 5 %").await;
3991 cx.shared_state().await.assert_eq(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
4002 cx.simulate_shared_keystrokes("7 5 %").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 jumps over
4009 the lazy dog
4010 The ˇquick brown
4011 fox jumps over
4012 the lazy dog"});
4013
4014 // Visual mode
4015 cx.set_shared_state(indoc! {"
4016 The ˇquick brown
4017 fox jumps over
4018 the lazy dog
4019 The quick brown
4020 fox jumps over
4021 the lazy dog
4022 The quick brown
4023 fox jumps over
4024 the lazy dog"})
4025 .await;
4026 cx.simulate_shared_keystrokes("v 5 0 %").await;
4027 cx.shared_state().await.assert_eq(indoc! {"
4028 The «quick brown
4029 fox jumps over
4030 the lazy dog
4031 The quick brown
4032 fox jˇ»umps over
4033 the lazy dog
4034 The quick brown
4035 fox jumps over
4036 the lazy dog"});
4037
4038 cx.set_shared_state(indoc! {"
4039 The ˇquick brown
4040 fox jumps over
4041 the lazy dog
4042 The quick brown
4043 fox jumps over
4044 the lazy dog
4045 The quick brown
4046 fox jumps over
4047 the lazy dog"})
4048 .await;
4049 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4050 cx.shared_state().await.assert_eq(indoc! {"
4051 The «quick brown
4052 fox jumps over
4053 the lazy dog
4054 The quick brown
4055 fox jumps over
4056 the lazy dog
4057 The quick brown
4058 fox jumps over
4059 the lˇ»azy dog"});
4060 }
4061
4062 #[gpui::test]
4063 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4064 let mut cx = NeovimBackedTestContext::new(cx).await;
4065
4066 cx.set_shared_state("ˇπππππ").await;
4067 cx.simulate_shared_keystrokes("3 space").await;
4068 cx.shared_state().await.assert_eq("πππˇππ");
4069 }
4070
4071 #[gpui::test]
4072 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4073 let mut cx = NeovimBackedTestContext::new(cx).await;
4074
4075 cx.set_shared_state(indoc! {"
4076 ππππˇπ
4077 πanotherline"})
4078 .await;
4079 cx.simulate_shared_keystrokes("4 space").await;
4080 cx.shared_state().await.assert_eq(indoc! {"
4081 πππππ
4082 πanˇotherline"});
4083 }
4084
4085 #[gpui::test]
4086 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4087 let mut cx = NeovimBackedTestContext::new(cx).await;
4088
4089 cx.set_shared_state(indoc! {"
4090 ππππ
4091 πanˇotherline"})
4092 .await;
4093 cx.simulate_shared_keystrokes("4 backspace").await;
4094 cx.shared_state().await.assert_eq(indoc! {"
4095 πππˇπ
4096 πanotherline"});
4097 }
4098
4099 #[gpui::test]
4100 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4101 let mut cx = VimTestContext::new(cx, true).await;
4102 cx.set_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("] =");
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("2 [ =");
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 cx.simulate_keystrokes("] +");
4162 cx.assert_state(
4163 indoc! {
4164 "func empty(a string) bool {
4165 if a == \"\" {
4166 ˇreturn true
4167 }
4168 return false
4169 }"
4170 },
4171 Mode::Normal,
4172 );
4173 cx.simulate_keystrokes("] -");
4174 cx.assert_state(
4175 indoc! {
4176 "func empty(a string) bool {
4177 if a == \"\" {
4178 return true
4179 ˇ}
4180 return false
4181 }"
4182 },
4183 Mode::Normal,
4184 );
4185 }
4186
4187 #[gpui::test]
4188 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4189 let mut cx = NeovimBackedTestContext::new(cx).await;
4190 cx.set_shared_state("abˇc").await;
4191 cx.simulate_shared_keystrokes("delete").await;
4192 cx.shared_state().await.assert_eq("aˇb");
4193 }
4194
4195 #[gpui::test]
4196 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4197 let mut cx = NeovimBackedTestContext::new(cx).await;
4198
4199 cx.set_shared_state(indoc! {"
4200 ˇthe quick brown fox
4201 jumped over the lazy dog"})
4202 .await;
4203 cx.simulate_shared_keystrokes("d v 0").await;
4204 cx.shared_state().await.assert_eq(indoc! {"
4205 ˇhe quick brown fox
4206 jumped over the lazy dog"});
4207 assert!(!cx.cx.forced_motion());
4208
4209 cx.set_shared_state(indoc! {"
4210 the quick bˇrown fox
4211 jumped over the lazy dog"})
4212 .await;
4213 cx.simulate_shared_keystrokes("d v 0").await;
4214 cx.shared_state().await.assert_eq(indoc! {"
4215 ˇown fox
4216 jumped over the lazy dog"});
4217 assert!(!cx.cx.forced_motion());
4218
4219 cx.set_shared_state(indoc! {"
4220 the quick brown foˇx
4221 jumped over the lazy dog"})
4222 .await;
4223 cx.simulate_shared_keystrokes("d v 0").await;
4224 cx.shared_state().await.assert_eq(indoc! {"
4225 ˇ
4226 jumped over the lazy dog"});
4227 assert!(!cx.cx.forced_motion());
4228 }
4229
4230 #[gpui::test]
4231 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4232 let mut cx = NeovimBackedTestContext::new(cx).await;
4233
4234 cx.set_shared_state(indoc! {"
4235 ˇthe quick brown fox
4236 jumped over the lazy dog"})
4237 .await;
4238 cx.simulate_shared_keystrokes("d v g shift-m").await;
4239 cx.shared_state().await.assert_eq(indoc! {"
4240 ˇbrown fox
4241 jumped over the lazy dog"});
4242 assert!(!cx.cx.forced_motion());
4243
4244 cx.set_shared_state(indoc! {"
4245 the quick bˇrown fox
4246 jumped over the lazy dog"})
4247 .await;
4248 cx.simulate_shared_keystrokes("d v g shift-m").await;
4249 cx.shared_state().await.assert_eq(indoc! {"
4250 the quickˇown fox
4251 jumped over the lazy dog"});
4252 assert!(!cx.cx.forced_motion());
4253
4254 cx.set_shared_state(indoc! {"
4255 the quick brown foˇx
4256 jumped over the lazy dog"})
4257 .await;
4258 cx.simulate_shared_keystrokes("d v g shift-m").await;
4259 cx.shared_state().await.assert_eq(indoc! {"
4260 the quicˇk
4261 jumped over the lazy dog"});
4262 assert!(!cx.cx.forced_motion());
4263
4264 cx.set_shared_state(indoc! {"
4265 ˇthe quick brown fox
4266 jumped over the lazy dog"})
4267 .await;
4268 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4269 cx.shared_state().await.assert_eq(indoc! {"
4270 ˇ fox
4271 jumped over the lazy dog"});
4272 assert!(!cx.cx.forced_motion());
4273
4274 cx.set_shared_state(indoc! {"
4275 ˇthe quick brown fox
4276 jumped over the lazy dog"})
4277 .await;
4278 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4279 cx.shared_state().await.assert_eq(indoc! {"
4280 ˇuick brown fox
4281 jumped over the lazy dog"});
4282 assert!(!cx.cx.forced_motion());
4283 }
4284
4285 #[gpui::test]
4286 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4287 let mut cx = NeovimBackedTestContext::new(cx).await;
4288
4289 cx.set_shared_state(indoc! {"
4290 the quick brown foˇx
4291 jumped over the lazy dog"})
4292 .await;
4293 cx.simulate_shared_keystrokes("d v $").await;
4294 cx.shared_state().await.assert_eq(indoc! {"
4295 the quick brown foˇx
4296 jumped over the lazy dog"});
4297 assert!(!cx.cx.forced_motion());
4298
4299 cx.set_shared_state(indoc! {"
4300 ˇthe quick brown fox
4301 jumped over the lazy dog"})
4302 .await;
4303 cx.simulate_shared_keystrokes("d v $").await;
4304 cx.shared_state().await.assert_eq(indoc! {"
4305 ˇx
4306 jumped over the lazy dog"});
4307 assert!(!cx.cx.forced_motion());
4308 }
4309
4310 #[gpui::test]
4311 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4312 let mut cx = NeovimBackedTestContext::new(cx).await;
4313
4314 cx.set_shared_state(indoc! {"
4315 ˇthe quick brown fox
4316 jumped over the lazy dog"})
4317 .await;
4318 cx.simulate_shared_keystrokes("y v j p").await;
4319 cx.shared_state().await.assert_eq(indoc! {"
4320 the quick brown fox
4321 ˇthe quick brown fox
4322 jumped over the lazy dog"});
4323 assert!(!cx.cx.forced_motion());
4324
4325 cx.set_shared_state(indoc! {"
4326 the quick bˇrown fox
4327 jumped over the lazy dog"})
4328 .await;
4329 cx.simulate_shared_keystrokes("y v j p").await;
4330 cx.shared_state().await.assert_eq(indoc! {"
4331 the quick brˇrown fox
4332 jumped overown fox
4333 jumped over the lazy dog"});
4334 assert!(!cx.cx.forced_motion());
4335
4336 cx.set_shared_state(indoc! {"
4337 the quick brown foˇx
4338 jumped over the lazy dog"})
4339 .await;
4340 cx.simulate_shared_keystrokes("y v j p").await;
4341 cx.shared_state().await.assert_eq(indoc! {"
4342 the quick brown foxˇx
4343 jumped over the la
4344 jumped over the lazy dog"});
4345 assert!(!cx.cx.forced_motion());
4346
4347 cx.set_shared_state(indoc! {"
4348 the quick brown fox
4349 jˇumped over the lazy dog"})
4350 .await;
4351 cx.simulate_shared_keystrokes("y v k p").await;
4352 cx.shared_state().await.assert_eq(indoc! {"
4353 thˇhe quick brown fox
4354 je quick brown fox
4355 jumped over the lazy dog"});
4356 assert!(!cx.cx.forced_motion());
4357 }
4358
4359 #[gpui::test]
4360 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4361 let mut cx = NeovimBackedTestContext::new(cx).await;
4362
4363 cx.set_shared_state(indoc! {"
4364 ˇthe quick brown fox
4365 jumped over the lazy dog"})
4366 .await;
4367 cx.simulate_shared_keystrokes("d v e").await;
4368 cx.shared_state().await.assert_eq(indoc! {"
4369 ˇe quick brown fox
4370 jumped over the lazy dog"});
4371 assert!(!cx.cx.forced_motion());
4372
4373 cx.set_shared_state(indoc! {"
4374 the quick bˇrown fox
4375 jumped over the lazy dog"})
4376 .await;
4377 cx.simulate_shared_keystrokes("d v e").await;
4378 cx.shared_state().await.assert_eq(indoc! {"
4379 the quick bˇn fox
4380 jumped over the lazy dog"});
4381 assert!(!cx.cx.forced_motion());
4382
4383 cx.set_shared_state(indoc! {"
4384 the quick brown foˇx
4385 jumped over the lazy dog"})
4386 .await;
4387 cx.simulate_shared_keystrokes("d v e").await;
4388 cx.shared_state().await.assert_eq(indoc! {"
4389 the quick brown foˇd over the lazy dog"});
4390 assert!(!cx.cx.forced_motion());
4391 }
4392}