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