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