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