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 if let Some(ch) = map.buffer_snapshot.chars_at(point).next() {
1816 point.column += ch.len_utf8() as u32;
1817 }
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 if let Some(ch) = map.buffer_snapshot.chars_at(point).next() {
1991 point.column += ch.len_utf8() as u32;
1992 }
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 if classifier.kind(ch) != CharKind::Whitespace {
2059 return end_of_line.to_display_point(map);
2060 }
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 return 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 return 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
2642fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2643 if smartcase {
2644 if target.is_uppercase() {
2645 target == other
2646 } else {
2647 target == other.to_ascii_lowercase()
2648 }
2649 } else {
2650 target == other
2651 }
2652}
2653
2654fn sneak(
2655 map: &DisplaySnapshot,
2656 from: DisplayPoint,
2657 first_target: char,
2658 second_target: char,
2659 times: usize,
2660 smartcase: bool,
2661) -> Option<DisplayPoint> {
2662 let mut to = from;
2663 let mut found = false;
2664
2665 for _ in 0..times {
2666 found = false;
2667 let new_to = find_boundary(
2668 map,
2669 movement::right(map, to),
2670 FindRange::MultiLine,
2671 |left, right| {
2672 found = is_character_match(first_target, left, smartcase)
2673 && is_character_match(second_target, right, smartcase);
2674 found
2675 },
2676 );
2677 if to == new_to {
2678 break;
2679 }
2680 to = new_to;
2681 }
2682
2683 if found {
2684 Some(movement::left(map, to))
2685 } else {
2686 None
2687 }
2688}
2689
2690fn sneak_backward(
2691 map: &DisplaySnapshot,
2692 from: DisplayPoint,
2693 first_target: char,
2694 second_target: char,
2695 times: usize,
2696 smartcase: bool,
2697) -> Option<DisplayPoint> {
2698 let mut to = from;
2699 let mut found = false;
2700
2701 for _ in 0..times {
2702 found = false;
2703 let new_to =
2704 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2705 found = is_character_match(first_target, left, smartcase)
2706 && is_character_match(second_target, right, smartcase);
2707 found
2708 });
2709 if to == new_to {
2710 break;
2711 }
2712 to = new_to;
2713 }
2714
2715 if found {
2716 Some(movement::left(map, to))
2717 } else {
2718 None
2719 }
2720}
2721
2722fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2723 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2724 first_non_whitespace(map, false, correct_line)
2725}
2726
2727fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2728 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2729 first_non_whitespace(map, false, correct_line)
2730}
2731
2732fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2733 let correct_line = start_of_relative_buffer_row(map, point, 0);
2734 right(map, correct_line, times.saturating_sub(1))
2735}
2736
2737pub(crate) fn next_line_end(
2738 map: &DisplaySnapshot,
2739 mut point: DisplayPoint,
2740 times: usize,
2741) -> DisplayPoint {
2742 if times > 1 {
2743 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2744 }
2745 end_of_line(map, false, point, 1)
2746}
2747
2748fn window_top(
2749 map: &DisplaySnapshot,
2750 point: DisplayPoint,
2751 text_layout_details: &TextLayoutDetails,
2752 mut times: usize,
2753) -> (DisplayPoint, SelectionGoal) {
2754 let first_visible_line = text_layout_details
2755 .scroll_anchor
2756 .anchor
2757 .to_display_point(map);
2758
2759 if first_visible_line.row() != DisplayRow(0)
2760 && text_layout_details.vertical_scroll_margin as usize > times
2761 {
2762 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2763 }
2764
2765 if let Some(visible_rows) = text_layout_details.visible_rows {
2766 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2767 let new_row = (first_visible_line.row().0 + (times as u32))
2768 .min(bottom_row)
2769 .min(map.max_point().row().0);
2770 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2771
2772 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2773 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2774 } else {
2775 let new_row =
2776 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2777 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2778
2779 let new_point = DisplayPoint::new(new_row, new_col);
2780 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2781 }
2782}
2783
2784fn window_middle(
2785 map: &DisplaySnapshot,
2786 point: DisplayPoint,
2787 text_layout_details: &TextLayoutDetails,
2788) -> (DisplayPoint, SelectionGoal) {
2789 if let Some(visible_rows) = text_layout_details.visible_rows {
2790 let first_visible_line = text_layout_details
2791 .scroll_anchor
2792 .anchor
2793 .to_display_point(map);
2794
2795 let max_visible_rows =
2796 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2797
2798 let new_row =
2799 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2800 let new_row = DisplayRow(new_row);
2801 let new_col = point.column().min(map.line_len(new_row));
2802 let new_point = DisplayPoint::new(new_row, new_col);
2803 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2804 } else {
2805 (point, SelectionGoal::None)
2806 }
2807}
2808
2809fn window_bottom(
2810 map: &DisplaySnapshot,
2811 point: DisplayPoint,
2812 text_layout_details: &TextLayoutDetails,
2813 mut times: usize,
2814) -> (DisplayPoint, SelectionGoal) {
2815 if let Some(visible_rows) = text_layout_details.visible_rows {
2816 let first_visible_line = text_layout_details
2817 .scroll_anchor
2818 .anchor
2819 .to_display_point(map);
2820 let bottom_row = first_visible_line.row().0
2821 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2822 if bottom_row < map.max_point().row().0
2823 && text_layout_details.vertical_scroll_margin as usize > times
2824 {
2825 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2826 }
2827 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2828 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2829 {
2830 first_visible_line.row()
2831 } else {
2832 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2833 };
2834 let new_col = point.column().min(map.line_len(new_row));
2835 let new_point = DisplayPoint::new(new_row, new_col);
2836 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2837 } else {
2838 (point, SelectionGoal::None)
2839 }
2840}
2841
2842fn method_motion(
2843 map: &DisplaySnapshot,
2844 mut display_point: DisplayPoint,
2845 times: usize,
2846 direction: Direction,
2847 is_start: bool,
2848) -> DisplayPoint {
2849 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2850 return display_point;
2851 };
2852
2853 for _ in 0..times {
2854 let point = map.display_point_to_point(display_point, Bias::Left);
2855 let offset = point.to_offset(&map.buffer_snapshot);
2856 let range = if direction == Direction::Prev {
2857 0..offset
2858 } else {
2859 offset..buffer.len()
2860 };
2861
2862 let possibilities = buffer
2863 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2864 .filter_map(|(range, object)| {
2865 if !matches!(object, language::TextObject::AroundFunction) {
2866 return None;
2867 }
2868
2869 let relevant = if is_start { range.start } else { range.end };
2870 if direction == Direction::Prev && relevant < offset {
2871 Some(relevant)
2872 } else if direction == Direction::Next && relevant > offset + 1 {
2873 Some(relevant)
2874 } else {
2875 None
2876 }
2877 });
2878
2879 let dest = if direction == Direction::Prev {
2880 possibilities.max().unwrap_or(offset)
2881 } else {
2882 possibilities.min().unwrap_or(offset)
2883 };
2884 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2885 if new_point == display_point {
2886 break;
2887 }
2888 display_point = new_point;
2889 }
2890 display_point
2891}
2892
2893fn comment_motion(
2894 map: &DisplaySnapshot,
2895 mut display_point: DisplayPoint,
2896 times: usize,
2897 direction: Direction,
2898) -> DisplayPoint {
2899 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2900 return display_point;
2901 };
2902
2903 for _ in 0..times {
2904 let point = map.display_point_to_point(display_point, Bias::Left);
2905 let offset = point.to_offset(&map.buffer_snapshot);
2906 let range = if direction == Direction::Prev {
2907 0..offset
2908 } else {
2909 offset..buffer.len()
2910 };
2911
2912 let possibilities = buffer
2913 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2914 .filter_map(|(range, object)| {
2915 if !matches!(object, language::TextObject::AroundComment) {
2916 return None;
2917 }
2918
2919 let relevant = if direction == Direction::Prev {
2920 range.start
2921 } else {
2922 range.end
2923 };
2924 if direction == Direction::Prev && relevant < offset {
2925 Some(relevant)
2926 } else if direction == Direction::Next && relevant > offset + 1 {
2927 Some(relevant)
2928 } else {
2929 None
2930 }
2931 });
2932
2933 let dest = if direction == Direction::Prev {
2934 possibilities.max().unwrap_or(offset)
2935 } else {
2936 possibilities.min().unwrap_or(offset)
2937 };
2938 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2939 if new_point == display_point {
2940 break;
2941 }
2942 display_point = new_point;
2943 }
2944
2945 display_point
2946}
2947
2948fn section_motion(
2949 map: &DisplaySnapshot,
2950 mut display_point: DisplayPoint,
2951 times: usize,
2952 direction: Direction,
2953 is_start: bool,
2954) -> DisplayPoint {
2955 if map.buffer_snapshot.as_singleton().is_some() {
2956 for _ in 0..times {
2957 let offset = map
2958 .display_point_to_point(display_point, Bias::Left)
2959 .to_offset(&map.buffer_snapshot);
2960 let range = if direction == Direction::Prev {
2961 0..offset
2962 } else {
2963 offset..map.buffer_snapshot.len()
2964 };
2965
2966 // we set a max start depth here because we want a section to only be "top level"
2967 // similar to vim's default of '{' in the first column.
2968 // (and without it, ]] at the start of editor.rs is -very- slow)
2969 let mut possibilities = map
2970 .buffer_snapshot
2971 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2972 .filter(|(_, object)| {
2973 matches!(
2974 object,
2975 language::TextObject::AroundClass | language::TextObject::AroundFunction
2976 )
2977 })
2978 .collect::<Vec<_>>();
2979 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2980 let mut prev_end = None;
2981 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2982 if t == language::TextObject::AroundFunction
2983 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2984 {
2985 return None;
2986 }
2987 prev_end = Some(range.end);
2988
2989 let relevant = if is_start { range.start } else { range.end };
2990 if direction == Direction::Prev && relevant < offset {
2991 Some(relevant)
2992 } else if direction == Direction::Next && relevant > offset + 1 {
2993 Some(relevant)
2994 } else {
2995 None
2996 }
2997 });
2998
2999 let offset = if direction == Direction::Prev {
3000 possibilities.max().unwrap_or(0)
3001 } else {
3002 possibilities.min().unwrap_or(map.buffer_snapshot.len())
3003 };
3004
3005 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
3006 if new_point == display_point {
3007 break;
3008 }
3009 display_point = new_point;
3010 }
3011 return display_point;
3012 };
3013
3014 for _ in 0..times {
3015 let next_point = if is_start {
3016 movement::start_of_excerpt(map, display_point, direction)
3017 } else {
3018 movement::end_of_excerpt(map, display_point, direction)
3019 };
3020 if next_point == display_point {
3021 break;
3022 }
3023 display_point = next_point;
3024 }
3025
3026 display_point
3027}
3028
3029fn matches_indent_type(
3030 target_indent: &text::LineIndent,
3031 current_indent: &text::LineIndent,
3032 indent_type: IndentType,
3033) -> bool {
3034 match indent_type {
3035 IndentType::Lesser => {
3036 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3037 }
3038 IndentType::Greater => {
3039 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3040 }
3041 IndentType::Same => {
3042 target_indent.spaces == current_indent.spaces
3043 && target_indent.tabs == current_indent.tabs
3044 }
3045 }
3046}
3047
3048fn indent_motion(
3049 map: &DisplaySnapshot,
3050 mut display_point: DisplayPoint,
3051 times: usize,
3052 direction: Direction,
3053 indent_type: IndentType,
3054) -> DisplayPoint {
3055 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3056 let current_row = MultiBufferRow(buffer_point.row);
3057 let current_indent = map.line_indent_for_buffer_row(current_row);
3058 if current_indent.is_line_empty() {
3059 return display_point;
3060 }
3061 let max_row = map.max_point().to_point(map).row;
3062
3063 for _ in 0..times {
3064 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3065
3066 let target_row = match direction {
3067 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3068 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3069 !indent.is_line_empty()
3070 && matches_indent_type(&indent, ¤t_indent, indent_type)
3071 }),
3072 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3073 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3074 !indent.is_line_empty()
3075 && matches_indent_type(&indent, ¤t_indent, indent_type)
3076 }),
3077 }
3078 .unwrap_or(current_buffer_row);
3079
3080 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3081 let new_point = first_non_whitespace(map, false, new_point);
3082 if new_point == display_point {
3083 break;
3084 }
3085 display_point = new_point;
3086 }
3087 display_point
3088}
3089
3090#[cfg(test)]
3091mod test {
3092
3093 use crate::{
3094 state::Mode,
3095 test::{NeovimBackedTestContext, VimTestContext},
3096 };
3097 use editor::display_map::Inlay;
3098 use indoc::indoc;
3099 use language::Point;
3100 use multi_buffer::MultiBufferRow;
3101
3102 #[gpui::test]
3103 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3104 let mut cx = NeovimBackedTestContext::new(cx).await;
3105
3106 let initial_state = indoc! {r"ˇabc
3107 def
3108
3109 paragraph
3110 the second
3111
3112
3113
3114 third and
3115 final"};
3116
3117 // goes down once
3118 cx.set_shared_state(initial_state).await;
3119 cx.simulate_shared_keystrokes("}").await;
3120 cx.shared_state().await.assert_eq(indoc! {r"abc
3121 def
3122 ˇ
3123 paragraph
3124 the second
3125
3126
3127
3128 third and
3129 final"});
3130
3131 // goes up once
3132 cx.simulate_shared_keystrokes("{").await;
3133 cx.shared_state().await.assert_eq(initial_state);
3134
3135 // goes down twice
3136 cx.simulate_shared_keystrokes("2 }").await;
3137 cx.shared_state().await.assert_eq(indoc! {r"abc
3138 def
3139
3140 paragraph
3141 the second
3142 ˇ
3143
3144
3145 third and
3146 final"});
3147
3148 // goes down over multiple blanks
3149 cx.simulate_shared_keystrokes("}").await;
3150 cx.shared_state().await.assert_eq(indoc! {r"abc
3151 def
3152
3153 paragraph
3154 the second
3155
3156
3157
3158 third and
3159 finaˇl"});
3160
3161 // goes up twice
3162 cx.simulate_shared_keystrokes("2 {").await;
3163 cx.shared_state().await.assert_eq(indoc! {r"abc
3164 def
3165 ˇ
3166 paragraph
3167 the second
3168
3169
3170
3171 third and
3172 final"});
3173 }
3174
3175 #[gpui::test]
3176 async fn test_matching(cx: &mut gpui::TestAppContext) {
3177 let mut cx = NeovimBackedTestContext::new(cx).await;
3178
3179 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3180 do(something(with<Types>.and_arrays[0, 2]))
3181 }"})
3182 .await;
3183 cx.simulate_shared_keystrokes("%").await;
3184 cx.shared_state()
3185 .await
3186 .assert_eq(indoc! {r"func (a stringˇ) {
3187 do(something(with<Types>.and_arrays[0, 2]))
3188 }"});
3189
3190 // test it works on the last character of the line
3191 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3192 do(something(with<Types>.and_arrays[0, 2]))
3193 }"})
3194 .await;
3195 cx.simulate_shared_keystrokes("%").await;
3196 cx.shared_state()
3197 .await
3198 .assert_eq(indoc! {r"func (a string) {
3199 do(something(with<Types>.and_arrays[0, 2]))
3200 ˇ}"});
3201
3202 // test it works on immediate nesting
3203 cx.set_shared_state("ˇ{()}").await;
3204 cx.simulate_shared_keystrokes("%").await;
3205 cx.shared_state().await.assert_eq("{()ˇ}");
3206 cx.simulate_shared_keystrokes("%").await;
3207 cx.shared_state().await.assert_eq("ˇ{()}");
3208
3209 // test it works on immediate nesting inside braces
3210 cx.set_shared_state("{\n ˇ{()}\n}").await;
3211 cx.simulate_shared_keystrokes("%").await;
3212 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3213
3214 // test it jumps to the next paren on a line
3215 cx.set_shared_state("func ˇboop() {\n}").await;
3216 cx.simulate_shared_keystrokes("%").await;
3217 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3218 }
3219
3220 #[gpui::test]
3221 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3222 let mut cx = NeovimBackedTestContext::new(cx).await;
3223
3224 // test it works with curly braces
3225 cx.set_shared_state(indoc! {r"func (a string) {
3226 do(something(with<Types>.anˇd_arrays[0, 2]))
3227 }"})
3228 .await;
3229 cx.simulate_shared_keystrokes("] }").await;
3230 cx.shared_state()
3231 .await
3232 .assert_eq(indoc! {r"func (a string) {
3233 do(something(with<Types>.and_arrays[0, 2]))
3234 ˇ}"});
3235
3236 // test it works with brackets
3237 cx.set_shared_state(indoc! {r"func (a string) {
3238 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3239 }"})
3240 .await;
3241 cx.simulate_shared_keystrokes("] )").await;
3242 cx.shared_state()
3243 .await
3244 .assert_eq(indoc! {r"func (a string) {
3245 do(something(with<Types>.and_arrays[0, 2])ˇ)
3246 }"});
3247
3248 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3249 .await;
3250 cx.simulate_shared_keystrokes("] )").await;
3251 cx.shared_state()
3252 .await
3253 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3254
3255 // test it works on immediate nesting
3256 cx.set_shared_state("{ˇ {}{}}").await;
3257 cx.simulate_shared_keystrokes("] }").await;
3258 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3259 cx.set_shared_state("(ˇ ()())").await;
3260 cx.simulate_shared_keystrokes("] )").await;
3261 cx.shared_state().await.assert_eq("( ()()ˇ)");
3262
3263 // test it works on immediate nesting inside braces
3264 cx.set_shared_state("{\n ˇ {()}\n}").await;
3265 cx.simulate_shared_keystrokes("] }").await;
3266 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3267 cx.set_shared_state("(\n ˇ {()}\n)").await;
3268 cx.simulate_shared_keystrokes("] )").await;
3269 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3270 }
3271
3272 #[gpui::test]
3273 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3274 let mut cx = NeovimBackedTestContext::new(cx).await;
3275
3276 // test it works with curly braces
3277 cx.set_shared_state(indoc! {r"func (a string) {
3278 do(something(with<Types>.anˇd_arrays[0, 2]))
3279 }"})
3280 .await;
3281 cx.simulate_shared_keystrokes("[ {").await;
3282 cx.shared_state()
3283 .await
3284 .assert_eq(indoc! {r"func (a string) ˇ{
3285 do(something(with<Types>.and_arrays[0, 2]))
3286 }"});
3287
3288 // test it works with brackets
3289 cx.set_shared_state(indoc! {r"func (a string) {
3290 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3291 }"})
3292 .await;
3293 cx.simulate_shared_keystrokes("[ (").await;
3294 cx.shared_state()
3295 .await
3296 .assert_eq(indoc! {r"func (a string) {
3297 doˇ(something(with<Types>.and_arrays[0, 2]))
3298 }"});
3299
3300 // test it works on immediate nesting
3301 cx.set_shared_state("{{}{} ˇ }").await;
3302 cx.simulate_shared_keystrokes("[ {").await;
3303 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3304 cx.set_shared_state("(()() ˇ )").await;
3305 cx.simulate_shared_keystrokes("[ (").await;
3306 cx.shared_state().await.assert_eq("ˇ(()() )");
3307
3308 // test it works on immediate nesting inside braces
3309 cx.set_shared_state("{\n {()} ˇ\n}").await;
3310 cx.simulate_shared_keystrokes("[ {").await;
3311 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3312 cx.set_shared_state("(\n {()} ˇ\n)").await;
3313 cx.simulate_shared_keystrokes("[ (").await;
3314 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3315 }
3316
3317 #[gpui::test]
3318 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3319 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3320
3321 cx.neovim.exec("set filetype=html").await;
3322
3323 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3324 cx.simulate_shared_keystrokes("%").await;
3325 cx.shared_state()
3326 .await
3327 .assert_eq(indoc! {r"<body><ˇ/body>"});
3328 cx.simulate_shared_keystrokes("%").await;
3329
3330 // test jumping backwards
3331 cx.shared_state()
3332 .await
3333 .assert_eq(indoc! {r"<ˇbody></body>"});
3334
3335 // test self-closing tags
3336 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3337 cx.simulate_shared_keystrokes("%").await;
3338 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3339
3340 // test tag with attributes
3341 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3342 </div>
3343 "})
3344 .await;
3345 cx.simulate_shared_keystrokes("%").await;
3346 cx.shared_state()
3347 .await
3348 .assert_eq(indoc! {r"<div class='test' id='main'>
3349 <ˇ/div>
3350 "});
3351
3352 // test multi-line self-closing tag
3353 cx.set_shared_state(indoc! {r#"<a>
3354 <br
3355 test = "test"
3356 /ˇ>
3357 </a>"#})
3358 .await;
3359 cx.simulate_shared_keystrokes("%").await;
3360 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3361 ˇ<br
3362 test = "test"
3363 />
3364 </a>"#});
3365 }
3366
3367 #[gpui::test]
3368 async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
3369 let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
3370
3371 // test brackets within tags
3372 cx.set_shared_state(indoc! {r"function f() {
3373 return (
3374 <div rules={ˇ[{ a: 1 }]}>
3375 <h1>test</h1>
3376 </div>
3377 );
3378 }"})
3379 .await;
3380 cx.simulate_shared_keystrokes("%").await;
3381 cx.shared_state().await.assert_eq(indoc! {r"function f() {
3382 return (
3383 <div rules={[{ a: 1 }ˇ]}>
3384 <h1>test</h1>
3385 </div>
3386 );
3387 }"});
3388 }
3389
3390 #[gpui::test]
3391 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3392 let mut cx = NeovimBackedTestContext::new(cx).await;
3393
3394 // f and F
3395 cx.set_shared_state("ˇone two three four").await;
3396 cx.simulate_shared_keystrokes("f o").await;
3397 cx.shared_state().await.assert_eq("one twˇo three four");
3398 cx.simulate_shared_keystrokes(",").await;
3399 cx.shared_state().await.assert_eq("ˇone two three four");
3400 cx.simulate_shared_keystrokes("2 ;").await;
3401 cx.shared_state().await.assert_eq("one two three fˇour");
3402 cx.simulate_shared_keystrokes("shift-f e").await;
3403 cx.shared_state().await.assert_eq("one two threˇe four");
3404 cx.simulate_shared_keystrokes("2 ;").await;
3405 cx.shared_state().await.assert_eq("onˇe two three four");
3406 cx.simulate_shared_keystrokes(",").await;
3407 cx.shared_state().await.assert_eq("one two thrˇee four");
3408
3409 // t and T
3410 cx.set_shared_state("ˇone two three four").await;
3411 cx.simulate_shared_keystrokes("t o").await;
3412 cx.shared_state().await.assert_eq("one tˇwo three four");
3413 cx.simulate_shared_keystrokes(",").await;
3414 cx.shared_state().await.assert_eq("oˇne two three four");
3415 cx.simulate_shared_keystrokes("2 ;").await;
3416 cx.shared_state().await.assert_eq("one two three ˇfour");
3417 cx.simulate_shared_keystrokes("shift-t e").await;
3418 cx.shared_state().await.assert_eq("one two threeˇ four");
3419 cx.simulate_shared_keystrokes("3 ;").await;
3420 cx.shared_state().await.assert_eq("oneˇ two three four");
3421 cx.simulate_shared_keystrokes(",").await;
3422 cx.shared_state().await.assert_eq("one two thˇree four");
3423 }
3424
3425 #[gpui::test]
3426 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3427 let mut cx = NeovimBackedTestContext::new(cx).await;
3428 let initial_state = indoc! {r"something(ˇfoo)"};
3429 cx.set_shared_state(initial_state).await;
3430 cx.simulate_shared_keystrokes("}").await;
3431 cx.shared_state().await.assert_eq("something(fooˇ)");
3432 }
3433
3434 #[gpui::test]
3435 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3436 let mut cx = NeovimBackedTestContext::new(cx).await;
3437 cx.set_shared_state("ˇone\n two\nthree").await;
3438 cx.simulate_shared_keystrokes("enter").await;
3439 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3440 }
3441
3442 #[gpui::test]
3443 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3444 let mut cx = NeovimBackedTestContext::new(cx).await;
3445 cx.set_shared_state("ˇ one\n two \nthree").await;
3446 cx.simulate_shared_keystrokes("g _").await;
3447 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3448
3449 cx.set_shared_state("ˇ one \n two \nthree").await;
3450 cx.simulate_shared_keystrokes("g _").await;
3451 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3452 cx.simulate_shared_keystrokes("2 g _").await;
3453 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3454 }
3455
3456 #[gpui::test]
3457 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3458 let mut cx = NeovimBackedTestContext::new(cx).await;
3459 let initial_state = indoc! {r"abc
3460 def
3461 paragraph
3462 the second
3463 third ˇand
3464 final"};
3465
3466 cx.set_shared_state(initial_state).await;
3467 cx.simulate_shared_keystrokes("shift-h").await;
3468 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3469 def
3470 paragraph
3471 the second
3472 third and
3473 final"});
3474
3475 // clip point
3476 cx.set_shared_state(indoc! {r"
3477 1 2 3
3478 4 5 6
3479 7 8 ˇ9
3480 "})
3481 .await;
3482 cx.simulate_shared_keystrokes("shift-h").await;
3483 cx.shared_state().await.assert_eq(indoc! {"
3484 1 2 ˇ3
3485 4 5 6
3486 7 8 9
3487 "});
3488
3489 cx.set_shared_state(indoc! {r"
3490 1 2 3
3491 4 5 6
3492 ˇ7 8 9
3493 "})
3494 .await;
3495 cx.simulate_shared_keystrokes("shift-h").await;
3496 cx.shared_state().await.assert_eq(indoc! {"
3497 ˇ1 2 3
3498 4 5 6
3499 7 8 9
3500 "});
3501
3502 cx.set_shared_state(indoc! {r"
3503 1 2 3
3504 4 5 ˇ6
3505 7 8 9"})
3506 .await;
3507 cx.simulate_shared_keystrokes("9 shift-h").await;
3508 cx.shared_state().await.assert_eq(indoc! {"
3509 1 2 3
3510 4 5 6
3511 7 8 ˇ9"});
3512 }
3513
3514 #[gpui::test]
3515 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3516 let mut cx = NeovimBackedTestContext::new(cx).await;
3517 let initial_state = indoc! {r"abˇc
3518 def
3519 paragraph
3520 the second
3521 third and
3522 final"};
3523
3524 cx.set_shared_state(initial_state).await;
3525 cx.simulate_shared_keystrokes("shift-m").await;
3526 cx.shared_state().await.assert_eq(indoc! {r"abc
3527 def
3528 paˇragraph
3529 the second
3530 third and
3531 final"});
3532
3533 cx.set_shared_state(indoc! {r"
3534 1 2 3
3535 4 5 6
3536 7 8 ˇ9
3537 "})
3538 .await;
3539 cx.simulate_shared_keystrokes("shift-m").await;
3540 cx.shared_state().await.assert_eq(indoc! {"
3541 1 2 3
3542 4 5 ˇ6
3543 7 8 9
3544 "});
3545 cx.set_shared_state(indoc! {r"
3546 1 2 3
3547 4 5 6
3548 ˇ7 8 9
3549 "})
3550 .await;
3551 cx.simulate_shared_keystrokes("shift-m").await;
3552 cx.shared_state().await.assert_eq(indoc! {"
3553 1 2 3
3554 ˇ4 5 6
3555 7 8 9
3556 "});
3557 cx.set_shared_state(indoc! {r"
3558 ˇ1 2 3
3559 4 5 6
3560 7 8 9
3561 "})
3562 .await;
3563 cx.simulate_shared_keystrokes("shift-m").await;
3564 cx.shared_state().await.assert_eq(indoc! {"
3565 1 2 3
3566 ˇ4 5 6
3567 7 8 9
3568 "});
3569 cx.set_shared_state(indoc! {r"
3570 1 2 3
3571 ˇ4 5 6
3572 7 8 9
3573 "})
3574 .await;
3575 cx.simulate_shared_keystrokes("shift-m").await;
3576 cx.shared_state().await.assert_eq(indoc! {"
3577 1 2 3
3578 ˇ4 5 6
3579 7 8 9
3580 "});
3581 cx.set_shared_state(indoc! {r"
3582 1 2 3
3583 4 5 ˇ6
3584 7 8 9
3585 "})
3586 .await;
3587 cx.simulate_shared_keystrokes("shift-m").await;
3588 cx.shared_state().await.assert_eq(indoc! {"
3589 1 2 3
3590 4 5 ˇ6
3591 7 8 9
3592 "});
3593 }
3594
3595 #[gpui::test]
3596 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3597 let mut cx = NeovimBackedTestContext::new(cx).await;
3598 let initial_state = indoc! {r"abc
3599 deˇf
3600 paragraph
3601 the second
3602 third and
3603 final"};
3604
3605 cx.set_shared_state(initial_state).await;
3606 cx.simulate_shared_keystrokes("shift-l").await;
3607 cx.shared_state().await.assert_eq(indoc! {r"abc
3608 def
3609 paragraph
3610 the second
3611 third and
3612 fiˇnal"});
3613
3614 cx.set_shared_state(indoc! {r"
3615 1 2 3
3616 4 5 ˇ6
3617 7 8 9
3618 "})
3619 .await;
3620 cx.simulate_shared_keystrokes("shift-l").await;
3621 cx.shared_state().await.assert_eq(indoc! {"
3622 1 2 3
3623 4 5 6
3624 7 8 9
3625 ˇ"});
3626
3627 cx.set_shared_state(indoc! {r"
3628 1 2 3
3629 ˇ4 5 6
3630 7 8 9
3631 "})
3632 .await;
3633 cx.simulate_shared_keystrokes("shift-l").await;
3634 cx.shared_state().await.assert_eq(indoc! {"
3635 1 2 3
3636 4 5 6
3637 7 8 9
3638 ˇ"});
3639
3640 cx.set_shared_state(indoc! {r"
3641 1 2 ˇ3
3642 4 5 6
3643 7 8 9
3644 "})
3645 .await;
3646 cx.simulate_shared_keystrokes("shift-l").await;
3647 cx.shared_state().await.assert_eq(indoc! {"
3648 1 2 3
3649 4 5 6
3650 7 8 9
3651 ˇ"});
3652
3653 cx.set_shared_state(indoc! {r"
3654 ˇ1 2 3
3655 4 5 6
3656 7 8 9
3657 "})
3658 .await;
3659 cx.simulate_shared_keystrokes("shift-l").await;
3660 cx.shared_state().await.assert_eq(indoc! {"
3661 1 2 3
3662 4 5 6
3663 7 8 9
3664 ˇ"});
3665
3666 cx.set_shared_state(indoc! {r"
3667 1 2 3
3668 4 5 ˇ6
3669 7 8 9
3670 "})
3671 .await;
3672 cx.simulate_shared_keystrokes("9 shift-l").await;
3673 cx.shared_state().await.assert_eq(indoc! {"
3674 1 2 ˇ3
3675 4 5 6
3676 7 8 9
3677 "});
3678 }
3679
3680 #[gpui::test]
3681 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3682 let mut cx = NeovimBackedTestContext::new(cx).await;
3683 cx.set_shared_state(indoc! {r"
3684 456 5ˇ67 678
3685 "})
3686 .await;
3687 cx.simulate_shared_keystrokes("g e").await;
3688 cx.shared_state().await.assert_eq(indoc! {"
3689 45ˇ6 567 678
3690 "});
3691
3692 // Test times
3693 cx.set_shared_state(indoc! {r"
3694 123 234 345
3695 456 5ˇ67 678
3696 "})
3697 .await;
3698 cx.simulate_shared_keystrokes("4 g e").await;
3699 cx.shared_state().await.assert_eq(indoc! {"
3700 12ˇ3 234 345
3701 456 567 678
3702 "});
3703
3704 // With punctuation
3705 cx.set_shared_state(indoc! {r"
3706 123 234 345
3707 4;5.6 5ˇ67 678
3708 789 890 901
3709 "})
3710 .await;
3711 cx.simulate_shared_keystrokes("g e").await;
3712 cx.shared_state().await.assert_eq(indoc! {"
3713 123 234 345
3714 4;5.ˇ6 567 678
3715 789 890 901
3716 "});
3717
3718 // With punctuation and count
3719 cx.set_shared_state(indoc! {r"
3720 123 234 345
3721 4;5.6 5ˇ67 678
3722 789 890 901
3723 "})
3724 .await;
3725 cx.simulate_shared_keystrokes("5 g e").await;
3726 cx.shared_state().await.assert_eq(indoc! {"
3727 123 234 345
3728 ˇ4;5.6 567 678
3729 789 890 901
3730 "});
3731
3732 // newlines
3733 cx.set_shared_state(indoc! {r"
3734 123 234 345
3735
3736 78ˇ9 890 901
3737 "})
3738 .await;
3739 cx.simulate_shared_keystrokes("g e").await;
3740 cx.shared_state().await.assert_eq(indoc! {"
3741 123 234 345
3742 ˇ
3743 789 890 901
3744 "});
3745 cx.simulate_shared_keystrokes("g e").await;
3746 cx.shared_state().await.assert_eq(indoc! {"
3747 123 234 34ˇ5
3748
3749 789 890 901
3750 "});
3751
3752 // With punctuation
3753 cx.set_shared_state(indoc! {r"
3754 123 234 345
3755 4;5.ˇ6 567 678
3756 789 890 901
3757 "})
3758 .await;
3759 cx.simulate_shared_keystrokes("g shift-e").await;
3760 cx.shared_state().await.assert_eq(indoc! {"
3761 123 234 34ˇ5
3762 4;5.6 567 678
3763 789 890 901
3764 "});
3765
3766 // With multi byte char
3767 cx.set_shared_state(indoc! {r"
3768 bar ˇó
3769 "})
3770 .await;
3771 cx.simulate_shared_keystrokes("g e").await;
3772 cx.shared_state().await.assert_eq(indoc! {"
3773 baˇr ó
3774 "});
3775 }
3776
3777 #[gpui::test]
3778 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3779 let mut cx = NeovimBackedTestContext::new(cx).await;
3780
3781 cx.set_shared_state(indoc! {"
3782 fn aˇ() {
3783 return
3784 }
3785 "})
3786 .await;
3787 cx.simulate_shared_keystrokes("v $ %").await;
3788 cx.shared_state().await.assert_eq(indoc! {"
3789 fn a«() {
3790 return
3791 }ˇ»
3792 "});
3793 }
3794
3795 #[gpui::test]
3796 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3797 let mut cx = VimTestContext::new(cx, true).await;
3798
3799 cx.set_state(
3800 indoc! {"
3801 struct Foo {
3802 ˇ
3803 }
3804 "},
3805 Mode::Normal,
3806 );
3807
3808 cx.update_editor(|editor, _window, cx| {
3809 let range = editor.selections.newest_anchor().range();
3810 let inlay_text = " field: int,\n field2: string\n field3: float";
3811 let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
3812 editor.splice_inlays(&[], vec![inlay], cx);
3813 });
3814
3815 cx.simulate_keystrokes("j");
3816 cx.assert_state(
3817 indoc! {"
3818 struct Foo {
3819
3820 ˇ}
3821 "},
3822 Mode::Normal,
3823 );
3824 }
3825
3826 #[gpui::test]
3827 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3828 let mut cx = VimTestContext::new(cx, true).await;
3829
3830 cx.set_state(
3831 indoc! {"
3832 ˇstruct Foo {
3833
3834 }
3835 "},
3836 Mode::Normal,
3837 );
3838 cx.update_editor(|editor, _window, cx| {
3839 let snapshot = editor.buffer().read(cx).snapshot(cx);
3840 let end_of_line =
3841 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3842 let inlay_text = " hint";
3843 let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
3844 editor.splice_inlays(&[], vec![inlay], cx);
3845 });
3846 cx.simulate_keystrokes("$");
3847 cx.assert_state(
3848 indoc! {"
3849 struct Foo ˇ{
3850
3851 }
3852 "},
3853 Mode::Normal,
3854 );
3855 }
3856
3857 #[gpui::test]
3858 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3859 let mut cx = NeovimBackedTestContext::new(cx).await;
3860 // Normal mode
3861 cx.set_shared_state(indoc! {"
3862 The ˇquick brown
3863 fox jumps over
3864 the lazy dog
3865 The quick brown
3866 fox jumps over
3867 the lazy dog
3868 The quick brown
3869 fox jumps over
3870 the lazy dog"})
3871 .await;
3872 cx.simulate_shared_keystrokes("2 0 %").await;
3873 cx.shared_state().await.assert_eq(indoc! {"
3874 The quick brown
3875 fox ˇjumps over
3876 the lazy dog
3877 The quick brown
3878 fox jumps over
3879 the lazy dog
3880 The quick brown
3881 fox jumps over
3882 the lazy dog"});
3883
3884 cx.simulate_shared_keystrokes("2 5 %").await;
3885 cx.shared_state().await.assert_eq(indoc! {"
3886 The quick brown
3887 fox jumps over
3888 the ˇlazy dog
3889 The quick brown
3890 fox jumps over
3891 the lazy dog
3892 The quick brown
3893 fox jumps over
3894 the lazy dog"});
3895
3896 cx.simulate_shared_keystrokes("7 5 %").await;
3897 cx.shared_state().await.assert_eq(indoc! {"
3898 The quick brown
3899 fox jumps over
3900 the lazy dog
3901 The quick brown
3902 fox jumps over
3903 the lazy dog
3904 The ˇquick brown
3905 fox jumps over
3906 the lazy dog"});
3907
3908 // Visual mode
3909 cx.set_shared_state(indoc! {"
3910 The ˇquick brown
3911 fox jumps over
3912 the lazy dog
3913 The quick brown
3914 fox jumps over
3915 the lazy dog
3916 The quick brown
3917 fox jumps over
3918 the lazy dog"})
3919 .await;
3920 cx.simulate_shared_keystrokes("v 5 0 %").await;
3921 cx.shared_state().await.assert_eq(indoc! {"
3922 The «quick brown
3923 fox jumps over
3924 the lazy dog
3925 The quick brown
3926 fox jˇ»umps over
3927 the lazy dog
3928 The quick brown
3929 fox jumps over
3930 the lazy dog"});
3931
3932 cx.set_shared_state(indoc! {"
3933 The ˇquick brown
3934 fox jumps over
3935 the lazy dog
3936 The quick brown
3937 fox jumps over
3938 the lazy dog
3939 The quick brown
3940 fox jumps over
3941 the lazy dog"})
3942 .await;
3943 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3944 cx.shared_state().await.assert_eq(indoc! {"
3945 The «quick brown
3946 fox jumps over
3947 the lazy dog
3948 The quick brown
3949 fox jumps over
3950 the lazy dog
3951 The quick brown
3952 fox jumps over
3953 the lˇ»azy dog"});
3954 }
3955
3956 #[gpui::test]
3957 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3958 let mut cx = NeovimBackedTestContext::new(cx).await;
3959
3960 cx.set_shared_state("ˇπππππ").await;
3961 cx.simulate_shared_keystrokes("3 space").await;
3962 cx.shared_state().await.assert_eq("πππˇππ");
3963 }
3964
3965 #[gpui::test]
3966 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3967 let mut cx = NeovimBackedTestContext::new(cx).await;
3968
3969 cx.set_shared_state(indoc! {"
3970 ππππˇπ
3971 πanotherline"})
3972 .await;
3973 cx.simulate_shared_keystrokes("4 space").await;
3974 cx.shared_state().await.assert_eq(indoc! {"
3975 πππππ
3976 πanˇotherline"});
3977 }
3978
3979 #[gpui::test]
3980 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3981 let mut cx = NeovimBackedTestContext::new(cx).await;
3982
3983 cx.set_shared_state(indoc! {"
3984 ππππ
3985 πanˇotherline"})
3986 .await;
3987 cx.simulate_shared_keystrokes("4 backspace").await;
3988 cx.shared_state().await.assert_eq(indoc! {"
3989 πππˇπ
3990 πanotherline"});
3991 }
3992
3993 #[gpui::test]
3994 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
3995 let mut cx = VimTestContext::new(cx, true).await;
3996 cx.set_state(
3997 indoc! {
3998 "func empty(a string) bool {
3999 ˇif a == \"\" {
4000 return true
4001 }
4002 return false
4003 }"
4004 },
4005 Mode::Normal,
4006 );
4007 cx.simulate_keystrokes("[ -");
4008 cx.assert_state(
4009 indoc! {
4010 "ˇfunc empty(a string) bool {
4011 if a == \"\" {
4012 return true
4013 }
4014 return false
4015 }"
4016 },
4017 Mode::Normal,
4018 );
4019 cx.simulate_keystrokes("] =");
4020 cx.assert_state(
4021 indoc! {
4022 "func empty(a string) bool {
4023 if a == \"\" {
4024 return true
4025 }
4026 return false
4027 ˇ}"
4028 },
4029 Mode::Normal,
4030 );
4031 cx.simulate_keystrokes("[ +");
4032 cx.assert_state(
4033 indoc! {
4034 "func empty(a string) bool {
4035 if a == \"\" {
4036 return true
4037 }
4038 ˇreturn false
4039 }"
4040 },
4041 Mode::Normal,
4042 );
4043 cx.simulate_keystrokes("2 [ =");
4044 cx.assert_state(
4045 indoc! {
4046 "func empty(a string) bool {
4047 ˇif a == \"\" {
4048 return true
4049 }
4050 return false
4051 }"
4052 },
4053 Mode::Normal,
4054 );
4055 cx.simulate_keystrokes("] +");
4056 cx.assert_state(
4057 indoc! {
4058 "func empty(a string) bool {
4059 if a == \"\" {
4060 ˇreturn true
4061 }
4062 return false
4063 }"
4064 },
4065 Mode::Normal,
4066 );
4067 cx.simulate_keystrokes("] -");
4068 cx.assert_state(
4069 indoc! {
4070 "func empty(a string) bool {
4071 if a == \"\" {
4072 return true
4073 ˇ}
4074 return false
4075 }"
4076 },
4077 Mode::Normal,
4078 );
4079 }
4080
4081 #[gpui::test]
4082 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
4083 let mut cx = NeovimBackedTestContext::new(cx).await;
4084 cx.set_shared_state("abˇc").await;
4085 cx.simulate_shared_keystrokes("delete").await;
4086 cx.shared_state().await.assert_eq("aˇb");
4087 }
4088
4089 #[gpui::test]
4090 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
4091 let mut cx = NeovimBackedTestContext::new(cx).await;
4092
4093 cx.set_shared_state(indoc! {"
4094 ˇthe quick brown fox
4095 jumped over the lazy dog"})
4096 .await;
4097 cx.simulate_shared_keystrokes("d v 0").await;
4098 cx.shared_state().await.assert_eq(indoc! {"
4099 ˇhe quick brown fox
4100 jumped over the lazy dog"});
4101 assert_eq!(cx.cx.forced_motion(), false);
4102
4103 cx.set_shared_state(indoc! {"
4104 the quick bˇrown fox
4105 jumped over the lazy dog"})
4106 .await;
4107 cx.simulate_shared_keystrokes("d v 0").await;
4108 cx.shared_state().await.assert_eq(indoc! {"
4109 ˇown fox
4110 jumped over the lazy dog"});
4111 assert_eq!(cx.cx.forced_motion(), false);
4112
4113 cx.set_shared_state(indoc! {"
4114 the quick brown foˇx
4115 jumped over the lazy dog"})
4116 .await;
4117 cx.simulate_shared_keystrokes("d v 0").await;
4118 cx.shared_state().await.assert_eq(indoc! {"
4119 ˇ
4120 jumped over the lazy dog"});
4121 assert_eq!(cx.cx.forced_motion(), false);
4122 }
4123
4124 #[gpui::test]
4125 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
4126 let mut cx = NeovimBackedTestContext::new(cx).await;
4127
4128 cx.set_shared_state(indoc! {"
4129 ˇthe quick brown fox
4130 jumped over the lazy dog"})
4131 .await;
4132 cx.simulate_shared_keystrokes("d v g shift-m").await;
4133 cx.shared_state().await.assert_eq(indoc! {"
4134 ˇbrown fox
4135 jumped over the lazy dog"});
4136 assert_eq!(cx.cx.forced_motion(), false);
4137
4138 cx.set_shared_state(indoc! {"
4139 the quick bˇrown fox
4140 jumped over the lazy dog"})
4141 .await;
4142 cx.simulate_shared_keystrokes("d v g shift-m").await;
4143 cx.shared_state().await.assert_eq(indoc! {"
4144 the quickˇown fox
4145 jumped over the lazy dog"});
4146 assert_eq!(cx.cx.forced_motion(), false);
4147
4148 cx.set_shared_state(indoc! {"
4149 the quick brown foˇx
4150 jumped over the lazy dog"})
4151 .await;
4152 cx.simulate_shared_keystrokes("d v g shift-m").await;
4153 cx.shared_state().await.assert_eq(indoc! {"
4154 the quicˇk
4155 jumped over the lazy dog"});
4156 assert_eq!(cx.cx.forced_motion(), false);
4157
4158 cx.set_shared_state(indoc! {"
4159 ˇthe quick brown fox
4160 jumped over the lazy dog"})
4161 .await;
4162 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4163 cx.shared_state().await.assert_eq(indoc! {"
4164 ˇ fox
4165 jumped over the lazy dog"});
4166 assert_eq!(cx.cx.forced_motion(), false);
4167
4168 cx.set_shared_state(indoc! {"
4169 ˇthe quick brown fox
4170 jumped over the lazy dog"})
4171 .await;
4172 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4173 cx.shared_state().await.assert_eq(indoc! {"
4174 ˇuick brown fox
4175 jumped over the lazy dog"});
4176 assert_eq!(cx.cx.forced_motion(), false);
4177 }
4178
4179 #[gpui::test]
4180 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4181 let mut cx = NeovimBackedTestContext::new(cx).await;
4182
4183 cx.set_shared_state(indoc! {"
4184 the quick brown foˇx
4185 jumped over the lazy dog"})
4186 .await;
4187 cx.simulate_shared_keystrokes("d v $").await;
4188 cx.shared_state().await.assert_eq(indoc! {"
4189 the quick brown foˇx
4190 jumped over the lazy dog"});
4191 assert_eq!(cx.cx.forced_motion(), false);
4192
4193 cx.set_shared_state(indoc! {"
4194 ˇthe quick brown fox
4195 jumped over the lazy dog"})
4196 .await;
4197 cx.simulate_shared_keystrokes("d v $").await;
4198 cx.shared_state().await.assert_eq(indoc! {"
4199 ˇx
4200 jumped over the lazy dog"});
4201 assert_eq!(cx.cx.forced_motion(), false);
4202 }
4203
4204 #[gpui::test]
4205 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4206 let mut cx = NeovimBackedTestContext::new(cx).await;
4207
4208 cx.set_shared_state(indoc! {"
4209 ˇthe quick brown fox
4210 jumped over the lazy dog"})
4211 .await;
4212 cx.simulate_shared_keystrokes("y v j p").await;
4213 cx.shared_state().await.assert_eq(indoc! {"
4214 the quick brown fox
4215 ˇthe quick brown fox
4216 jumped over the lazy dog"});
4217 assert_eq!(cx.cx.forced_motion(), false);
4218
4219 cx.set_shared_state(indoc! {"
4220 the quick bˇrown fox
4221 jumped over the lazy dog"})
4222 .await;
4223 cx.simulate_shared_keystrokes("y v j p").await;
4224 cx.shared_state().await.assert_eq(indoc! {"
4225 the quick brˇrown fox
4226 jumped overown fox
4227 jumped over the lazy dog"});
4228 assert_eq!(cx.cx.forced_motion(), false);
4229
4230 cx.set_shared_state(indoc! {"
4231 the quick brown foˇx
4232 jumped over the lazy dog"})
4233 .await;
4234 cx.simulate_shared_keystrokes("y v j p").await;
4235 cx.shared_state().await.assert_eq(indoc! {"
4236 the quick brown foxˇx
4237 jumped over the la
4238 jumped over the lazy dog"});
4239 assert_eq!(cx.cx.forced_motion(), false);
4240
4241 cx.set_shared_state(indoc! {"
4242 the quick brown fox
4243 jˇumped over the lazy dog"})
4244 .await;
4245 cx.simulate_shared_keystrokes("y v k p").await;
4246 cx.shared_state().await.assert_eq(indoc! {"
4247 thˇhe quick brown fox
4248 je quick brown fox
4249 jumped over the lazy dog"});
4250 assert_eq!(cx.cx.forced_motion(), false);
4251 }
4252
4253 #[gpui::test]
4254 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4255 let mut cx = NeovimBackedTestContext::new(cx).await;
4256
4257 cx.set_shared_state(indoc! {"
4258 ˇthe quick brown fox
4259 jumped over the lazy dog"})
4260 .await;
4261 cx.simulate_shared_keystrokes("d v e").await;
4262 cx.shared_state().await.assert_eq(indoc! {"
4263 ˇe quick brown fox
4264 jumped over the lazy dog"});
4265 assert_eq!(cx.cx.forced_motion(), false);
4266
4267 cx.set_shared_state(indoc! {"
4268 the quick bˇrown fox
4269 jumped over the lazy dog"})
4270 .await;
4271 cx.simulate_shared_keystrokes("d v e").await;
4272 cx.shared_state().await.assert_eq(indoc! {"
4273 the quick bˇn fox
4274 jumped over the lazy dog"});
4275 assert_eq!(cx.cx.forced_motion(), false);
4276
4277 cx.set_shared_state(indoc! {"
4278 the quick brown foˇx
4279 jumped over the lazy dog"})
4280 .await;
4281 cx.simulate_shared_keystrokes("d v e").await;
4282 cx.shared_state().await.assert_eq(indoc! {"
4283 the quick brown foˇd over the lazy dog"});
4284 assert_eq!(cx.cx.forced_motion(), false);
4285 }
4286}