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