1use editor::{
2 Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
3 display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
4 movement::{
5 self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
6 },
7};
8use gpui::{Action, Context, Window, actions, px};
9use language::{CharKind, Point, Selection, SelectionGoal};
10use multi_buffer::MultiBufferRow;
11use schemars::JsonSchema;
12use serde::Deserialize;
13use std::ops::Range;
14use workspace::searchable::Direction;
15
16use crate::{
17 Vim,
18 normal::mark,
19 state::{Mode, Operator},
20 surrounds::SurroundsType,
21};
22
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub(crate) enum MotionKind {
25 Linewise,
26 Exclusive,
27 Inclusive,
28}
29
30impl MotionKind {
31 pub(crate) fn for_mode(mode: Mode) -> Self {
32 match mode {
33 Mode::VisualLine => MotionKind::Linewise,
34 _ => MotionKind::Exclusive,
35 }
36 }
37
38 pub(crate) fn linewise(&self) -> bool {
39 matches!(self, MotionKind::Linewise)
40 }
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum Motion {
45 Left,
46 WrappingLeft,
47 Down {
48 display_lines: bool,
49 },
50 Up {
51 display_lines: bool,
52 },
53 Right,
54 WrappingRight,
55 NextWordStart {
56 ignore_punctuation: bool,
57 },
58 NextWordEnd {
59 ignore_punctuation: bool,
60 },
61 PreviousWordStart {
62 ignore_punctuation: bool,
63 },
64 PreviousWordEnd {
65 ignore_punctuation: bool,
66 },
67 NextSubwordStart {
68 ignore_punctuation: bool,
69 },
70 NextSubwordEnd {
71 ignore_punctuation: bool,
72 },
73 PreviousSubwordStart {
74 ignore_punctuation: bool,
75 },
76 PreviousSubwordEnd {
77 ignore_punctuation: bool,
78 },
79 FirstNonWhitespace {
80 display_lines: bool,
81 },
82 CurrentLine,
83 StartOfLine {
84 display_lines: bool,
85 },
86 MiddleOfLine {
87 display_lines: bool,
88 },
89 EndOfLine {
90 display_lines: bool,
91 },
92 SentenceBackward,
93 SentenceForward,
94 StartOfParagraph,
95 EndOfParagraph,
96 StartOfDocument,
97 EndOfDocument,
98 Matching,
99 GoToPercentage,
100 UnmatchedForward {
101 char: char,
102 },
103 UnmatchedBackward {
104 char: char,
105 },
106 FindForward {
107 before: bool,
108 char: char,
109 mode: FindRange,
110 smartcase: bool,
111 },
112 FindBackward {
113 after: bool,
114 char: char,
115 mode: FindRange,
116 smartcase: bool,
117 },
118 Sneak {
119 first_char: char,
120 second_char: char,
121 smartcase: bool,
122 },
123 SneakBackward {
124 first_char: char,
125 second_char: char,
126 smartcase: bool,
127 },
128 RepeatFind {
129 last_find: Box<Motion>,
130 },
131 RepeatFindReversed {
132 last_find: Box<Motion>,
133 },
134 NextLineStart,
135 PreviousLineStart,
136 StartOfLineDownward,
137 EndOfLineDownward,
138 GoToColumn,
139 WindowTop,
140 WindowMiddle,
141 WindowBottom,
142 NextSectionStart,
143 NextSectionEnd,
144 PreviousSectionStart,
145 PreviousSectionEnd,
146 NextMethodStart,
147 NextMethodEnd,
148 PreviousMethodStart,
149 PreviousMethodEnd,
150 NextComment,
151 PreviousComment,
152 PreviousLesserIndent,
153 PreviousGreaterIndent,
154 PreviousSameIndent,
155 NextLesserIndent,
156 NextGreaterIndent,
157 NextSameIndent,
158
159 // we don't have a good way to run a search synchronously, so
160 // we handle search motions by running the search async and then
161 // calling back into motion with this
162 ZedSearchResult {
163 prior_selections: Vec<Range<Anchor>>,
164 new_selections: Vec<Range<Anchor>>,
165 },
166 Jump {
167 anchor: Anchor,
168 line: bool,
169 },
170}
171
172#[derive(Clone, Copy)]
173enum IndentType {
174 Lesser,
175 Greater,
176 Same,
177}
178
179/// Moves to the start of the next word.
180#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
181#[action(namespace = vim)]
182#[serde(deny_unknown_fields)]
183struct NextWordStart {
184 #[serde(default)]
185 ignore_punctuation: bool,
186}
187
188/// Moves to the end of the next word.
189#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
190#[action(namespace = vim)]
191#[serde(deny_unknown_fields)]
192struct NextWordEnd {
193 #[serde(default)]
194 ignore_punctuation: bool,
195}
196
197/// Moves to the start of the previous word.
198#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
199#[action(namespace = vim)]
200#[serde(deny_unknown_fields)]
201struct PreviousWordStart {
202 #[serde(default)]
203 ignore_punctuation: bool,
204}
205
206/// Moves to the end of the previous word.
207#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
208#[action(namespace = vim)]
209#[serde(deny_unknown_fields)]
210struct PreviousWordEnd {
211 #[serde(default)]
212 ignore_punctuation: bool,
213}
214
215/// Moves to the start of the next subword.
216#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
217#[action(namespace = vim)]
218#[serde(deny_unknown_fields)]
219pub(crate) struct NextSubwordStart {
220 #[serde(default)]
221 pub(crate) ignore_punctuation: bool,
222}
223
224/// Moves to the end of the next subword.
225#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
226#[action(namespace = vim)]
227#[serde(deny_unknown_fields)]
228pub(crate) struct NextSubwordEnd {
229 #[serde(default)]
230 pub(crate) ignore_punctuation: bool,
231}
232
233/// Moves to the start of the previous subword.
234#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
235#[action(namespace = vim)]
236#[serde(deny_unknown_fields)]
237pub(crate) struct PreviousSubwordStart {
238 #[serde(default)]
239 pub(crate) ignore_punctuation: bool,
240}
241
242/// Moves to the end of the previous subword.
243#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
244#[action(namespace = vim)]
245#[serde(deny_unknown_fields)]
246pub(crate) struct PreviousSubwordEnd {
247 #[serde(default)]
248 pub(crate) ignore_punctuation: bool,
249}
250
251/// Moves cursor up by the specified number of lines.
252#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
253#[action(namespace = vim)]
254#[serde(deny_unknown_fields)]
255pub(crate) struct Up {
256 #[serde(default)]
257 pub(crate) display_lines: bool,
258}
259
260/// Moves cursor down by the specified number of lines.
261#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
262#[action(namespace = vim)]
263#[serde(deny_unknown_fields)]
264pub(crate) struct Down {
265 #[serde(default)]
266 pub(crate) display_lines: bool,
267}
268
269/// Moves to the first non-whitespace character on the current line.
270#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
271#[action(namespace = vim)]
272#[serde(deny_unknown_fields)]
273struct FirstNonWhitespace {
274 #[serde(default)]
275 display_lines: bool,
276}
277
278/// Moves to the end of the current line.
279#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
280#[action(namespace = vim)]
281#[serde(deny_unknown_fields)]
282struct EndOfLine {
283 #[serde(default)]
284 display_lines: bool,
285}
286
287/// Moves to the start of the current line.
288#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
289#[action(namespace = vim)]
290#[serde(deny_unknown_fields)]
291pub struct StartOfLine {
292 #[serde(default)]
293 pub(crate) display_lines: bool,
294}
295
296/// Moves to the middle of the current line.
297#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
298#[action(namespace = vim)]
299#[serde(deny_unknown_fields)]
300struct MiddleOfLine {
301 #[serde(default)]
302 display_lines: bool,
303}
304
305/// Finds the next unmatched bracket or delimiter.
306#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
307#[action(namespace = vim)]
308#[serde(deny_unknown_fields)]
309struct UnmatchedForward {
310 #[serde(default)]
311 char: char,
312}
313
314/// Finds the previous unmatched bracket or delimiter.
315#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
316#[action(namespace = vim)]
317#[serde(deny_unknown_fields)]
318struct UnmatchedBackward {
319 #[serde(default)]
320 char: char,
321}
322
323actions!(
324 vim,
325 [
326 /// Moves cursor left one character.
327 Left,
328 /// Moves cursor left one character, wrapping to previous line.
329 #[action(deprecated_aliases = ["vim::Backspace"])]
330 WrappingLeft,
331 /// Moves cursor right one character.
332 Right,
333 /// Moves cursor right one character, wrapping to next line.
334 #[action(deprecated_aliases = ["vim::Space"])]
335 WrappingRight,
336 /// Selects the current line.
337 CurrentLine,
338 /// Moves to the start of the next sentence.
339 SentenceForward,
340 /// Moves to the start of the previous sentence.
341 SentenceBackward,
342 /// Moves to the start of the paragraph.
343 StartOfParagraph,
344 /// Moves to the end of the paragraph.
345 EndOfParagraph,
346 /// Moves to the start of the document.
347 StartOfDocument,
348 /// Moves to the end of the document.
349 EndOfDocument,
350 /// Moves to the matching bracket or delimiter.
351 Matching,
352 /// Goes to a percentage position in the file.
353 GoToPercentage,
354 /// Moves to the start of the next line.
355 NextLineStart,
356 /// Moves to the start of the previous line.
357 PreviousLineStart,
358 /// Moves to the start of a line downward.
359 StartOfLineDownward,
360 /// Moves to the end of a line downward.
361 EndOfLineDownward,
362 /// Goes to a specific column number.
363 GoToColumn,
364 /// Repeats the last character find.
365 RepeatFind,
366 /// Repeats the last character find in reverse.
367 RepeatFindReversed,
368 /// Moves to the top of the window.
369 WindowTop,
370 /// Moves to the middle of the window.
371 WindowMiddle,
372 /// Moves to the bottom of the window.
373 WindowBottom,
374 /// Moves to the start of the next section.
375 NextSectionStart,
376 /// Moves to the end of the next section.
377 NextSectionEnd,
378 /// Moves to the start of the previous section.
379 PreviousSectionStart,
380 /// Moves to the end of the previous section.
381 PreviousSectionEnd,
382 /// Moves to the start of the next method.
383 NextMethodStart,
384 /// Moves to the end of the next method.
385 NextMethodEnd,
386 /// Moves to the start of the previous method.
387 PreviousMethodStart,
388 /// Moves to the end of the previous method.
389 PreviousMethodEnd,
390 /// Moves to the next comment.
391 NextComment,
392 /// Moves to the previous comment.
393 PreviousComment,
394 /// Moves to the previous line with lesser indentation.
395 PreviousLesserIndent,
396 /// Moves to the previous line with greater indentation.
397 PreviousGreaterIndent,
398 /// Moves to the previous line with the same indentation.
399 PreviousSameIndent,
400 /// Moves to the next line with lesser indentation.
401 NextLesserIndent,
402 /// Moves to the next line with greater indentation.
403 NextGreaterIndent,
404 /// Moves to the next line with the same indentation.
405 NextSameIndent,
406 ]
407);
408
409pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
410 Vim::action(editor, cx, |vim, _: &Left, window, cx| {
411 vim.motion(Motion::Left, window, cx)
412 });
413 Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
414 vim.motion(Motion::WrappingLeft, window, cx)
415 });
416 Vim::action(editor, cx, |vim, action: &Down, window, cx| {
417 vim.motion(
418 Motion::Down {
419 display_lines: action.display_lines,
420 },
421 window,
422 cx,
423 )
424 });
425 Vim::action(editor, cx, |vim, action: &Up, window, cx| {
426 vim.motion(
427 Motion::Up {
428 display_lines: action.display_lines,
429 },
430 window,
431 cx,
432 )
433 });
434 Vim::action(editor, cx, |vim, _: &Right, window, cx| {
435 vim.motion(Motion::Right, window, cx)
436 });
437 Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
438 vim.motion(Motion::WrappingRight, window, cx)
439 });
440 Vim::action(
441 editor,
442 cx,
443 |vim, action: &FirstNonWhitespace, window, cx| {
444 vim.motion(
445 Motion::FirstNonWhitespace {
446 display_lines: action.display_lines,
447 },
448 window,
449 cx,
450 )
451 },
452 );
453 Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
454 vim.motion(
455 Motion::StartOfLine {
456 display_lines: action.display_lines,
457 },
458 window,
459 cx,
460 )
461 });
462 Vim::action(editor, cx, |vim, action: &MiddleOfLine, window, cx| {
463 vim.motion(
464 Motion::MiddleOfLine {
465 display_lines: action.display_lines,
466 },
467 window,
468 cx,
469 )
470 });
471 Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| {
472 vim.motion(
473 Motion::EndOfLine {
474 display_lines: action.display_lines,
475 },
476 window,
477 cx,
478 )
479 });
480 Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| {
481 vim.motion(Motion::CurrentLine, window, cx)
482 });
483 Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| {
484 vim.motion(Motion::StartOfParagraph, window, cx)
485 });
486 Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| {
487 vim.motion(Motion::EndOfParagraph, window, cx)
488 });
489
490 Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| {
491 vim.motion(Motion::SentenceForward, window, cx)
492 });
493 Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| {
494 vim.motion(Motion::SentenceBackward, window, cx)
495 });
496 Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| {
497 vim.motion(Motion::StartOfDocument, window, cx)
498 });
499 Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| {
500 vim.motion(Motion::EndOfDocument, window, cx)
501 });
502 Vim::action(editor, cx, |vim, _: &Matching, window, cx| {
503 vim.motion(Motion::Matching, window, cx)
504 });
505 Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| {
506 vim.motion(Motion::GoToPercentage, window, cx)
507 });
508 Vim::action(
509 editor,
510 cx,
511 |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| {
512 vim.motion(Motion::UnmatchedForward { char }, window, cx)
513 },
514 );
515 Vim::action(
516 editor,
517 cx,
518 |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| {
519 vim.motion(Motion::UnmatchedBackward { char }, window, cx)
520 },
521 );
522 Vim::action(
523 editor,
524 cx,
525 |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| {
526 vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx)
527 },
528 );
529 Vim::action(
530 editor,
531 cx,
532 |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| {
533 vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx)
534 },
535 );
536 Vim::action(
537 editor,
538 cx,
539 |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| {
540 vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx)
541 },
542 );
543 Vim::action(
544 editor,
545 cx,
546 |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| {
547 vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx)
548 },
549 );
550 Vim::action(
551 editor,
552 cx,
553 |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| {
554 vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx)
555 },
556 );
557 Vim::action(
558 editor,
559 cx,
560 |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| {
561 vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx)
562 },
563 );
564 Vim::action(
565 editor,
566 cx,
567 |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| {
568 vim.motion(
569 Motion::PreviousSubwordStart { ignore_punctuation },
570 window,
571 cx,
572 )
573 },
574 );
575 Vim::action(
576 editor,
577 cx,
578 |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| {
579 vim.motion(
580 Motion::PreviousSubwordEnd { ignore_punctuation },
581 window,
582 cx,
583 )
584 },
585 );
586 Vim::action(editor, cx, |vim, &NextLineStart, window, cx| {
587 vim.motion(Motion::NextLineStart, window, cx)
588 });
589 Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| {
590 vim.motion(Motion::PreviousLineStart, window, cx)
591 });
592 Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| {
593 vim.motion(Motion::StartOfLineDownward, window, cx)
594 });
595 Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| {
596 vim.motion(Motion::EndOfLineDownward, window, cx)
597 });
598 Vim::action(editor, cx, |vim, &GoToColumn, window, cx| {
599 vim.motion(Motion::GoToColumn, window, cx)
600 });
601
602 Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| {
603 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
604 vim.motion(Motion::RepeatFind { last_find }, window, cx);
605 }
606 });
607
608 Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| {
609 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
610 vim.motion(Motion::RepeatFindReversed { last_find }, window, cx);
611 }
612 });
613 Vim::action(editor, cx, |vim, &WindowTop, window, cx| {
614 vim.motion(Motion::WindowTop, window, cx)
615 });
616 Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| {
617 vim.motion(Motion::WindowMiddle, window, cx)
618 });
619 Vim::action(editor, cx, |vim, &WindowBottom, window, cx| {
620 vim.motion(Motion::WindowBottom, window, cx)
621 });
622
623 Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| {
624 vim.motion(Motion::PreviousSectionStart, window, cx)
625 });
626 Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| {
627 vim.motion(Motion::NextSectionStart, window, cx)
628 });
629 Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| {
630 vim.motion(Motion::PreviousSectionEnd, window, cx)
631 });
632 Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| {
633 vim.motion(Motion::NextSectionEnd, window, cx)
634 });
635 Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| {
636 vim.motion(Motion::PreviousMethodStart, window, cx)
637 });
638 Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| {
639 vim.motion(Motion::NextMethodStart, window, cx)
640 });
641 Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| {
642 vim.motion(Motion::PreviousMethodEnd, window, cx)
643 });
644 Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| {
645 vim.motion(Motion::NextMethodEnd, window, cx)
646 });
647 Vim::action(editor, cx, |vim, &NextComment, window, cx| {
648 vim.motion(Motion::NextComment, window, cx)
649 });
650 Vim::action(editor, cx, |vim, &PreviousComment, window, cx| {
651 vim.motion(Motion::PreviousComment, window, cx)
652 });
653 Vim::action(editor, cx, |vim, &PreviousLesserIndent, window, cx| {
654 vim.motion(Motion::PreviousLesserIndent, window, cx)
655 });
656 Vim::action(editor, cx, |vim, &PreviousGreaterIndent, window, cx| {
657 vim.motion(Motion::PreviousGreaterIndent, window, cx)
658 });
659 Vim::action(editor, cx, |vim, &PreviousSameIndent, window, cx| {
660 vim.motion(Motion::PreviousSameIndent, window, cx)
661 });
662 Vim::action(editor, cx, |vim, &NextLesserIndent, window, cx| {
663 vim.motion(Motion::NextLesserIndent, window, cx)
664 });
665 Vim::action(editor, cx, |vim, &NextGreaterIndent, window, cx| {
666 vim.motion(Motion::NextGreaterIndent, window, cx)
667 });
668 Vim::action(editor, cx, |vim, &NextSameIndent, window, cx| {
669 vim.motion(Motion::NextSameIndent, window, cx)
670 });
671}
672
673impl Vim {
674 pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context<Self>) {
675 if let Motion::ZedSearchResult {
676 prior_selections, ..
677 } = &m
678 {
679 match self.mode {
680 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
681 if !prior_selections.is_empty() {
682 self.update_editor(cx, |_, editor, cx| {
683 editor.change_selections(Default::default(), window, cx, |s| {
684 s.select_ranges(prior_selections.iter().cloned())
685 })
686 });
687 }
688 }
689 Mode::Normal | Mode::Replace | Mode::Insert => {
690 if self.active_operator().is_none() {
691 return;
692 }
693 }
694
695 Mode::HelixNormal => {}
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, 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 always_advance: bool,
1727) -> DisplayPoint {
1728 let classifier = map
1729 .buffer_snapshot
1730 .char_classifier_at(point.to_point(map))
1731 .ignore_punctuation(ignore_punctuation);
1732 for _ in 0..times {
1733 let mut need_next_char = false;
1734 let new_point = if always_advance {
1735 next_char(map, point, allow_cross_newline)
1736 } else {
1737 point
1738 };
1739 let new_point = movement::find_boundary_exclusive(
1740 map,
1741 new_point,
1742 FindRange::MultiLine,
1743 |left, right| {
1744 let left_kind = classifier.kind(left);
1745 let right_kind = classifier.kind(right);
1746 let at_newline = right == '\n';
1747
1748 if !allow_cross_newline && at_newline {
1749 need_next_char = true;
1750 return true;
1751 }
1752
1753 left_kind != right_kind && left_kind != CharKind::Whitespace
1754 },
1755 );
1756 let new_point = if need_next_char {
1757 next_char(map, new_point, true)
1758 } else {
1759 new_point
1760 };
1761 let new_point = map.clip_point(new_point, Bias::Left);
1762 if point == new_point {
1763 break;
1764 }
1765 point = new_point;
1766 }
1767 point
1768}
1769
1770fn previous_word_start(
1771 map: &DisplaySnapshot,
1772 mut point: DisplayPoint,
1773 ignore_punctuation: bool,
1774 times: usize,
1775) -> DisplayPoint {
1776 let classifier = map
1777 .buffer_snapshot
1778 .char_classifier_at(point.to_point(map))
1779 .ignore_punctuation(ignore_punctuation);
1780 for _ in 0..times {
1781 // This works even though find_preceding_boundary is called for every character in the line containing
1782 // cursor because the newline is checked only once.
1783 let new_point = movement::find_preceding_boundary_display_point(
1784 map,
1785 point,
1786 FindRange::MultiLine,
1787 |left, right| {
1788 let left_kind = classifier.kind(left);
1789 let right_kind = classifier.kind(right);
1790
1791 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1792 },
1793 );
1794 if point == new_point {
1795 break;
1796 }
1797 point = new_point;
1798 }
1799 point
1800}
1801
1802fn previous_word_end(
1803 map: &DisplaySnapshot,
1804 point: DisplayPoint,
1805 ignore_punctuation: bool,
1806 times: usize,
1807) -> DisplayPoint {
1808 let classifier = map
1809 .buffer_snapshot
1810 .char_classifier_at(point.to_point(map))
1811 .ignore_punctuation(ignore_punctuation);
1812 let mut point = point.to_point(map);
1813
1814 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row))
1815 && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1816 {
1817 point.column += ch.len_utf8() as u32;
1818 }
1819 for _ in 0..times {
1820 let new_point = movement::find_preceding_boundary_point(
1821 &map.buffer_snapshot,
1822 point,
1823 FindRange::MultiLine,
1824 |left, right| {
1825 let left_kind = classifier.kind(left);
1826 let right_kind = classifier.kind(right);
1827 match (left_kind, right_kind) {
1828 (CharKind::Punctuation, CharKind::Whitespace)
1829 | (CharKind::Punctuation, CharKind::Word)
1830 | (CharKind::Word, CharKind::Whitespace)
1831 | (CharKind::Word, CharKind::Punctuation) => true,
1832 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1833 _ => false,
1834 }
1835 },
1836 );
1837 if new_point == point {
1838 break;
1839 }
1840 point = new_point;
1841 }
1842 movement::saturating_left(map, point.to_display_point(map))
1843}
1844
1845fn next_subword_start(
1846 map: &DisplaySnapshot,
1847 mut point: DisplayPoint,
1848 ignore_punctuation: bool,
1849 times: usize,
1850) -> DisplayPoint {
1851 let classifier = map
1852 .buffer_snapshot
1853 .char_classifier_at(point.to_point(map))
1854 .ignore_punctuation(ignore_punctuation);
1855 for _ in 0..times {
1856 let mut crossed_newline = false;
1857 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1858 let left_kind = classifier.kind(left);
1859 let right_kind = classifier.kind(right);
1860 let at_newline = right == '\n';
1861
1862 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1863 let is_subword_start =
1864 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1865
1866 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1867 || at_newline && crossed_newline
1868 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1869
1870 crossed_newline |= at_newline;
1871 found
1872 });
1873 if point == new_point {
1874 break;
1875 }
1876 point = new_point;
1877 }
1878 point
1879}
1880
1881pub(crate) fn next_subword_end(
1882 map: &DisplaySnapshot,
1883 mut point: DisplayPoint,
1884 ignore_punctuation: bool,
1885 times: usize,
1886 allow_cross_newline: bool,
1887) -> DisplayPoint {
1888 let classifier = map
1889 .buffer_snapshot
1890 .char_classifier_at(point.to_point(map))
1891 .ignore_punctuation(ignore_punctuation);
1892 for _ in 0..times {
1893 let new_point = next_char(map, point, allow_cross_newline);
1894
1895 let mut crossed_newline = false;
1896 let mut need_backtrack = false;
1897 let new_point =
1898 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1899 let left_kind = classifier.kind(left);
1900 let right_kind = classifier.kind(right);
1901 let at_newline = right == '\n';
1902
1903 if !allow_cross_newline && at_newline {
1904 return true;
1905 }
1906
1907 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1908 let is_subword_end =
1909 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1910
1911 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1912
1913 if found && (is_word_end || is_subword_end) {
1914 need_backtrack = true;
1915 }
1916
1917 crossed_newline |= at_newline;
1918 found
1919 });
1920 let mut new_point = map.clip_point(new_point, Bias::Left);
1921 if need_backtrack {
1922 *new_point.column_mut() -= 1;
1923 }
1924 let new_point = map.clip_point(new_point, Bias::Left);
1925 if point == new_point {
1926 break;
1927 }
1928 point = new_point;
1929 }
1930 point
1931}
1932
1933fn previous_subword_start(
1934 map: &DisplaySnapshot,
1935 mut point: DisplayPoint,
1936 ignore_punctuation: bool,
1937 times: usize,
1938) -> DisplayPoint {
1939 let classifier = map
1940 .buffer_snapshot
1941 .char_classifier_at(point.to_point(map))
1942 .ignore_punctuation(ignore_punctuation);
1943 for _ in 0..times {
1944 let mut crossed_newline = false;
1945 // This works even though find_preceding_boundary is called for every character in the line containing
1946 // cursor because the newline is checked only once.
1947 let new_point = movement::find_preceding_boundary_display_point(
1948 map,
1949 point,
1950 FindRange::MultiLine,
1951 |left, right| {
1952 let left_kind = classifier.kind(left);
1953 let right_kind = classifier.kind(right);
1954 let at_newline = right == '\n';
1955
1956 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1957 let is_subword_start =
1958 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1959
1960 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1961 || at_newline && crossed_newline
1962 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1963
1964 crossed_newline |= at_newline;
1965
1966 found
1967 },
1968 );
1969 if point == new_point {
1970 break;
1971 }
1972 point = new_point;
1973 }
1974 point
1975}
1976
1977fn previous_subword_end(
1978 map: &DisplaySnapshot,
1979 point: DisplayPoint,
1980 ignore_punctuation: bool,
1981 times: usize,
1982) -> DisplayPoint {
1983 let classifier = map
1984 .buffer_snapshot
1985 .char_classifier_at(point.to_point(map))
1986 .ignore_punctuation(ignore_punctuation);
1987 let mut point = point.to_point(map);
1988
1989 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row))
1990 && let Some(ch) = map.buffer_snapshot.chars_at(point).next()
1991 {
1992 point.column += ch.len_utf8() as u32;
1993 }
1994 for _ in 0..times {
1995 let new_point = movement::find_preceding_boundary_point(
1996 &map.buffer_snapshot,
1997 point,
1998 FindRange::MultiLine,
1999 |left, right| {
2000 let left_kind = classifier.kind(left);
2001 let right_kind = classifier.kind(right);
2002
2003 let is_subword_end =
2004 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
2005
2006 if is_subword_end {
2007 return true;
2008 }
2009
2010 match (left_kind, right_kind) {
2011 (CharKind::Word, CharKind::Whitespace)
2012 | (CharKind::Word, CharKind::Punctuation) => true,
2013 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2014 _ => false,
2015 }
2016 },
2017 );
2018 if new_point == point {
2019 break;
2020 }
2021 point = new_point;
2022 }
2023 movement::saturating_left(map, point.to_display_point(map))
2024}
2025
2026pub(crate) fn first_non_whitespace(
2027 map: &DisplaySnapshot,
2028 display_lines: bool,
2029 from: DisplayPoint,
2030) -> DisplayPoint {
2031 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2032 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2033 for (ch, offset) in map.buffer_chars_at(start_offset) {
2034 if ch == '\n' {
2035 return from;
2036 }
2037
2038 start_offset = offset;
2039
2040 if classifier.kind(ch) != CharKind::Whitespace {
2041 break;
2042 }
2043 }
2044
2045 start_offset.to_display_point(map)
2046}
2047
2048pub(crate) fn last_non_whitespace(
2049 map: &DisplaySnapshot,
2050 from: DisplayPoint,
2051 count: usize,
2052) -> DisplayPoint {
2053 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2054 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
2055
2056 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2057 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next()
2058 && classifier.kind(ch) != CharKind::Whitespace
2059 {
2060 return end_of_line.to_display_point(map);
2061 }
2062
2063 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2064 if ch == '\n' {
2065 break;
2066 }
2067 end_of_line = offset;
2068 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2069 break;
2070 }
2071 }
2072
2073 end_of_line.to_display_point(map)
2074}
2075
2076pub(crate) fn start_of_line(
2077 map: &DisplaySnapshot,
2078 display_lines: bool,
2079 point: DisplayPoint,
2080) -> DisplayPoint {
2081 if display_lines {
2082 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2083 } else {
2084 map.prev_line_boundary(point.to_point(map)).1
2085 }
2086}
2087
2088pub(crate) fn middle_of_line(
2089 map: &DisplaySnapshot,
2090 display_lines: bool,
2091 point: DisplayPoint,
2092 times: Option<usize>,
2093) -> DisplayPoint {
2094 let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2095 times as f64 / 100.
2096 } else {
2097 0.5
2098 };
2099 if display_lines {
2100 map.clip_point(
2101 DisplayPoint::new(
2102 point.row(),
2103 (map.line_len(point.row()) as f64 * percent) as u32,
2104 ),
2105 Bias::Left,
2106 )
2107 } else {
2108 let mut buffer_point = point.to_point(map);
2109 buffer_point.column = (map
2110 .buffer_snapshot
2111 .line_len(MultiBufferRow(buffer_point.row)) as f64
2112 * percent) as u32;
2113
2114 map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2115 }
2116}
2117
2118pub(crate) fn end_of_line(
2119 map: &DisplaySnapshot,
2120 display_lines: bool,
2121 mut point: DisplayPoint,
2122 times: usize,
2123) -> DisplayPoint {
2124 if times > 1 {
2125 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2126 }
2127 if display_lines {
2128 map.clip_point(
2129 DisplayPoint::new(point.row(), map.line_len(point.row())),
2130 Bias::Left,
2131 )
2132 } else {
2133 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2134 }
2135}
2136
2137pub(crate) fn sentence_backwards(
2138 map: &DisplaySnapshot,
2139 point: DisplayPoint,
2140 mut times: usize,
2141) -> DisplayPoint {
2142 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
2143 let mut chars = map.reverse_buffer_chars_at(start).peekable();
2144
2145 let mut was_newline = map
2146 .buffer_chars_at(start)
2147 .next()
2148 .is_some_and(|(c, _)| c == '\n');
2149
2150 while let Some((ch, offset)) = chars.next() {
2151 let start_of_next_sentence = if was_newline && ch == '\n' {
2152 Some(offset + ch.len_utf8())
2153 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2154 Some(next_non_blank(map, offset + ch.len_utf8()))
2155 } else if ch == '.' || ch == '?' || ch == '!' {
2156 start_of_next_sentence(map, offset + ch.len_utf8())
2157 } else {
2158 None
2159 };
2160
2161 if let Some(start_of_next_sentence) = start_of_next_sentence {
2162 if start_of_next_sentence < start {
2163 times = times.saturating_sub(1);
2164 }
2165 if times == 0 || offset == 0 {
2166 return map.clip_point(
2167 start_of_next_sentence
2168 .to_offset(&map.buffer_snapshot)
2169 .to_display_point(map),
2170 Bias::Left,
2171 );
2172 }
2173 }
2174 if was_newline {
2175 start = offset;
2176 }
2177 was_newline = ch == '\n';
2178 }
2179
2180 DisplayPoint::zero()
2181}
2182
2183pub(crate) fn sentence_forwards(
2184 map: &DisplaySnapshot,
2185 point: DisplayPoint,
2186 mut times: usize,
2187) -> DisplayPoint {
2188 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
2189 let mut chars = map.buffer_chars_at(start).peekable();
2190
2191 let mut was_newline = map
2192 .reverse_buffer_chars_at(start)
2193 .next()
2194 .is_some_and(|(c, _)| c == '\n')
2195 && chars.peek().is_some_and(|(c, _)| *c == '\n');
2196
2197 while let Some((ch, offset)) = chars.next() {
2198 if was_newline && ch == '\n' {
2199 continue;
2200 }
2201 let start_of_next_sentence = if was_newline {
2202 Some(next_non_blank(map, offset))
2203 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2204 Some(next_non_blank(map, offset + ch.len_utf8()))
2205 } else if ch == '.' || ch == '?' || ch == '!' {
2206 start_of_next_sentence(map, offset + ch.len_utf8())
2207 } else {
2208 None
2209 };
2210
2211 if let Some(start_of_next_sentence) = start_of_next_sentence {
2212 times = times.saturating_sub(1);
2213 if times == 0 {
2214 return map.clip_point(
2215 start_of_next_sentence
2216 .to_offset(&map.buffer_snapshot)
2217 .to_display_point(map),
2218 Bias::Right,
2219 );
2220 }
2221 }
2222
2223 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2224 }
2225
2226 map.max_point()
2227}
2228
2229fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
2230 for (c, o) in map.buffer_chars_at(start) {
2231 if c == '\n' || !c.is_whitespace() {
2232 return o;
2233 }
2234 }
2235
2236 map.buffer_snapshot.len()
2237}
2238
2239// given the offset after a ., !, or ? find the start of the next sentence.
2240// if this is not a sentence boundary, returns None.
2241fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
2242 let chars = map.buffer_chars_at(end_of_sentence);
2243 let mut seen_space = false;
2244
2245 for (char, offset) in chars {
2246 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2247 continue;
2248 }
2249
2250 if char == '\n' && seen_space {
2251 return Some(offset);
2252 } else if char.is_whitespace() {
2253 seen_space = true;
2254 } else if seen_space {
2255 return Some(offset);
2256 } else {
2257 return None;
2258 }
2259 }
2260
2261 Some(map.buffer_snapshot.len())
2262}
2263
2264fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2265 let point = map.display_point_to_point(display_point, Bias::Left);
2266 let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2267 return display_point;
2268 };
2269 let offset = excerpt.buffer().point_to_offset(
2270 excerpt
2271 .buffer()
2272 .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2273 );
2274 let buffer_range = excerpt.buffer_range();
2275 if offset >= buffer_range.start && offset <= buffer_range.end {
2276 let point = map
2277 .buffer_snapshot
2278 .offset_to_point(excerpt.map_offset_from_buffer(offset));
2279 return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2280 }
2281 let mut last_position = None;
2282 for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2283 let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer)
2284 ..language::ToOffset::to_offset(&range.context.end, buffer);
2285 if offset >= excerpt_range.start && offset <= excerpt_range.end {
2286 let text_anchor = buffer.anchor_after(offset);
2287 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2288 return anchor.to_display_point(map);
2289 } else if offset <= excerpt_range.start {
2290 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2291 return anchor.to_display_point(map);
2292 } else {
2293 last_position = Some(Anchor::in_buffer(
2294 excerpt,
2295 buffer.remote_id(),
2296 range.context.end,
2297 ));
2298 }
2299 }
2300
2301 let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2302 last_point.column = point.column;
2303
2304 map.clip_point(
2305 map.point_to_display_point(
2306 map.buffer_snapshot.clip_point(point, Bias::Left),
2307 Bias::Left,
2308 ),
2309 Bias::Left,
2310 )
2311}
2312
2313fn start_of_document(
2314 map: &DisplaySnapshot,
2315 display_point: DisplayPoint,
2316 maybe_times: Option<usize>,
2317) -> DisplayPoint {
2318 if let Some(times) = maybe_times {
2319 return go_to_line(map, display_point, times);
2320 }
2321
2322 let point = map.display_point_to_point(display_point, Bias::Left);
2323 let mut first_point = Point::zero();
2324 first_point.column = point.column;
2325
2326 map.clip_point(
2327 map.point_to_display_point(
2328 map.buffer_snapshot.clip_point(first_point, Bias::Left),
2329 Bias::Left,
2330 ),
2331 Bias::Left,
2332 )
2333}
2334
2335fn end_of_document(
2336 map: &DisplaySnapshot,
2337 display_point: DisplayPoint,
2338 maybe_times: Option<usize>,
2339) -> DisplayPoint {
2340 if let Some(times) = maybe_times {
2341 return go_to_line(map, display_point, times);
2342 };
2343 let point = map.display_point_to_point(display_point, Bias::Left);
2344 let mut last_point = map.buffer_snapshot.max_point();
2345 last_point.column = point.column;
2346
2347 map.clip_point(
2348 map.point_to_display_point(
2349 map.buffer_snapshot.clip_point(last_point, Bias::Left),
2350 Bias::Left,
2351 ),
2352 Bias::Left,
2353 )
2354}
2355
2356fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2357 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2358 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2359
2360 if head > outer.start && head < inner.start {
2361 let mut offset = inner.end.to_offset(map, Bias::Left);
2362 for c in map.buffer_snapshot.chars_at(offset) {
2363 if c == '/' || c == '\n' || c == '>' {
2364 return Some(offset.to_display_point(map));
2365 }
2366 offset += c.len_utf8();
2367 }
2368 } else {
2369 let mut offset = outer.start.to_offset(map, Bias::Left);
2370 for c in map.buffer_snapshot.chars_at(offset) {
2371 offset += c.len_utf8();
2372 if c == '<' || c == '\n' {
2373 return Some(offset.to_display_point(map));
2374 }
2375 }
2376 }
2377
2378 None
2379}
2380
2381fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2382 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2383 let display_point = map.clip_at_line_end(display_point);
2384 let point = display_point.to_point(map);
2385 let offset = point.to_offset(&map.buffer_snapshot);
2386
2387 // Ensure the range is contained by the current line.
2388 let mut line_end = map.next_line_boundary(point).0;
2389 if line_end == point {
2390 line_end = map.max_point().to_point(map);
2391 }
2392
2393 if let Some((opening_range, closing_range)) = map
2394 .buffer_snapshot
2395 .innermost_enclosing_bracket_ranges(offset..offset, None)
2396 {
2397 if opening_range.contains(&offset) {
2398 return closing_range.start.to_display_point(map);
2399 } else if closing_range.contains(&offset) {
2400 return opening_range.start.to_display_point(map);
2401 }
2402 }
2403
2404 let line_range = map.prev_line_boundary(point).0..line_end;
2405 let visible_line_range =
2406 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2407 let ranges = map
2408 .buffer_snapshot
2409 .bracket_ranges(visible_line_range.clone());
2410 if let Some(ranges) = ranges {
2411 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2412 ..line_range.end.to_offset(&map.buffer_snapshot);
2413 let mut closest_pair_destination = None;
2414 let mut closest_distance = usize::MAX;
2415
2416 for (open_range, close_range) in ranges {
2417 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2418 if offset > open_range.start && offset < close_range.start {
2419 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2420 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2421 return display_point;
2422 }
2423 if let Some(tag) = matching_tag(map, display_point) {
2424 return tag;
2425 }
2426 } else if close_range.contains(&offset) {
2427 return open_range.start.to_display_point(map);
2428 } else if open_range.contains(&offset) {
2429 return (close_range.end - 1).to_display_point(map);
2430 }
2431 }
2432
2433 if (open_range.contains(&offset) || open_range.start >= offset)
2434 && line_range.contains(&open_range.start)
2435 {
2436 let distance = open_range.start.saturating_sub(offset);
2437 if distance < closest_distance {
2438 closest_pair_destination = Some(close_range.start);
2439 closest_distance = distance;
2440 continue;
2441 }
2442 }
2443
2444 if (close_range.contains(&offset) || close_range.start >= offset)
2445 && line_range.contains(&close_range.start)
2446 {
2447 let distance = close_range.start.saturating_sub(offset);
2448 if distance < closest_distance {
2449 closest_pair_destination = Some(open_range.start);
2450 closest_distance = distance;
2451 continue;
2452 }
2453 }
2454
2455 continue;
2456 }
2457
2458 closest_pair_destination
2459 .map(|destination| destination.to_display_point(map))
2460 .unwrap_or(display_point)
2461 } else {
2462 display_point
2463 }
2464}
2465
2466// Go to {count} percentage in the file, on the first
2467// non-blank in the line linewise. To compute the new
2468// line number this formula is used:
2469// ({count} * number-of-lines + 99) / 100
2470//
2471// https://neovim.io/doc/user/motion.html#N%25
2472fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2473 let total_lines = map.buffer_snapshot.max_point().row + 1;
2474 let target_line = (count * total_lines as usize).div_ceil(100);
2475 let target_point = DisplayPoint::new(
2476 DisplayRow(target_line.saturating_sub(1) as u32),
2477 point.column(),
2478 );
2479 map.clip_point(target_point, Bias::Left)
2480}
2481
2482fn unmatched_forward(
2483 map: &DisplaySnapshot,
2484 mut display_point: DisplayPoint,
2485 char: char,
2486 times: usize,
2487) -> DisplayPoint {
2488 for _ in 0..times {
2489 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2490 let point = display_point.to_point(map);
2491 let offset = point.to_offset(&map.buffer_snapshot);
2492
2493 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2494 let Some(ranges) = ranges else { break };
2495 let mut closest_closing_destination = None;
2496 let mut closest_distance = usize::MAX;
2497
2498 for (_, close_range) in ranges {
2499 if close_range.start > offset {
2500 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2501 if Some(char) == chars.next() {
2502 let distance = close_range.start - offset;
2503 if distance < closest_distance {
2504 closest_closing_destination = Some(close_range.start);
2505 closest_distance = distance;
2506 continue;
2507 }
2508 }
2509 }
2510 }
2511
2512 let new_point = closest_closing_destination
2513 .map(|destination| destination.to_display_point(map))
2514 .unwrap_or(display_point);
2515 if new_point == display_point {
2516 break;
2517 }
2518 display_point = new_point;
2519 }
2520 display_point
2521}
2522
2523fn unmatched_backward(
2524 map: &DisplaySnapshot,
2525 mut display_point: DisplayPoint,
2526 char: char,
2527 times: usize,
2528) -> DisplayPoint {
2529 for _ in 0..times {
2530 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2531 let point = display_point.to_point(map);
2532 let offset = point.to_offset(&map.buffer_snapshot);
2533
2534 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2535 let Some(ranges) = ranges else {
2536 break;
2537 };
2538
2539 let mut closest_starting_destination = None;
2540 let mut closest_distance = usize::MAX;
2541
2542 for (start_range, _) in ranges {
2543 if start_range.start < offset {
2544 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2545 if Some(char) == chars.next() {
2546 let distance = offset - start_range.start;
2547 if distance < closest_distance {
2548 closest_starting_destination = Some(start_range.start);
2549 closest_distance = distance;
2550 continue;
2551 }
2552 }
2553 }
2554 }
2555
2556 let new_point = closest_starting_destination
2557 .map(|destination| destination.to_display_point(map))
2558 .unwrap_or(display_point);
2559 if new_point == display_point {
2560 break;
2561 } else {
2562 display_point = new_point;
2563 }
2564 }
2565 display_point
2566}
2567
2568fn find_forward(
2569 map: &DisplaySnapshot,
2570 from: DisplayPoint,
2571 before: bool,
2572 target: char,
2573 times: usize,
2574 mode: FindRange,
2575 smartcase: bool,
2576) -> Option<DisplayPoint> {
2577 let mut to = from;
2578 let mut found = false;
2579
2580 for _ in 0..times {
2581 found = false;
2582 let new_to = find_boundary(map, to, mode, |_, right| {
2583 found = is_character_match(target, right, smartcase);
2584 found
2585 });
2586 if to == new_to {
2587 break;
2588 }
2589 to = new_to;
2590 }
2591
2592 if found {
2593 if before && to.column() > 0 {
2594 *to.column_mut() -= 1;
2595 Some(map.clip_point(to, Bias::Left))
2596 } else if before && to.row().0 > 0 {
2597 *to.row_mut() -= 1;
2598 *to.column_mut() = map.line(to.row()).len() as u32;
2599 Some(map.clip_point(to, Bias::Left))
2600 } else {
2601 Some(to)
2602 }
2603 } else {
2604 None
2605 }
2606}
2607
2608fn find_backward(
2609 map: &DisplaySnapshot,
2610 from: DisplayPoint,
2611 after: bool,
2612 target: char,
2613 times: usize,
2614 mode: FindRange,
2615 smartcase: bool,
2616) -> DisplayPoint {
2617 let mut to = from;
2618
2619 for _ in 0..times {
2620 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2621 is_character_match(target, right, smartcase)
2622 });
2623 if to == new_to {
2624 break;
2625 }
2626 to = new_to;
2627 }
2628
2629 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2630 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2631 if after {
2632 *to.column_mut() += 1;
2633 map.clip_point(to, Bias::Right)
2634 } else {
2635 to
2636 }
2637 } else {
2638 from
2639 }
2640}
2641
2642/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2643pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2644 if smartcase {
2645 if target.is_uppercase() {
2646 target == other
2647 } else {
2648 target == other.to_ascii_lowercase()
2649 }
2650 } else {
2651 target == other
2652 }
2653}
2654
2655fn sneak(
2656 map: &DisplaySnapshot,
2657 from: DisplayPoint,
2658 first_target: char,
2659 second_target: char,
2660 times: usize,
2661 smartcase: bool,
2662) -> Option<DisplayPoint> {
2663 let mut to = from;
2664 let mut found = false;
2665
2666 for _ in 0..times {
2667 found = false;
2668 let new_to = find_boundary(
2669 map,
2670 movement::right(map, to),
2671 FindRange::MultiLine,
2672 |left, right| {
2673 found = is_character_match(first_target, left, smartcase)
2674 && is_character_match(second_target, right, smartcase);
2675 found
2676 },
2677 );
2678 if to == new_to {
2679 break;
2680 }
2681 to = new_to;
2682 }
2683
2684 if found {
2685 Some(movement::left(map, to))
2686 } else {
2687 None
2688 }
2689}
2690
2691fn sneak_backward(
2692 map: &DisplaySnapshot,
2693 from: DisplayPoint,
2694 first_target: char,
2695 second_target: char,
2696 times: usize,
2697 smartcase: bool,
2698) -> Option<DisplayPoint> {
2699 let mut to = from;
2700 let mut found = false;
2701
2702 for _ in 0..times {
2703 found = false;
2704 let new_to =
2705 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2706 found = is_character_match(first_target, left, smartcase)
2707 && is_character_match(second_target, right, smartcase);
2708 found
2709 });
2710 if to == new_to {
2711 break;
2712 }
2713 to = new_to;
2714 }
2715
2716 if found {
2717 Some(movement::left(map, to))
2718 } else {
2719 None
2720 }
2721}
2722
2723fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2724 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2725 first_non_whitespace(map, false, correct_line)
2726}
2727
2728fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2729 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2730 first_non_whitespace(map, false, correct_line)
2731}
2732
2733fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2734 let correct_line = start_of_relative_buffer_row(map, point, 0);
2735 right(map, correct_line, times.saturating_sub(1))
2736}
2737
2738pub(crate) fn next_line_end(
2739 map: &DisplaySnapshot,
2740 mut point: DisplayPoint,
2741 times: usize,
2742) -> DisplayPoint {
2743 if times > 1 {
2744 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2745 }
2746 end_of_line(map, false, point, 1)
2747}
2748
2749fn window_top(
2750 map: &DisplaySnapshot,
2751 point: DisplayPoint,
2752 text_layout_details: &TextLayoutDetails,
2753 mut times: usize,
2754) -> (DisplayPoint, SelectionGoal) {
2755 let first_visible_line = text_layout_details
2756 .scroll_anchor
2757 .anchor
2758 .to_display_point(map);
2759
2760 if first_visible_line.row() != DisplayRow(0)
2761 && text_layout_details.vertical_scroll_margin as usize > times
2762 {
2763 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2764 }
2765
2766 if let Some(visible_rows) = text_layout_details.visible_rows {
2767 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2768 let new_row = (first_visible_line.row().0 + (times as u32))
2769 .min(bottom_row)
2770 .min(map.max_point().row().0);
2771 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2772
2773 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2774 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2775 } else {
2776 let new_row =
2777 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2778 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2779
2780 let new_point = DisplayPoint::new(new_row, new_col);
2781 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2782 }
2783}
2784
2785fn window_middle(
2786 map: &DisplaySnapshot,
2787 point: DisplayPoint,
2788 text_layout_details: &TextLayoutDetails,
2789) -> (DisplayPoint, SelectionGoal) {
2790 if let Some(visible_rows) = text_layout_details.visible_rows {
2791 let first_visible_line = text_layout_details
2792 .scroll_anchor
2793 .anchor
2794 .to_display_point(map);
2795
2796 let max_visible_rows =
2797 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2798
2799 let new_row =
2800 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2801 let new_row = DisplayRow(new_row);
2802 let new_col = point.column().min(map.line_len(new_row));
2803 let new_point = DisplayPoint::new(new_row, new_col);
2804 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2805 } else {
2806 (point, SelectionGoal::None)
2807 }
2808}
2809
2810fn window_bottom(
2811 map: &DisplaySnapshot,
2812 point: DisplayPoint,
2813 text_layout_details: &TextLayoutDetails,
2814 mut times: usize,
2815) -> (DisplayPoint, SelectionGoal) {
2816 if let Some(visible_rows) = text_layout_details.visible_rows {
2817 let first_visible_line = text_layout_details
2818 .scroll_anchor
2819 .anchor
2820 .to_display_point(map);
2821 let bottom_row = first_visible_line.row().0
2822 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2823 if bottom_row < map.max_point().row().0
2824 && text_layout_details.vertical_scroll_margin as usize > times
2825 {
2826 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2827 }
2828 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2829 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2830 {
2831 first_visible_line.row()
2832 } else {
2833 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2834 };
2835 let new_col = point.column().min(map.line_len(new_row));
2836 let new_point = DisplayPoint::new(new_row, new_col);
2837 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2838 } else {
2839 (point, SelectionGoal::None)
2840 }
2841}
2842
2843fn method_motion(
2844 map: &DisplaySnapshot,
2845 mut display_point: DisplayPoint,
2846 times: usize,
2847 direction: Direction,
2848 is_start: bool,
2849) -> DisplayPoint {
2850 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2851 return display_point;
2852 };
2853
2854 for _ in 0..times {
2855 let point = map.display_point_to_point(display_point, Bias::Left);
2856 let offset = point.to_offset(&map.buffer_snapshot);
2857 let range = if direction == Direction::Prev {
2858 0..offset
2859 } else {
2860 offset..buffer.len()
2861 };
2862
2863 let possibilities = buffer
2864 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2865 .filter_map(|(range, object)| {
2866 if !matches!(object, language::TextObject::AroundFunction) {
2867 return None;
2868 }
2869
2870 let relevant = if is_start { range.start } else { range.end };
2871 if direction == Direction::Prev && relevant < offset {
2872 Some(relevant)
2873 } else if direction == Direction::Next && relevant > offset + 1 {
2874 Some(relevant)
2875 } else {
2876 None
2877 }
2878 });
2879
2880 let dest = if direction == Direction::Prev {
2881 possibilities.max().unwrap_or(offset)
2882 } else {
2883 possibilities.min().unwrap_or(offset)
2884 };
2885 let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2886 if new_point == display_point {
2887 break;
2888 }
2889 display_point = new_point;
2890 }
2891 display_point
2892}
2893
2894fn comment_motion(
2895 map: &DisplaySnapshot,
2896 mut display_point: DisplayPoint,
2897 times: usize,
2898 direction: Direction,
2899) -> DisplayPoint {
2900 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2901 return display_point;
2902 };
2903
2904 for _ in 0..times {
2905 let point = map.display_point_to_point(display_point, Bias::Left);
2906 let offset = point.to_offset(&map.buffer_snapshot);
2907 let range = if direction == Direction::Prev {
2908 0..offset
2909 } else {
2910 offset..buffer.len()
2911 };
2912
2913 let possibilities = buffer
2914 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2915 .filter_map(|(range, object)| {
2916 if !matches!(object, language::TextObject::AroundComment) {
2917 return None;
2918 }
2919
2920 let relevant = if direction == Direction::Prev {
2921 range.start
2922 } else {
2923 range.end
2924 };
2925 if direction == Direction::Prev && relevant < offset {
2926 Some(relevant)
2927 } else if direction == Direction::Next && relevant > offset + 1 {
2928 Some(relevant)
2929 } else {
2930 None
2931 }
2932 });
2933
2934 let dest = if direction == Direction::Prev {
2935 possibilities.max().unwrap_or(offset)
2936 } else {
2937 possibilities.min().unwrap_or(offset)
2938 };
2939 let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
2940 if new_point == display_point {
2941 break;
2942 }
2943 display_point = new_point;
2944 }
2945
2946 display_point
2947}
2948
2949fn section_motion(
2950 map: &DisplaySnapshot,
2951 mut display_point: DisplayPoint,
2952 times: usize,
2953 direction: Direction,
2954 is_start: bool,
2955) -> DisplayPoint {
2956 if map.buffer_snapshot.as_singleton().is_some() {
2957 for _ in 0..times {
2958 let offset = map
2959 .display_point_to_point(display_point, Bias::Left)
2960 .to_offset(&map.buffer_snapshot);
2961 let range = if direction == Direction::Prev {
2962 0..offset
2963 } else {
2964 offset..map.buffer_snapshot.len()
2965 };
2966
2967 // we set a max start depth here because we want a section to only be "top level"
2968 // similar to vim's default of '{' in the first column.
2969 // (and without it, ]] at the start of editor.rs is -very- slow)
2970 let mut possibilities = map
2971 .buffer_snapshot
2972 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2973 .filter(|(_, object)| {
2974 matches!(
2975 object,
2976 language::TextObject::AroundClass | language::TextObject::AroundFunction
2977 )
2978 })
2979 .collect::<Vec<_>>();
2980 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2981 let mut prev_end = None;
2982 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2983 if t == language::TextObject::AroundFunction
2984 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2985 {
2986 return None;
2987 }
2988 prev_end = Some(range.end);
2989
2990 let relevant = if is_start { range.start } else { range.end };
2991 if direction == Direction::Prev && relevant < offset {
2992 Some(relevant)
2993 } else if direction == Direction::Next && relevant > offset + 1 {
2994 Some(relevant)
2995 } else {
2996 None
2997 }
2998 });
2999
3000 let offset = if direction == Direction::Prev {
3001 possibilities.max().unwrap_or(0)
3002 } else {
3003 possibilities.min().unwrap_or(map.buffer_snapshot.len())
3004 };
3005
3006 let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3007 if new_point == display_point {
3008 break;
3009 }
3010 display_point = new_point;
3011 }
3012 return display_point;
3013 };
3014
3015 for _ in 0..times {
3016 let next_point = if is_start {
3017 movement::start_of_excerpt(map, display_point, direction)
3018 } else {
3019 movement::end_of_excerpt(map, display_point, direction)
3020 };
3021 if next_point == display_point {
3022 break;
3023 }
3024 display_point = next_point;
3025 }
3026
3027 display_point
3028}
3029
3030fn matches_indent_type(
3031 target_indent: &text::LineIndent,
3032 current_indent: &text::LineIndent,
3033 indent_type: IndentType,
3034) -> bool {
3035 match indent_type {
3036 IndentType::Lesser => {
3037 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3038 }
3039 IndentType::Greater => {
3040 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3041 }
3042 IndentType::Same => {
3043 target_indent.spaces == current_indent.spaces
3044 && target_indent.tabs == current_indent.tabs
3045 }
3046 }
3047}
3048
3049fn indent_motion(
3050 map: &DisplaySnapshot,
3051 mut display_point: DisplayPoint,
3052 times: usize,
3053 direction: Direction,
3054 indent_type: IndentType,
3055) -> DisplayPoint {
3056 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3057 let current_row = MultiBufferRow(buffer_point.row);
3058 let current_indent = map.line_indent_for_buffer_row(current_row);
3059 if current_indent.is_line_empty() {
3060 return display_point;
3061 }
3062 let max_row = map.max_point().to_point(map).row;
3063
3064 for _ in 0..times {
3065 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3066
3067 let target_row = match direction {
3068 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3069 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3070 !indent.is_line_empty()
3071 && matches_indent_type(&indent, ¤t_indent, indent_type)
3072 }),
3073 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3074 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3075 !indent.is_line_empty()
3076 && matches_indent_type(&indent, ¤t_indent, indent_type)
3077 }),
3078 }
3079 .unwrap_or(current_buffer_row);
3080
3081 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3082 let new_point = first_non_whitespace(map, false, new_point);
3083 if new_point == display_point {
3084 break;
3085 }
3086 display_point = new_point;
3087 }
3088 display_point
3089}
3090
3091#[cfg(test)]
3092mod test {
3093
3094 use crate::{
3095 state::Mode,
3096 test::{NeovimBackedTestContext, VimTestContext},
3097 };
3098 use editor::display_map::Inlay;
3099 use indoc::indoc;
3100 use language::Point;
3101 use multi_buffer::MultiBufferRow;
3102
3103 #[gpui::test]
3104 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3105 let mut cx = NeovimBackedTestContext::new(cx).await;
3106
3107 let initial_state = indoc! {r"ˇabc
3108 def
3109
3110 paragraph
3111 the second
3112
3113
3114
3115 third and
3116 final"};
3117
3118 // goes down once
3119 cx.set_shared_state(initial_state).await;
3120 cx.simulate_shared_keystrokes("}").await;
3121 cx.shared_state().await.assert_eq(indoc! {r"abc
3122 def
3123 ˇ
3124 paragraph
3125 the second
3126
3127
3128
3129 third and
3130 final"});
3131
3132 // goes up once
3133 cx.simulate_shared_keystrokes("{").await;
3134 cx.shared_state().await.assert_eq(initial_state);
3135
3136 // goes down twice
3137 cx.simulate_shared_keystrokes("2 }").await;
3138 cx.shared_state().await.assert_eq(indoc! {r"abc
3139 def
3140
3141 paragraph
3142 the second
3143 ˇ
3144
3145
3146 third and
3147 final"});
3148
3149 // goes down over multiple blanks
3150 cx.simulate_shared_keystrokes("}").await;
3151 cx.shared_state().await.assert_eq(indoc! {r"abc
3152 def
3153
3154 paragraph
3155 the second
3156
3157
3158
3159 third and
3160 finaˇl"});
3161
3162 // goes up twice
3163 cx.simulate_shared_keystrokes("2 {").await;
3164 cx.shared_state().await.assert_eq(indoc! {r"abc
3165 def
3166 ˇ
3167 paragraph
3168 the second
3169
3170
3171
3172 third and
3173 final"});
3174 }
3175
3176 #[gpui::test]
3177 async fn test_matching(cx: &mut gpui::TestAppContext) {
3178 let mut cx = NeovimBackedTestContext::new(cx).await;
3179
3180 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3181 do(something(with<Types>.and_arrays[0, 2]))
3182 }"})
3183 .await;
3184 cx.simulate_shared_keystrokes("%").await;
3185 cx.shared_state()
3186 .await
3187 .assert_eq(indoc! {r"func (a stringˇ) {
3188 do(something(with<Types>.and_arrays[0, 2]))
3189 }"});
3190
3191 // test it works on the last character of the line
3192 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3193 do(something(with<Types>.and_arrays[0, 2]))
3194 }"})
3195 .await;
3196 cx.simulate_shared_keystrokes("%").await;
3197 cx.shared_state()
3198 .await
3199 .assert_eq(indoc! {r"func (a string) {
3200 do(something(with<Types>.and_arrays[0, 2]))
3201 ˇ}"});
3202
3203 // test it works on immediate nesting
3204 cx.set_shared_state("ˇ{()}").await;
3205 cx.simulate_shared_keystrokes("%").await;
3206 cx.shared_state().await.assert_eq("{()ˇ}");
3207 cx.simulate_shared_keystrokes("%").await;
3208 cx.shared_state().await.assert_eq("ˇ{()}");
3209
3210 // test it works on immediate nesting inside braces
3211 cx.set_shared_state("{\n ˇ{()}\n}").await;
3212 cx.simulate_shared_keystrokes("%").await;
3213 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3214
3215 // test it jumps to the next paren on a line
3216 cx.set_shared_state("func ˇboop() {\n}").await;
3217 cx.simulate_shared_keystrokes("%").await;
3218 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3219 }
3220
3221 #[gpui::test]
3222 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3223 let mut cx = NeovimBackedTestContext::new(cx).await;
3224
3225 // test it works with curly braces
3226 cx.set_shared_state(indoc! {r"func (a string) {
3227 do(something(with<Types>.anˇd_arrays[0, 2]))
3228 }"})
3229 .await;
3230 cx.simulate_shared_keystrokes("] }").await;
3231 cx.shared_state()
3232 .await
3233 .assert_eq(indoc! {r"func (a string) {
3234 do(something(with<Types>.and_arrays[0, 2]))
3235 ˇ}"});
3236
3237 // test it works with brackets
3238 cx.set_shared_state(indoc! {r"func (a string) {
3239 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3240 }"})
3241 .await;
3242 cx.simulate_shared_keystrokes("] )").await;
3243 cx.shared_state()
3244 .await
3245 .assert_eq(indoc! {r"func (a string) {
3246 do(something(with<Types>.and_arrays[0, 2])ˇ)
3247 }"});
3248
3249 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3250 .await;
3251 cx.simulate_shared_keystrokes("] )").await;
3252 cx.shared_state()
3253 .await
3254 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3255
3256 // test it works on immediate nesting
3257 cx.set_shared_state("{ˇ {}{}}").await;
3258 cx.simulate_shared_keystrokes("] }").await;
3259 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3260 cx.set_shared_state("(ˇ ()())").await;
3261 cx.simulate_shared_keystrokes("] )").await;
3262 cx.shared_state().await.assert_eq("( ()()ˇ)");
3263
3264 // test it works on immediate nesting inside braces
3265 cx.set_shared_state("{\n ˇ {()}\n}").await;
3266 cx.simulate_shared_keystrokes("] }").await;
3267 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3268 cx.set_shared_state("(\n ˇ {()}\n)").await;
3269 cx.simulate_shared_keystrokes("] )").await;
3270 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3271 }
3272
3273 #[gpui::test]
3274 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3275 let mut cx = NeovimBackedTestContext::new(cx).await;
3276
3277 // test it works with curly braces
3278 cx.set_shared_state(indoc! {r"func (a string) {
3279 do(something(with<Types>.anˇd_arrays[0, 2]))
3280 }"})
3281 .await;
3282 cx.simulate_shared_keystrokes("[ {").await;
3283 cx.shared_state()
3284 .await
3285 .assert_eq(indoc! {r"func (a string) ˇ{
3286 do(something(with<Types>.and_arrays[0, 2]))
3287 }"});
3288
3289 // test it works with brackets
3290 cx.set_shared_state(indoc! {r"func (a string) {
3291 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3292 }"})
3293 .await;
3294 cx.simulate_shared_keystrokes("[ (").await;
3295 cx.shared_state()
3296 .await
3297 .assert_eq(indoc! {r"func (a string) {
3298 doˇ(something(with<Types>.and_arrays[0, 2]))
3299 }"});
3300
3301 // test it works on immediate nesting
3302 cx.set_shared_state("{{}{} ˇ }").await;
3303 cx.simulate_shared_keystrokes("[ {").await;
3304 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3305 cx.set_shared_state("(()() ˇ )").await;
3306 cx.simulate_shared_keystrokes("[ (").await;
3307 cx.shared_state().await.assert_eq("ˇ(()() )");
3308
3309 // test it works on immediate nesting inside braces
3310 cx.set_shared_state("{\n {()} ˇ\n}").await;
3311 cx.simulate_shared_keystrokes("[ {").await;
3312 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3313 cx.set_shared_state("(\n {()} ˇ\n)").await;
3314 cx.simulate_shared_keystrokes("[ (").await;
3315 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3316 }
3317
3318 #[gpui::test]
3319 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3320 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3321
3322 cx.neovim.exec("set filetype=html").await;
3323
3324 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3325 cx.simulate_shared_keystrokes("%").await;
3326 cx.shared_state()
3327 .await
3328 .assert_eq(indoc! {r"<body><ˇ/body>"});
3329 cx.simulate_shared_keystrokes("%").await;
3330
3331 // test jumping backwards
3332 cx.shared_state()
3333 .await
3334 .assert_eq(indoc! {r"<ˇbody></body>"});
3335
3336 // test self-closing tags
3337 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3338 cx.simulate_shared_keystrokes("%").await;
3339 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3340
3341 // test tag with attributes
3342 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3343 </div>
3344 "})
3345 .await;
3346 cx.simulate_shared_keystrokes("%").await;
3347 cx.shared_state()
3348 .await
3349 .assert_eq(indoc! {r"<div class='test' id='main'>
3350 <ˇ/div>
3351 "});
3352
3353 // test multi-line self-closing tag
3354 cx.set_shared_state(indoc! {r#"<a>
3355 <br
3356 test = "test"
3357 /ˇ>
3358 </a>"#})
3359 .await;
3360 cx.simulate_shared_keystrokes("%").await;
3361 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3362 ˇ<br
3363 test = "test"
3364 />
3365 </a>"#});
3366 }
3367
3368 #[gpui::test]
3369 async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3370 let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3371
3372 // test brackets within tags
3373 cx.set_shared_state(indoc! {r"function f() {
3374 return (
3375 <div rules={ˇ[{ a: 1 }]}>
3376 <h1>test</h1>
3377 </div>
3378 );
3379 }"})
3380 .await;
3381 cx.simulate_shared_keystrokes("%").await;
3382 cx.shared_state().await.assert_eq(indoc! {r"function f() {
3383 return (
3384 <div rules={[{ a: 1 }ˇ]}>
3385 <h1>test</h1>
3386 </div>
3387 );
3388 }"});
3389 }
3390
3391 #[gpui::test]
3392 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3393 let mut cx = NeovimBackedTestContext::new(cx).await;
3394
3395 // f and F
3396 cx.set_shared_state("ˇone two three four").await;
3397 cx.simulate_shared_keystrokes("f o").await;
3398 cx.shared_state().await.assert_eq("one twˇo three four");
3399 cx.simulate_shared_keystrokes(",").await;
3400 cx.shared_state().await.assert_eq("ˇone two three four");
3401 cx.simulate_shared_keystrokes("2 ;").await;
3402 cx.shared_state().await.assert_eq("one two three fˇour");
3403 cx.simulate_shared_keystrokes("shift-f e").await;
3404 cx.shared_state().await.assert_eq("one two threˇe four");
3405 cx.simulate_shared_keystrokes("2 ;").await;
3406 cx.shared_state().await.assert_eq("onˇe two three four");
3407 cx.simulate_shared_keystrokes(",").await;
3408 cx.shared_state().await.assert_eq("one two thrˇee four");
3409
3410 // t and T
3411 cx.set_shared_state("ˇone two three four").await;
3412 cx.simulate_shared_keystrokes("t o").await;
3413 cx.shared_state().await.assert_eq("one tˇwo three four");
3414 cx.simulate_shared_keystrokes(",").await;
3415 cx.shared_state().await.assert_eq("oˇne two three four");
3416 cx.simulate_shared_keystrokes("2 ;").await;
3417 cx.shared_state().await.assert_eq("one two three ˇfour");
3418 cx.simulate_shared_keystrokes("shift-t e").await;
3419 cx.shared_state().await.assert_eq("one two threeˇ four");
3420 cx.simulate_shared_keystrokes("3 ;").await;
3421 cx.shared_state().await.assert_eq("oneˇ two three four");
3422 cx.simulate_shared_keystrokes(",").await;
3423 cx.shared_state().await.assert_eq("one two thˇree four");
3424 }
3425
3426 #[gpui::test]
3427 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3428 let mut cx = NeovimBackedTestContext::new(cx).await;
3429 let initial_state = indoc! {r"something(ˇfoo)"};
3430 cx.set_shared_state(initial_state).await;
3431 cx.simulate_shared_keystrokes("}").await;
3432 cx.shared_state().await.assert_eq("something(fooˇ)");
3433 }
3434
3435 #[gpui::test]
3436 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3437 let mut cx = NeovimBackedTestContext::new(cx).await;
3438 cx.set_shared_state("ˇone\n two\nthree").await;
3439 cx.simulate_shared_keystrokes("enter").await;
3440 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3441 }
3442
3443 #[gpui::test]
3444 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3445 let mut cx = NeovimBackedTestContext::new(cx).await;
3446 cx.set_shared_state("ˇ one\n two \nthree").await;
3447 cx.simulate_shared_keystrokes("g _").await;
3448 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3449
3450 cx.set_shared_state("ˇ one \n two \nthree").await;
3451 cx.simulate_shared_keystrokes("g _").await;
3452 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3453 cx.simulate_shared_keystrokes("2 g _").await;
3454 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3455 }
3456
3457 #[gpui::test]
3458 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3459 let mut cx = NeovimBackedTestContext::new(cx).await;
3460 let initial_state = indoc! {r"abc
3461 def
3462 paragraph
3463 the second
3464 third ˇand
3465 final"};
3466
3467 cx.set_shared_state(initial_state).await;
3468 cx.simulate_shared_keystrokes("shift-h").await;
3469 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3470 def
3471 paragraph
3472 the second
3473 third and
3474 final"});
3475
3476 // clip point
3477 cx.set_shared_state(indoc! {r"
3478 1 2 3
3479 4 5 6
3480 7 8 ˇ9
3481 "})
3482 .await;
3483 cx.simulate_shared_keystrokes("shift-h").await;
3484 cx.shared_state().await.assert_eq(indoc! {"
3485 1 2 ˇ3
3486 4 5 6
3487 7 8 9
3488 "});
3489
3490 cx.set_shared_state(indoc! {r"
3491 1 2 3
3492 4 5 6
3493 ˇ7 8 9
3494 "})
3495 .await;
3496 cx.simulate_shared_keystrokes("shift-h").await;
3497 cx.shared_state().await.assert_eq(indoc! {"
3498 ˇ1 2 3
3499 4 5 6
3500 7 8 9
3501 "});
3502
3503 cx.set_shared_state(indoc! {r"
3504 1 2 3
3505 4 5 ˇ6
3506 7 8 9"})
3507 .await;
3508 cx.simulate_shared_keystrokes("9 shift-h").await;
3509 cx.shared_state().await.assert_eq(indoc! {"
3510 1 2 3
3511 4 5 6
3512 7 8 ˇ9"});
3513 }
3514
3515 #[gpui::test]
3516 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3517 let mut cx = NeovimBackedTestContext::new(cx).await;
3518 let initial_state = indoc! {r"abˇc
3519 def
3520 paragraph
3521 the second
3522 third and
3523 final"};
3524
3525 cx.set_shared_state(initial_state).await;
3526 cx.simulate_shared_keystrokes("shift-m").await;
3527 cx.shared_state().await.assert_eq(indoc! {r"abc
3528 def
3529 paˇragraph
3530 the second
3531 third and
3532 final"});
3533
3534 cx.set_shared_state(indoc! {r"
3535 1 2 3
3536 4 5 6
3537 7 8 ˇ9
3538 "})
3539 .await;
3540 cx.simulate_shared_keystrokes("shift-m").await;
3541 cx.shared_state().await.assert_eq(indoc! {"
3542 1 2 3
3543 4 5 ˇ6
3544 7 8 9
3545 "});
3546 cx.set_shared_state(indoc! {r"
3547 1 2 3
3548 4 5 6
3549 ˇ7 8 9
3550 "})
3551 .await;
3552 cx.simulate_shared_keystrokes("shift-m").await;
3553 cx.shared_state().await.assert_eq(indoc! {"
3554 1 2 3
3555 ˇ4 5 6
3556 7 8 9
3557 "});
3558 cx.set_shared_state(indoc! {r"
3559 ˇ1 2 3
3560 4 5 6
3561 7 8 9
3562 "})
3563 .await;
3564 cx.simulate_shared_keystrokes("shift-m").await;
3565 cx.shared_state().await.assert_eq(indoc! {"
3566 1 2 3
3567 ˇ4 5 6
3568 7 8 9
3569 "});
3570 cx.set_shared_state(indoc! {r"
3571 1 2 3
3572 ˇ4 5 6
3573 7 8 9
3574 "})
3575 .await;
3576 cx.simulate_shared_keystrokes("shift-m").await;
3577 cx.shared_state().await.assert_eq(indoc! {"
3578 1 2 3
3579 ˇ4 5 6
3580 7 8 9
3581 "});
3582 cx.set_shared_state(indoc! {r"
3583 1 2 3
3584 4 5 ˇ6
3585 7 8 9
3586 "})
3587 .await;
3588 cx.simulate_shared_keystrokes("shift-m").await;
3589 cx.shared_state().await.assert_eq(indoc! {"
3590 1 2 3
3591 4 5 ˇ6
3592 7 8 9
3593 "});
3594 }
3595
3596 #[gpui::test]
3597 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3598 let mut cx = NeovimBackedTestContext::new(cx).await;
3599 let initial_state = indoc! {r"abc
3600 deˇf
3601 paragraph
3602 the second
3603 third and
3604 final"};
3605
3606 cx.set_shared_state(initial_state).await;
3607 cx.simulate_shared_keystrokes("shift-l").await;
3608 cx.shared_state().await.assert_eq(indoc! {r"abc
3609 def
3610 paragraph
3611 the second
3612 third and
3613 fiˇnal"});
3614
3615 cx.set_shared_state(indoc! {r"
3616 1 2 3
3617 4 5 ˇ6
3618 7 8 9
3619 "})
3620 .await;
3621 cx.simulate_shared_keystrokes("shift-l").await;
3622 cx.shared_state().await.assert_eq(indoc! {"
3623 1 2 3
3624 4 5 6
3625 7 8 9
3626 ˇ"});
3627
3628 cx.set_shared_state(indoc! {r"
3629 1 2 3
3630 ˇ4 5 6
3631 7 8 9
3632 "})
3633 .await;
3634 cx.simulate_shared_keystrokes("shift-l").await;
3635 cx.shared_state().await.assert_eq(indoc! {"
3636 1 2 3
3637 4 5 6
3638 7 8 9
3639 ˇ"});
3640
3641 cx.set_shared_state(indoc! {r"
3642 1 2 ˇ3
3643 4 5 6
3644 7 8 9
3645 "})
3646 .await;
3647 cx.simulate_shared_keystrokes("shift-l").await;
3648 cx.shared_state().await.assert_eq(indoc! {"
3649 1 2 3
3650 4 5 6
3651 7 8 9
3652 ˇ"});
3653
3654 cx.set_shared_state(indoc! {r"
3655 ˇ1 2 3
3656 4 5 6
3657 7 8 9
3658 "})
3659 .await;
3660 cx.simulate_shared_keystrokes("shift-l").await;
3661 cx.shared_state().await.assert_eq(indoc! {"
3662 1 2 3
3663 4 5 6
3664 7 8 9
3665 ˇ"});
3666
3667 cx.set_shared_state(indoc! {r"
3668 1 2 3
3669 4 5 ˇ6
3670 7 8 9
3671 "})
3672 .await;
3673 cx.simulate_shared_keystrokes("9 shift-l").await;
3674 cx.shared_state().await.assert_eq(indoc! {"
3675 1 2 ˇ3
3676 4 5 6
3677 7 8 9
3678 "});
3679 }
3680
3681 #[gpui::test]
3682 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3683 let mut cx = NeovimBackedTestContext::new(cx).await;
3684 cx.set_shared_state(indoc! {r"
3685 456 5ˇ67 678
3686 "})
3687 .await;
3688 cx.simulate_shared_keystrokes("g e").await;
3689 cx.shared_state().await.assert_eq(indoc! {"
3690 45ˇ6 567 678
3691 "});
3692
3693 // Test times
3694 cx.set_shared_state(indoc! {r"
3695 123 234 345
3696 456 5ˇ67 678
3697 "})
3698 .await;
3699 cx.simulate_shared_keystrokes("4 g e").await;
3700 cx.shared_state().await.assert_eq(indoc! {"
3701 12ˇ3 234 345
3702 456 567 678
3703 "});
3704
3705 // With punctuation
3706 cx.set_shared_state(indoc! {r"
3707 123 234 345
3708 4;5.6 5ˇ67 678
3709 789 890 901
3710 "})
3711 .await;
3712 cx.simulate_shared_keystrokes("g e").await;
3713 cx.shared_state().await.assert_eq(indoc! {"
3714 123 234 345
3715 4;5.ˇ6 567 678
3716 789 890 901
3717 "});
3718
3719 // With punctuation and count
3720 cx.set_shared_state(indoc! {r"
3721 123 234 345
3722 4;5.6 5ˇ67 678
3723 789 890 901
3724 "})
3725 .await;
3726 cx.simulate_shared_keystrokes("5 g e").await;
3727 cx.shared_state().await.assert_eq(indoc! {"
3728 123 234 345
3729 ˇ4;5.6 567 678
3730 789 890 901
3731 "});
3732
3733 // newlines
3734 cx.set_shared_state(indoc! {r"
3735 123 234 345
3736
3737 78ˇ9 890 901
3738 "})
3739 .await;
3740 cx.simulate_shared_keystrokes("g e").await;
3741 cx.shared_state().await.assert_eq(indoc! {"
3742 123 234 345
3743 ˇ
3744 789 890 901
3745 "});
3746 cx.simulate_shared_keystrokes("g e").await;
3747 cx.shared_state().await.assert_eq(indoc! {"
3748 123 234 34ˇ5
3749
3750 789 890 901
3751 "});
3752
3753 // With punctuation
3754 cx.set_shared_state(indoc! {r"
3755 123 234 345
3756 4;5.ˇ6 567 678
3757 789 890 901
3758 "})
3759 .await;
3760 cx.simulate_shared_keystrokes("g shift-e").await;
3761 cx.shared_state().await.assert_eq(indoc! {"
3762 123 234 34ˇ5
3763 4;5.6 567 678
3764 789 890 901
3765 "});
3766
3767 // With multi byte char
3768 cx.set_shared_state(indoc! {r"
3769 bar ˇó
3770 "})
3771 .await;
3772 cx.simulate_shared_keystrokes("g e").await;
3773 cx.shared_state().await.assert_eq(indoc! {"
3774 baˇr ó
3775 "});
3776 }
3777
3778 #[gpui::test]
3779 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3780 let mut cx = NeovimBackedTestContext::new(cx).await;
3781
3782 cx.set_shared_state(indoc! {"
3783 fn aˇ() {
3784 return
3785 }
3786 "})
3787 .await;
3788 cx.simulate_shared_keystrokes("v $ %").await;
3789 cx.shared_state().await.assert_eq(indoc! {"
3790 fn a«() {
3791 return
3792 }ˇ»
3793 "});
3794 }
3795
3796 #[gpui::test]
3797 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3798 let mut cx = VimTestContext::new(cx, true).await;
3799
3800 cx.set_state(
3801 indoc! {"
3802 struct Foo {
3803 ˇ
3804 }
3805 "},
3806 Mode::Normal,
3807 );
3808
3809 cx.update_editor(|editor, _window, cx| {
3810 let range = editor.selections.newest_anchor().range();
3811 let inlay_text = " field: int,\n field2: string\n field3: float";
3812 let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
3813 editor.splice_inlays(&[], vec![inlay], cx);
3814 });
3815
3816 cx.simulate_keystrokes("j");
3817 cx.assert_state(
3818 indoc! {"
3819 struct Foo {
3820
3821 ˇ}
3822 "},
3823 Mode::Normal,
3824 );
3825 }
3826
3827 #[gpui::test]
3828 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3829 let mut cx = VimTestContext::new(cx, true).await;
3830
3831 cx.set_state(
3832 indoc! {"
3833 ˇstruct Foo {
3834
3835 }
3836 "},
3837 Mode::Normal,
3838 );
3839 cx.update_editor(|editor, _window, cx| {
3840 let snapshot = editor.buffer().read(cx).snapshot(cx);
3841 let end_of_line =
3842 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3843 let inlay_text = " hint";
3844 let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
3845 editor.splice_inlays(&[], vec![inlay], cx);
3846 });
3847 cx.simulate_keystrokes("$");
3848 cx.assert_state(
3849 indoc! {"
3850 struct Foo ˇ{
3851
3852 }
3853 "},
3854 Mode::Normal,
3855 );
3856 }
3857
3858 #[gpui::test]
3859 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3860 let mut cx = NeovimBackedTestContext::new(cx).await;
3861 // Normal mode
3862 cx.set_shared_state(indoc! {"
3863 The ˇquick brown
3864 fox jumps over
3865 the lazy dog
3866 The quick brown
3867 fox jumps over
3868 the lazy dog
3869 The quick brown
3870 fox jumps over
3871 the lazy dog"})
3872 .await;
3873 cx.simulate_shared_keystrokes("2 0 %").await;
3874 cx.shared_state().await.assert_eq(indoc! {"
3875 The quick brown
3876 fox ˇjumps over
3877 the lazy dog
3878 The quick brown
3879 fox jumps over
3880 the lazy dog
3881 The quick brown
3882 fox jumps over
3883 the lazy dog"});
3884
3885 cx.simulate_shared_keystrokes("2 5 %").await;
3886 cx.shared_state().await.assert_eq(indoc! {"
3887 The quick brown
3888 fox jumps over
3889 the ˇlazy dog
3890 The quick brown
3891 fox jumps over
3892 the lazy dog
3893 The quick brown
3894 fox jumps over
3895 the lazy dog"});
3896
3897 cx.simulate_shared_keystrokes("7 5 %").await;
3898 cx.shared_state().await.assert_eq(indoc! {"
3899 The quick brown
3900 fox jumps over
3901 the lazy dog
3902 The quick brown
3903 fox jumps over
3904 the lazy dog
3905 The ˇquick brown
3906 fox jumps over
3907 the lazy dog"});
3908
3909 // Visual mode
3910 cx.set_shared_state(indoc! {"
3911 The ˇquick brown
3912 fox jumps over
3913 the lazy dog
3914 The quick brown
3915 fox jumps over
3916 the lazy dog
3917 The quick brown
3918 fox jumps over
3919 the lazy dog"})
3920 .await;
3921 cx.simulate_shared_keystrokes("v 5 0 %").await;
3922 cx.shared_state().await.assert_eq(indoc! {"
3923 The «quick brown
3924 fox jumps over
3925 the lazy dog
3926 The quick brown
3927 fox jˇ»umps over
3928 the lazy dog
3929 The quick brown
3930 fox jumps over
3931 the lazy dog"});
3932
3933 cx.set_shared_state(indoc! {"
3934 The ˇquick brown
3935 fox jumps over
3936 the lazy dog
3937 The quick brown
3938 fox jumps over
3939 the lazy dog
3940 The quick brown
3941 fox jumps over
3942 the lazy dog"})
3943 .await;
3944 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3945 cx.shared_state().await.assert_eq(indoc! {"
3946 The «quick brown
3947 fox jumps over
3948 the lazy dog
3949 The quick brown
3950 fox jumps over
3951 the lazy dog
3952 The quick brown
3953 fox jumps over
3954 the lˇ»azy dog"});
3955 }
3956
3957 #[gpui::test]
3958 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3959 let mut cx = NeovimBackedTestContext::new(cx).await;
3960
3961 cx.set_shared_state("ˇπππππ").await;
3962 cx.simulate_shared_keystrokes("3 space").await;
3963 cx.shared_state().await.assert_eq("πππˇππ");
3964 }
3965
3966 #[gpui::test]
3967 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3968 let mut cx = NeovimBackedTestContext::new(cx).await;
3969
3970 cx.set_shared_state(indoc! {"
3971 ππππˇπ
3972 πanotherline"})
3973 .await;
3974 cx.simulate_shared_keystrokes("4 space").await;
3975 cx.shared_state().await.assert_eq(indoc! {"
3976 πππππ
3977 πanˇotherline"});
3978 }
3979
3980 #[gpui::test]
3981 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3982 let mut cx = NeovimBackedTestContext::new(cx).await;
3983
3984 cx.set_shared_state(indoc! {"
3985 ππππ
3986 πanˇotherline"})
3987 .await;
3988 cx.simulate_shared_keystrokes("4 backspace").await;
3989 cx.shared_state().await.assert_eq(indoc! {"
3990 πππˇπ
3991 πanotherline"});
3992 }
3993
3994 #[gpui::test]
3995 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
3996 let mut cx = VimTestContext::new(cx, true).await;
3997 cx.set_state(
3998 indoc! {
3999 "func empty(a string) bool {
4000 ˇif a == \"\" {
4001 return true
4002 }
4003 return false
4004 }"
4005 },
4006 Mode::Normal,
4007 );
4008 cx.simulate_keystrokes("[ -");
4009 cx.assert_state(
4010 indoc! {
4011 "ˇfunc empty(a string) bool {
4012 if a == \"\" {
4013 return true
4014 }
4015 return false
4016 }"
4017 },
4018 Mode::Normal,
4019 );
4020 cx.simulate_keystrokes("] =");
4021 cx.assert_state(
4022 indoc! {
4023 "func empty(a string) bool {
4024 if a == \"\" {
4025 return true
4026 }
4027 return false
4028 ˇ}"
4029 },
4030 Mode::Normal,
4031 );
4032 cx.simulate_keystrokes("[ +");
4033 cx.assert_state(
4034 indoc! {
4035 "func empty(a string) bool {
4036 if a == \"\" {
4037 return true
4038 }
4039 ˇreturn false
4040 }"
4041 },
4042 Mode::Normal,
4043 );
4044 cx.simulate_keystrokes("2 [ =");
4045 cx.assert_state(
4046 indoc! {
4047 "func empty(a string) bool {
4048 ˇif a == \"\" {
4049 return true
4050 }
4051 return false
4052 }"
4053 },
4054 Mode::Normal,
4055 );
4056 cx.simulate_keystrokes("] +");
4057 cx.assert_state(
4058 indoc! {
4059 "func empty(a string) bool {
4060 if a == \"\" {
4061 ˇreturn true
4062 }
4063 return false
4064 }"
4065 },
4066 Mode::Normal,
4067 );
4068 cx.simulate_keystrokes("] -");
4069 cx.assert_state(
4070 indoc! {
4071 "func empty(a string) bool {
4072 if a == \"\" {
4073 return true
4074 ˇ}
4075 return false
4076 }"
4077 },
4078 Mode::Normal,
4079 );
4080 }
4081
4082 #[gpui::test]
4083 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4084 let mut cx = NeovimBackedTestContext::new(cx).await;
4085 cx.set_shared_state("abˇc").await;
4086 cx.simulate_shared_keystrokes("delete").await;
4087 cx.shared_state().await.assert_eq("aˇb");
4088 }
4089
4090 #[gpui::test]
4091 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4092 let mut cx = NeovimBackedTestContext::new(cx).await;
4093
4094 cx.set_shared_state(indoc! {"
4095 ˇthe quick brown fox
4096 jumped over the lazy dog"})
4097 .await;
4098 cx.simulate_shared_keystrokes("d v 0").await;
4099 cx.shared_state().await.assert_eq(indoc! {"
4100 ˇhe quick brown fox
4101 jumped over the lazy dog"});
4102 assert_eq!(cx.cx.forced_motion(), false);
4103
4104 cx.set_shared_state(indoc! {"
4105 the quick bˇrown fox
4106 jumped over the lazy dog"})
4107 .await;
4108 cx.simulate_shared_keystrokes("d v 0").await;
4109 cx.shared_state().await.assert_eq(indoc! {"
4110 ˇown fox
4111 jumped over the lazy dog"});
4112 assert_eq!(cx.cx.forced_motion(), false);
4113
4114 cx.set_shared_state(indoc! {"
4115 the quick brown foˇx
4116 jumped over the lazy dog"})
4117 .await;
4118 cx.simulate_shared_keystrokes("d v 0").await;
4119 cx.shared_state().await.assert_eq(indoc! {"
4120 ˇ
4121 jumped over the lazy dog"});
4122 assert_eq!(cx.cx.forced_motion(), false);
4123 }
4124
4125 #[gpui::test]
4126 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4127 let mut cx = NeovimBackedTestContext::new(cx).await;
4128
4129 cx.set_shared_state(indoc! {"
4130 ˇthe quick brown fox
4131 jumped over the lazy dog"})
4132 .await;
4133 cx.simulate_shared_keystrokes("d v g shift-m").await;
4134 cx.shared_state().await.assert_eq(indoc! {"
4135 ˇbrown fox
4136 jumped over the lazy dog"});
4137 assert_eq!(cx.cx.forced_motion(), false);
4138
4139 cx.set_shared_state(indoc! {"
4140 the quick bˇrown fox
4141 jumped over the lazy dog"})
4142 .await;
4143 cx.simulate_shared_keystrokes("d v g shift-m").await;
4144 cx.shared_state().await.assert_eq(indoc! {"
4145 the quickˇown fox
4146 jumped over the lazy dog"});
4147 assert_eq!(cx.cx.forced_motion(), false);
4148
4149 cx.set_shared_state(indoc! {"
4150 the quick brown foˇx
4151 jumped over the lazy dog"})
4152 .await;
4153 cx.simulate_shared_keystrokes("d v g shift-m").await;
4154 cx.shared_state().await.assert_eq(indoc! {"
4155 the quicˇk
4156 jumped over the lazy dog"});
4157 assert_eq!(cx.cx.forced_motion(), false);
4158
4159 cx.set_shared_state(indoc! {"
4160 ˇthe quick brown fox
4161 jumped over the lazy dog"})
4162 .await;
4163 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4164 cx.shared_state().await.assert_eq(indoc! {"
4165 ˇ fox
4166 jumped over the lazy dog"});
4167 assert_eq!(cx.cx.forced_motion(), false);
4168
4169 cx.set_shared_state(indoc! {"
4170 ˇthe quick brown fox
4171 jumped over the lazy dog"})
4172 .await;
4173 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4174 cx.shared_state().await.assert_eq(indoc! {"
4175 ˇuick brown fox
4176 jumped over the lazy dog"});
4177 assert_eq!(cx.cx.forced_motion(), false);
4178 }
4179
4180 #[gpui::test]
4181 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4182 let mut cx = NeovimBackedTestContext::new(cx).await;
4183
4184 cx.set_shared_state(indoc! {"
4185 the quick brown foˇx
4186 jumped over the lazy dog"})
4187 .await;
4188 cx.simulate_shared_keystrokes("d v $").await;
4189 cx.shared_state().await.assert_eq(indoc! {"
4190 the quick brown foˇx
4191 jumped over the lazy dog"});
4192 assert_eq!(cx.cx.forced_motion(), false);
4193
4194 cx.set_shared_state(indoc! {"
4195 ˇthe quick brown fox
4196 jumped over the lazy dog"})
4197 .await;
4198 cx.simulate_shared_keystrokes("d v $").await;
4199 cx.shared_state().await.assert_eq(indoc! {"
4200 ˇx
4201 jumped over the lazy dog"});
4202 assert_eq!(cx.cx.forced_motion(), false);
4203 }
4204
4205 #[gpui::test]
4206 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4207 let mut cx = NeovimBackedTestContext::new(cx).await;
4208
4209 cx.set_shared_state(indoc! {"
4210 ˇthe quick brown fox
4211 jumped over the lazy dog"})
4212 .await;
4213 cx.simulate_shared_keystrokes("y v j p").await;
4214 cx.shared_state().await.assert_eq(indoc! {"
4215 the quick brown fox
4216 ˇthe quick brown fox
4217 jumped over the lazy dog"});
4218 assert_eq!(cx.cx.forced_motion(), false);
4219
4220 cx.set_shared_state(indoc! {"
4221 the quick bˇrown fox
4222 jumped over the lazy dog"})
4223 .await;
4224 cx.simulate_shared_keystrokes("y v j p").await;
4225 cx.shared_state().await.assert_eq(indoc! {"
4226 the quick brˇrown fox
4227 jumped overown fox
4228 jumped over the lazy dog"});
4229 assert_eq!(cx.cx.forced_motion(), false);
4230
4231 cx.set_shared_state(indoc! {"
4232 the quick brown foˇx
4233 jumped over the lazy dog"})
4234 .await;
4235 cx.simulate_shared_keystrokes("y v j p").await;
4236 cx.shared_state().await.assert_eq(indoc! {"
4237 the quick brown foxˇx
4238 jumped over the la
4239 jumped over the lazy dog"});
4240 assert_eq!(cx.cx.forced_motion(), false);
4241
4242 cx.set_shared_state(indoc! {"
4243 the quick brown fox
4244 jˇumped over the lazy dog"})
4245 .await;
4246 cx.simulate_shared_keystrokes("y v k p").await;
4247 cx.shared_state().await.assert_eq(indoc! {"
4248 thˇhe quick brown fox
4249 je quick brown fox
4250 jumped over the lazy dog"});
4251 assert_eq!(cx.cx.forced_motion(), false);
4252 }
4253
4254 #[gpui::test]
4255 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4256 let mut cx = NeovimBackedTestContext::new(cx).await;
4257
4258 cx.set_shared_state(indoc! {"
4259 ˇthe quick brown fox
4260 jumped over the lazy dog"})
4261 .await;
4262 cx.simulate_shared_keystrokes("d v e").await;
4263 cx.shared_state().await.assert_eq(indoc! {"
4264 ˇe quick brown fox
4265 jumped over the lazy dog"});
4266 assert_eq!(cx.cx.forced_motion(), false);
4267
4268 cx.set_shared_state(indoc! {"
4269 the quick bˇrown fox
4270 jumped over the lazy dog"})
4271 .await;
4272 cx.simulate_shared_keystrokes("d v e").await;
4273 cx.shared_state().await.assert_eq(indoc! {"
4274 the quick bˇn fox
4275 jumped over the lazy dog"});
4276 assert_eq!(cx.cx.forced_motion(), false);
4277
4278 cx.set_shared_state(indoc! {"
4279 the quick brown foˇx
4280 jumped over the lazy dog"})
4281 .await;
4282 cx.simulate_shared_keystrokes("d v e").await;
4283 cx.shared_state().await.assert_eq(indoc! {"
4284 the quick brown foˇd over the lazy dog"});
4285 assert_eq!(cx.cx.forced_motion(), false);
4286 }
4287}