1use editor::{
2 display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
3 movement::{
4 self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
5 },
6 scroll::Autoscroll,
7 Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset,
8};
9use gpui::{actions, impl_actions, px, ViewContext};
10use language::{CharKind, Point, Selection, SelectionGoal};
11use multi_buffer::MultiBufferRow;
12use serde::Deserialize;
13use std::ops::Range;
14use workspace::searchable::Direction;
15
16use crate::{
17 normal::mark,
18 state::{Mode, Operator},
19 surrounds::SurroundsType,
20 Vim,
21};
22
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum Motion {
25 Left,
26 Backspace,
27 Down {
28 display_lines: bool,
29 },
30 Up {
31 display_lines: bool,
32 },
33 Right,
34 Space,
35 NextWordStart {
36 ignore_punctuation: bool,
37 },
38 NextWordEnd {
39 ignore_punctuation: bool,
40 },
41 PreviousWordStart {
42 ignore_punctuation: bool,
43 },
44 PreviousWordEnd {
45 ignore_punctuation: bool,
46 },
47 NextSubwordStart {
48 ignore_punctuation: bool,
49 },
50 NextSubwordEnd {
51 ignore_punctuation: bool,
52 },
53 PreviousSubwordStart {
54 ignore_punctuation: bool,
55 },
56 PreviousSubwordEnd {
57 ignore_punctuation: bool,
58 },
59 FirstNonWhitespace {
60 display_lines: bool,
61 },
62 CurrentLine,
63 StartOfLine {
64 display_lines: bool,
65 },
66 EndOfLine {
67 display_lines: bool,
68 },
69 SentenceBackward,
70 SentenceForward,
71 StartOfParagraph,
72 EndOfParagraph,
73 StartOfDocument,
74 EndOfDocument,
75 Matching,
76 UnmatchedForward {
77 char: char,
78 },
79 UnmatchedBackward {
80 char: char,
81 },
82 FindForward {
83 before: bool,
84 char: char,
85 mode: FindRange,
86 smartcase: bool,
87 },
88 FindBackward {
89 after: bool,
90 char: char,
91 mode: FindRange,
92 smartcase: bool,
93 },
94 RepeatFind {
95 last_find: Box<Motion>,
96 },
97 RepeatFindReversed {
98 last_find: Box<Motion>,
99 },
100 NextLineStart,
101 PreviousLineStart,
102 StartOfLineDownward,
103 EndOfLineDownward,
104 GoToColumn,
105 WindowTop,
106 WindowMiddle,
107 WindowBottom,
108 NextSectionStart,
109 NextSectionEnd,
110 PreviousSectionStart,
111 PreviousSectionEnd,
112 NextMethodStart,
113 NextMethodEnd,
114 PreviousMethodStart,
115 PreviousMethodEnd,
116 NextComment,
117 PreviousComment,
118
119 // we don't have a good way to run a search synchronously, so
120 // we handle search motions by running the search async and then
121 // calling back into motion with this
122 ZedSearchResult {
123 prior_selections: Vec<Range<Anchor>>,
124 new_selections: Vec<Range<Anchor>>,
125 },
126 Jump {
127 anchor: Anchor,
128 line: bool,
129 },
130}
131
132#[derive(Clone, Deserialize, PartialEq)]
133#[serde(rename_all = "camelCase")]
134struct NextWordStart {
135 #[serde(default)]
136 ignore_punctuation: bool,
137}
138
139#[derive(Clone, Deserialize, PartialEq)]
140#[serde(rename_all = "camelCase")]
141struct NextWordEnd {
142 #[serde(default)]
143 ignore_punctuation: bool,
144}
145
146#[derive(Clone, Deserialize, PartialEq)]
147#[serde(rename_all = "camelCase")]
148struct PreviousWordStart {
149 #[serde(default)]
150 ignore_punctuation: bool,
151}
152
153#[derive(Clone, Deserialize, PartialEq)]
154#[serde(rename_all = "camelCase")]
155struct PreviousWordEnd {
156 #[serde(default)]
157 ignore_punctuation: bool,
158}
159
160#[derive(Clone, Deserialize, PartialEq)]
161#[serde(rename_all = "camelCase")]
162pub(crate) struct NextSubwordStart {
163 #[serde(default)]
164 pub(crate) ignore_punctuation: bool,
165}
166
167#[derive(Clone, Deserialize, PartialEq)]
168#[serde(rename_all = "camelCase")]
169pub(crate) struct NextSubwordEnd {
170 #[serde(default)]
171 pub(crate) ignore_punctuation: bool,
172}
173
174#[derive(Clone, Deserialize, PartialEq)]
175#[serde(rename_all = "camelCase")]
176pub(crate) struct PreviousSubwordStart {
177 #[serde(default)]
178 pub(crate) ignore_punctuation: bool,
179}
180
181#[derive(Clone, Deserialize, PartialEq)]
182#[serde(rename_all = "camelCase")]
183pub(crate) struct PreviousSubwordEnd {
184 #[serde(default)]
185 pub(crate) ignore_punctuation: bool,
186}
187
188#[derive(Clone, Deserialize, PartialEq)]
189#[serde(rename_all = "camelCase")]
190pub(crate) struct Up {
191 #[serde(default)]
192 pub(crate) display_lines: bool,
193}
194
195#[derive(Clone, Deserialize, PartialEq)]
196#[serde(rename_all = "camelCase")]
197pub(crate) struct Down {
198 #[serde(default)]
199 pub(crate) display_lines: bool,
200}
201
202#[derive(Clone, Deserialize, PartialEq)]
203#[serde(rename_all = "camelCase")]
204struct FirstNonWhitespace {
205 #[serde(default)]
206 display_lines: bool,
207}
208
209#[derive(Clone, Deserialize, PartialEq)]
210#[serde(rename_all = "camelCase")]
211struct EndOfLine {
212 #[serde(default)]
213 display_lines: bool,
214}
215
216#[derive(Clone, Deserialize, PartialEq)]
217#[serde(rename_all = "camelCase")]
218pub struct StartOfLine {
219 #[serde(default)]
220 pub(crate) display_lines: bool,
221}
222
223#[derive(Clone, Deserialize, PartialEq)]
224#[serde(rename_all = "camelCase")]
225struct UnmatchedForward {
226 #[serde(default)]
227 char: char,
228}
229
230#[derive(Clone, Deserialize, PartialEq)]
231#[serde(rename_all = "camelCase")]
232struct UnmatchedBackward {
233 #[serde(default)]
234 char: char,
235}
236
237impl_actions!(
238 vim,
239 [
240 StartOfLine,
241 EndOfLine,
242 FirstNonWhitespace,
243 Down,
244 Up,
245 NextWordStart,
246 NextWordEnd,
247 PreviousWordStart,
248 PreviousWordEnd,
249 NextSubwordStart,
250 NextSubwordEnd,
251 PreviousSubwordStart,
252 PreviousSubwordEnd,
253 UnmatchedForward,
254 UnmatchedBackward
255 ]
256);
257
258actions!(
259 vim,
260 [
261 Left,
262 Backspace,
263 Right,
264 Space,
265 CurrentLine,
266 SentenceForward,
267 SentenceBackward,
268 StartOfParagraph,
269 EndOfParagraph,
270 StartOfDocument,
271 EndOfDocument,
272 Matching,
273 NextLineStart,
274 PreviousLineStart,
275 StartOfLineDownward,
276 EndOfLineDownward,
277 GoToColumn,
278 RepeatFind,
279 RepeatFindReversed,
280 WindowTop,
281 WindowMiddle,
282 WindowBottom,
283 NextSectionStart,
284 NextSectionEnd,
285 PreviousSectionStart,
286 PreviousSectionEnd,
287 NextMethodStart,
288 NextMethodEnd,
289 PreviousMethodStart,
290 PreviousMethodEnd,
291 NextComment,
292 PreviousComment,
293 ]
294);
295
296pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
297 Vim::action(editor, cx, |vim, _: &Left, cx| vim.motion(Motion::Left, cx));
298 Vim::action(editor, cx, |vim, _: &Backspace, cx| {
299 vim.motion(Motion::Backspace, cx)
300 });
301 Vim::action(editor, cx, |vim, action: &Down, cx| {
302 vim.motion(
303 Motion::Down {
304 display_lines: action.display_lines,
305 },
306 cx,
307 )
308 });
309 Vim::action(editor, cx, |vim, action: &Up, cx| {
310 vim.motion(
311 Motion::Up {
312 display_lines: action.display_lines,
313 },
314 cx,
315 )
316 });
317 Vim::action(editor, cx, |vim, _: &Right, cx| {
318 vim.motion(Motion::Right, cx)
319 });
320 Vim::action(editor, cx, |vim, _: &Space, cx| {
321 vim.motion(Motion::Space, cx)
322 });
323 Vim::action(editor, cx, |vim, action: &FirstNonWhitespace, cx| {
324 vim.motion(
325 Motion::FirstNonWhitespace {
326 display_lines: action.display_lines,
327 },
328 cx,
329 )
330 });
331 Vim::action(editor, cx, |vim, action: &StartOfLine, cx| {
332 vim.motion(
333 Motion::StartOfLine {
334 display_lines: action.display_lines,
335 },
336 cx,
337 )
338 });
339 Vim::action(editor, cx, |vim, action: &EndOfLine, cx| {
340 vim.motion(
341 Motion::EndOfLine {
342 display_lines: action.display_lines,
343 },
344 cx,
345 )
346 });
347 Vim::action(editor, cx, |vim, _: &CurrentLine, cx| {
348 vim.motion(Motion::CurrentLine, cx)
349 });
350 Vim::action(editor, cx, |vim, _: &StartOfParagraph, cx| {
351 vim.motion(Motion::StartOfParagraph, cx)
352 });
353 Vim::action(editor, cx, |vim, _: &EndOfParagraph, cx| {
354 vim.motion(Motion::EndOfParagraph, cx)
355 });
356
357 Vim::action(editor, cx, |vim, _: &SentenceForward, cx| {
358 vim.motion(Motion::SentenceForward, cx)
359 });
360 Vim::action(editor, cx, |vim, _: &SentenceBackward, cx| {
361 vim.motion(Motion::SentenceBackward, cx)
362 });
363 Vim::action(editor, cx, |vim, _: &StartOfDocument, cx| {
364 vim.motion(Motion::StartOfDocument, cx)
365 });
366 Vim::action(editor, cx, |vim, _: &EndOfDocument, cx| {
367 vim.motion(Motion::EndOfDocument, cx)
368 });
369 Vim::action(editor, cx, |vim, _: &Matching, cx| {
370 vim.motion(Motion::Matching, cx)
371 });
372 Vim::action(
373 editor,
374 cx,
375 |vim, &UnmatchedForward { char }: &UnmatchedForward, cx| {
376 vim.motion(Motion::UnmatchedForward { char }, cx)
377 },
378 );
379 Vim::action(
380 editor,
381 cx,
382 |vim, &UnmatchedBackward { char }: &UnmatchedBackward, cx| {
383 vim.motion(Motion::UnmatchedBackward { char }, cx)
384 },
385 );
386 Vim::action(
387 editor,
388 cx,
389 |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, cx| {
390 vim.motion(Motion::NextWordStart { ignore_punctuation }, cx)
391 },
392 );
393 Vim::action(
394 editor,
395 cx,
396 |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx| {
397 vim.motion(Motion::NextWordEnd { ignore_punctuation }, cx)
398 },
399 );
400 Vim::action(
401 editor,
402 cx,
403 |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, cx| {
404 vim.motion(Motion::PreviousWordStart { ignore_punctuation }, cx)
405 },
406 );
407 Vim::action(
408 editor,
409 cx,
410 |vim, &PreviousWordEnd { ignore_punctuation }, cx| {
411 vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
412 },
413 );
414 Vim::action(
415 editor,
416 cx,
417 |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx| {
418 vim.motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
419 },
420 );
421 Vim::action(
422 editor,
423 cx,
424 |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx| {
425 vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
426 },
427 );
428 Vim::action(
429 editor,
430 cx,
431 |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, cx| {
432 vim.motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx)
433 },
434 );
435 Vim::action(
436 editor,
437 cx,
438 |vim, &PreviousSubwordEnd { ignore_punctuation }, cx| {
439 vim.motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
440 },
441 );
442 Vim::action(editor, cx, |vim, &NextLineStart, cx| {
443 vim.motion(Motion::NextLineStart, cx)
444 });
445 Vim::action(editor, cx, |vim, &PreviousLineStart, cx| {
446 vim.motion(Motion::PreviousLineStart, cx)
447 });
448 Vim::action(editor, cx, |vim, &StartOfLineDownward, cx| {
449 vim.motion(Motion::StartOfLineDownward, cx)
450 });
451 Vim::action(editor, cx, |vim, &EndOfLineDownward, cx| {
452 vim.motion(Motion::EndOfLineDownward, cx)
453 });
454 Vim::action(editor, cx, |vim, &GoToColumn, cx| {
455 vim.motion(Motion::GoToColumn, cx)
456 });
457
458 Vim::action(editor, cx, |vim, _: &RepeatFind, cx| {
459 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
460 vim.motion(Motion::RepeatFind { last_find }, cx);
461 }
462 });
463
464 Vim::action(editor, cx, |vim, _: &RepeatFindReversed, cx| {
465 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
466 vim.motion(Motion::RepeatFindReversed { last_find }, cx);
467 }
468 });
469 Vim::action(editor, cx, |vim, &WindowTop, cx| {
470 vim.motion(Motion::WindowTop, cx)
471 });
472 Vim::action(editor, cx, |vim, &WindowMiddle, cx| {
473 vim.motion(Motion::WindowMiddle, cx)
474 });
475 Vim::action(editor, cx, |vim, &WindowBottom, cx| {
476 vim.motion(Motion::WindowBottom, cx)
477 });
478
479 Vim::action(editor, cx, |vim, &PreviousSectionStart, cx| {
480 vim.motion(Motion::PreviousSectionStart, cx)
481 });
482 Vim::action(editor, cx, |vim, &NextSectionStart, cx| {
483 vim.motion(Motion::NextSectionStart, cx)
484 });
485 Vim::action(editor, cx, |vim, &PreviousSectionEnd, cx| {
486 vim.motion(Motion::PreviousSectionEnd, cx)
487 });
488 Vim::action(editor, cx, |vim, &NextSectionEnd, cx| {
489 vim.motion(Motion::NextSectionEnd, cx)
490 });
491 Vim::action(editor, cx, |vim, &PreviousMethodStart, cx| {
492 vim.motion(Motion::PreviousMethodStart, cx)
493 });
494 Vim::action(editor, cx, |vim, &NextMethodStart, cx| {
495 vim.motion(Motion::NextMethodStart, cx)
496 });
497 Vim::action(editor, cx, |vim, &PreviousMethodEnd, cx| {
498 vim.motion(Motion::PreviousMethodEnd, cx)
499 });
500 Vim::action(editor, cx, |vim, &NextMethodEnd, cx| {
501 vim.motion(Motion::NextMethodEnd, cx)
502 });
503 Vim::action(editor, cx, |vim, &NextComment, cx| {
504 vim.motion(Motion::NextComment, cx)
505 });
506 Vim::action(editor, cx, |vim, &PreviousComment, cx| {
507 vim.motion(Motion::PreviousComment, cx)
508 });
509}
510
511impl Vim {
512 pub(crate) fn search_motion(&mut self, m: Motion, cx: &mut ViewContext<Self>) {
513 if let Motion::ZedSearchResult {
514 prior_selections, ..
515 } = &m
516 {
517 match self.mode {
518 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
519 if !prior_selections.is_empty() {
520 self.update_editor(cx, |_, editor, cx| {
521 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
522 s.select_ranges(prior_selections.iter().cloned())
523 })
524 });
525 }
526 }
527 Mode::Normal | Mode::Replace | Mode::Insert => {
528 if self.active_operator().is_none() {
529 return;
530 }
531 }
532
533 Mode::HelixNormal => {}
534 }
535 }
536
537 self.motion(m, cx)
538 }
539
540 pub(crate) fn motion(&mut self, motion: Motion, cx: &mut ViewContext<Self>) {
541 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
542 self.active_operator()
543 {
544 self.pop_operator(cx);
545 }
546
547 let count = Vim::take_count(cx);
548 let active_operator = self.active_operator();
549 let mut waiting_operator: Option<Operator> = None;
550 match self.mode {
551 Mode::Normal | Mode::Replace | Mode::Insert => {
552 if active_operator == Some(Operator::AddSurrounds { target: None }) {
553 waiting_operator = Some(Operator::AddSurrounds {
554 target: Some(SurroundsType::Motion(motion)),
555 });
556 } else {
557 self.normal_motion(motion.clone(), active_operator.clone(), count, cx)
558 }
559 }
560 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
561 self.visual_motion(motion.clone(), count, cx)
562 }
563
564 Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, cx),
565 }
566 self.clear_operator(cx);
567 if let Some(operator) = waiting_operator {
568 self.push_operator(operator, cx);
569 Vim::globals(cx).pre_count = count
570 }
571 }
572}
573
574// Motion handling is specified here:
575// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
576impl Motion {
577 pub fn linewise(&self) -> bool {
578 use Motion::*;
579 match self {
580 Down { .. }
581 | Up { .. }
582 | StartOfDocument
583 | EndOfDocument
584 | CurrentLine
585 | NextLineStart
586 | PreviousLineStart
587 | StartOfLineDownward
588 | StartOfParagraph
589 | EndOfParagraph
590 | WindowTop
591 | WindowMiddle
592 | WindowBottom
593 | NextSectionStart
594 | NextSectionEnd
595 | PreviousSectionStart
596 | PreviousSectionEnd
597 | NextMethodStart
598 | NextMethodEnd
599 | PreviousMethodStart
600 | PreviousMethodEnd
601 | NextComment
602 | PreviousComment
603 | Jump { line: true, .. } => true,
604 EndOfLine { .. }
605 | Matching
606 | UnmatchedForward { .. }
607 | UnmatchedBackward { .. }
608 | FindForward { .. }
609 | Left
610 | Backspace
611 | Right
612 | SentenceBackward
613 | SentenceForward
614 | Space
615 | StartOfLine { .. }
616 | EndOfLineDownward
617 | GoToColumn
618 | NextWordStart { .. }
619 | NextWordEnd { .. }
620 | PreviousWordStart { .. }
621 | PreviousWordEnd { .. }
622 | NextSubwordStart { .. }
623 | NextSubwordEnd { .. }
624 | PreviousSubwordStart { .. }
625 | PreviousSubwordEnd { .. }
626 | FirstNonWhitespace { .. }
627 | FindBackward { .. }
628 | RepeatFind { .. }
629 | RepeatFindReversed { .. }
630 | Jump { line: false, .. }
631 | ZedSearchResult { .. } => false,
632 }
633 }
634
635 pub fn infallible(&self) -> bool {
636 use Motion::*;
637 match self {
638 StartOfDocument | EndOfDocument | CurrentLine => true,
639 Down { .. }
640 | Up { .. }
641 | EndOfLine { .. }
642 | Matching
643 | UnmatchedForward { .. }
644 | UnmatchedBackward { .. }
645 | FindForward { .. }
646 | RepeatFind { .. }
647 | Left
648 | Backspace
649 | Right
650 | Space
651 | StartOfLine { .. }
652 | StartOfParagraph
653 | EndOfParagraph
654 | SentenceBackward
655 | SentenceForward
656 | StartOfLineDownward
657 | EndOfLineDownward
658 | GoToColumn
659 | NextWordStart { .. }
660 | NextWordEnd { .. }
661 | PreviousWordStart { .. }
662 | PreviousWordEnd { .. }
663 | NextSubwordStart { .. }
664 | NextSubwordEnd { .. }
665 | PreviousSubwordStart { .. }
666 | PreviousSubwordEnd { .. }
667 | FirstNonWhitespace { .. }
668 | FindBackward { .. }
669 | RepeatFindReversed { .. }
670 | WindowTop
671 | WindowMiddle
672 | WindowBottom
673 | NextLineStart
674 | PreviousLineStart
675 | ZedSearchResult { .. }
676 | NextSectionStart
677 | NextSectionEnd
678 | PreviousSectionStart
679 | PreviousSectionEnd
680 | NextMethodStart
681 | NextMethodEnd
682 | PreviousMethodStart
683 | PreviousMethodEnd
684 | NextComment
685 | PreviousComment
686 | Jump { .. } => false,
687 }
688 }
689
690 pub fn inclusive(&self) -> bool {
691 use Motion::*;
692 match self {
693 Down { .. }
694 | Up { .. }
695 | StartOfDocument
696 | EndOfDocument
697 | CurrentLine
698 | EndOfLine { .. }
699 | EndOfLineDownward
700 | Matching
701 | UnmatchedForward { .. }
702 | UnmatchedBackward { .. }
703 | FindForward { .. }
704 | WindowTop
705 | WindowMiddle
706 | WindowBottom
707 | NextWordEnd { .. }
708 | PreviousWordEnd { .. }
709 | NextSubwordEnd { .. }
710 | PreviousSubwordEnd { .. }
711 | NextLineStart
712 | PreviousLineStart => true,
713 Left
714 | Backspace
715 | Right
716 | Space
717 | StartOfLine { .. }
718 | StartOfLineDownward
719 | StartOfParagraph
720 | EndOfParagraph
721 | SentenceBackward
722 | SentenceForward
723 | GoToColumn
724 | NextWordStart { .. }
725 | PreviousWordStart { .. }
726 | NextSubwordStart { .. }
727 | PreviousSubwordStart { .. }
728 | FirstNonWhitespace { .. }
729 | FindBackward { .. }
730 | Jump { .. }
731 | NextSectionStart
732 | NextSectionEnd
733 | PreviousSectionStart
734 | PreviousSectionEnd
735 | NextMethodStart
736 | NextMethodEnd
737 | PreviousMethodStart
738 | PreviousMethodEnd
739 | NextComment
740 | PreviousComment
741 | ZedSearchResult { .. } => false,
742 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
743 motion.inclusive()
744 }
745 }
746 }
747
748 pub fn move_point(
749 &self,
750 map: &DisplaySnapshot,
751 point: DisplayPoint,
752 goal: SelectionGoal,
753 maybe_times: Option<usize>,
754 text_layout_details: &TextLayoutDetails,
755 ) -> Option<(DisplayPoint, SelectionGoal)> {
756 let times = maybe_times.unwrap_or(1);
757 use Motion::*;
758 let infallible = self.infallible();
759 let (new_point, goal) = match self {
760 Left => (left(map, point, times), SelectionGoal::None),
761 Backspace => (backspace(map, point, times), SelectionGoal::None),
762 Down {
763 display_lines: false,
764 } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
765 Down {
766 display_lines: true,
767 } => down_display(map, point, goal, times, text_layout_details),
768 Up {
769 display_lines: false,
770 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details),
771 Up {
772 display_lines: true,
773 } => up_display(map, point, goal, times, text_layout_details),
774 Right => (right(map, point, times), SelectionGoal::None),
775 Space => (space(map, point, times), SelectionGoal::None),
776 NextWordStart { ignore_punctuation } => (
777 next_word_start(map, point, *ignore_punctuation, times),
778 SelectionGoal::None,
779 ),
780 NextWordEnd { ignore_punctuation } => (
781 next_word_end(map, point, *ignore_punctuation, times, true),
782 SelectionGoal::None,
783 ),
784 PreviousWordStart { ignore_punctuation } => (
785 previous_word_start(map, point, *ignore_punctuation, times),
786 SelectionGoal::None,
787 ),
788 PreviousWordEnd { ignore_punctuation } => (
789 previous_word_end(map, point, *ignore_punctuation, times),
790 SelectionGoal::None,
791 ),
792 NextSubwordStart { ignore_punctuation } => (
793 next_subword_start(map, point, *ignore_punctuation, times),
794 SelectionGoal::None,
795 ),
796 NextSubwordEnd { ignore_punctuation } => (
797 next_subword_end(map, point, *ignore_punctuation, times, true),
798 SelectionGoal::None,
799 ),
800 PreviousSubwordStart { ignore_punctuation } => (
801 previous_subword_start(map, point, *ignore_punctuation, times),
802 SelectionGoal::None,
803 ),
804 PreviousSubwordEnd { ignore_punctuation } => (
805 previous_subword_end(map, point, *ignore_punctuation, times),
806 SelectionGoal::None,
807 ),
808 FirstNonWhitespace { display_lines } => (
809 first_non_whitespace(map, *display_lines, point),
810 SelectionGoal::None,
811 ),
812 StartOfLine { display_lines } => (
813 start_of_line(map, *display_lines, point),
814 SelectionGoal::None,
815 ),
816 EndOfLine { display_lines } => (
817 end_of_line(map, *display_lines, point, times),
818 SelectionGoal::None,
819 ),
820 SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
821 SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
822 StartOfParagraph => (
823 movement::start_of_paragraph(map, point, times),
824 SelectionGoal::None,
825 ),
826 EndOfParagraph => (
827 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
828 SelectionGoal::None,
829 ),
830 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
831 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
832 EndOfDocument => (
833 end_of_document(map, point, maybe_times),
834 SelectionGoal::None,
835 ),
836 Matching => (matching(map, point), SelectionGoal::None),
837 UnmatchedForward { char } => (
838 unmatched_forward(map, point, *char, times),
839 SelectionGoal::None,
840 ),
841 UnmatchedBackward { char } => (
842 unmatched_backward(map, point, *char, times),
843 SelectionGoal::None,
844 ),
845 // t f
846 FindForward {
847 before,
848 char,
849 mode,
850 smartcase,
851 } => {
852 return find_forward(map, point, *before, *char, times, *mode, *smartcase)
853 .map(|new_point| (new_point, SelectionGoal::None))
854 }
855 // T F
856 FindBackward {
857 after,
858 char,
859 mode,
860 smartcase,
861 } => (
862 find_backward(map, point, *after, *char, times, *mode, *smartcase),
863 SelectionGoal::None,
864 ),
865 // ; -- repeat the last find done with t, f, T, F
866 RepeatFind { last_find } => match **last_find {
867 Motion::FindForward {
868 before,
869 char,
870 mode,
871 smartcase,
872 } => {
873 let mut new_point =
874 find_forward(map, point, before, char, times, mode, smartcase);
875 if new_point == Some(point) {
876 new_point =
877 find_forward(map, point, before, char, times + 1, mode, smartcase);
878 }
879
880 return new_point.map(|new_point| (new_point, SelectionGoal::None));
881 }
882
883 Motion::FindBackward {
884 after,
885 char,
886 mode,
887 smartcase,
888 } => {
889 let mut new_point =
890 find_backward(map, point, after, char, times, mode, smartcase);
891 if new_point == point {
892 new_point =
893 find_backward(map, point, after, char, times + 1, mode, smartcase);
894 }
895
896 (new_point, SelectionGoal::None)
897 }
898 _ => return None,
899 },
900 // , -- repeat the last find done with t, f, T, F, in opposite direction
901 RepeatFindReversed { last_find } => match **last_find {
902 Motion::FindForward {
903 before,
904 char,
905 mode,
906 smartcase,
907 } => {
908 let mut new_point =
909 find_backward(map, point, before, char, times, mode, smartcase);
910 if new_point == point {
911 new_point =
912 find_backward(map, point, before, char, times + 1, mode, smartcase);
913 }
914
915 (new_point, SelectionGoal::None)
916 }
917
918 Motion::FindBackward {
919 after,
920 char,
921 mode,
922 smartcase,
923 } => {
924 let mut new_point =
925 find_forward(map, point, after, char, times, mode, smartcase);
926 if new_point == Some(point) {
927 new_point =
928 find_forward(map, point, after, char, times + 1, mode, smartcase);
929 }
930
931 return new_point.map(|new_point| (new_point, SelectionGoal::None));
932 }
933 _ => return None,
934 },
935 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
936 PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
937 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
938 EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
939 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
940 WindowTop => window_top(map, point, text_layout_details, times - 1),
941 WindowMiddle => window_middle(map, point, text_layout_details),
942 WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
943 Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
944 ZedSearchResult { new_selections, .. } => {
945 // There will be only one selection, as
946 // Search::SelectNextMatch selects a single match.
947 if let Some(new_selection) = new_selections.first() {
948 (
949 new_selection.start.to_display_point(map),
950 SelectionGoal::None,
951 )
952 } else {
953 return None;
954 }
955 }
956 NextSectionStart => (
957 section_motion(map, point, times, Direction::Next, true),
958 SelectionGoal::None,
959 ),
960 NextSectionEnd => (
961 section_motion(map, point, times, Direction::Next, false),
962 SelectionGoal::None,
963 ),
964 PreviousSectionStart => (
965 section_motion(map, point, times, Direction::Prev, true),
966 SelectionGoal::None,
967 ),
968 PreviousSectionEnd => (
969 section_motion(map, point, times, Direction::Prev, false),
970 SelectionGoal::None,
971 ),
972
973 NextMethodStart => (
974 method_motion(map, point, times, Direction::Next, true),
975 SelectionGoal::None,
976 ),
977 NextMethodEnd => (
978 method_motion(map, point, times, Direction::Next, false),
979 SelectionGoal::None,
980 ),
981 PreviousMethodStart => (
982 method_motion(map, point, times, Direction::Prev, true),
983 SelectionGoal::None,
984 ),
985 PreviousMethodEnd => (
986 method_motion(map, point, times, Direction::Prev, false),
987 SelectionGoal::None,
988 ),
989 NextComment => (
990 comment_motion(map, point, times, Direction::Next),
991 SelectionGoal::None,
992 ),
993 PreviousComment => (
994 comment_motion(map, point, times, Direction::Prev),
995 SelectionGoal::None,
996 ),
997 };
998
999 (new_point != point || infallible).then_some((new_point, goal))
1000 }
1001
1002 // Get the range value after self is applied to the specified selection.
1003 pub fn range(
1004 &self,
1005 map: &DisplaySnapshot,
1006 selection: Selection<DisplayPoint>,
1007 times: Option<usize>,
1008 expand_to_surrounding_newline: bool,
1009 text_layout_details: &TextLayoutDetails,
1010 ) -> Option<Range<DisplayPoint>> {
1011 if let Motion::ZedSearchResult {
1012 prior_selections,
1013 new_selections,
1014 } = self
1015 {
1016 if let Some((prior_selection, new_selection)) =
1017 prior_selections.first().zip(new_selections.first())
1018 {
1019 let start = prior_selection
1020 .start
1021 .to_display_point(map)
1022 .min(new_selection.start.to_display_point(map));
1023 let end = new_selection
1024 .end
1025 .to_display_point(map)
1026 .max(prior_selection.end.to_display_point(map));
1027
1028 if start < end {
1029 return Some(start..end);
1030 } else {
1031 return Some(end..start);
1032 }
1033 } else {
1034 return None;
1035 }
1036 }
1037
1038 if let Some((new_head, goal)) = self.move_point(
1039 map,
1040 selection.head(),
1041 selection.goal,
1042 times,
1043 text_layout_details,
1044 ) {
1045 let mut selection = selection.clone();
1046 selection.set_head(new_head, goal);
1047
1048 if self.linewise() {
1049 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
1050
1051 if expand_to_surrounding_newline {
1052 if selection.end.row() < map.max_point().row() {
1053 *selection.end.row_mut() += 1;
1054 *selection.end.column_mut() = 0;
1055 selection.end = map.clip_point(selection.end, Bias::Right);
1056 // Don't reset the end here
1057 return Some(selection.start..selection.end);
1058 } else if selection.start.row().0 > 0 {
1059 *selection.start.row_mut() -= 1;
1060 *selection.start.column_mut() = map.line_len(selection.start.row());
1061 selection.start = map.clip_point(selection.start, Bias::Left);
1062 }
1063 }
1064
1065 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
1066 } else {
1067 // Another special case: When using the "w" motion in combination with an
1068 // operator and the last word moved over is at the end of a line, the end of
1069 // that word becomes the end of the operated text, not the first word in the
1070 // next line.
1071 if let Motion::NextWordStart {
1072 ignore_punctuation: _,
1073 } = self
1074 {
1075 let start_row = MultiBufferRow(selection.start.to_point(map).row);
1076 if selection.end.to_point(map).row > start_row.0 {
1077 selection.end =
1078 Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
1079 .to_display_point(map)
1080 }
1081 }
1082
1083 // If the motion is exclusive and the end of the motion is in column 1, the
1084 // end of the motion is moved to the end of the previous line and the motion
1085 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
1086 // but "d}" will not include that line.
1087 let mut inclusive = self.inclusive();
1088 let start_point = selection.start.to_point(map);
1089 let mut end_point = selection.end.to_point(map);
1090
1091 // DisplayPoint
1092
1093 if !inclusive
1094 && self != &Motion::Backspace
1095 && end_point.row > start_point.row
1096 && end_point.column == 0
1097 {
1098 inclusive = true;
1099 end_point.row -= 1;
1100 end_point.column = 0;
1101 selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
1102 }
1103
1104 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
1105 selection.end = movement::saturating_right(map, selection.end)
1106 }
1107 }
1108 Some(selection.start..selection.end)
1109 } else {
1110 None
1111 }
1112 }
1113
1114 // Expands a selection using self for an operator
1115 pub fn expand_selection(
1116 &self,
1117 map: &DisplaySnapshot,
1118 selection: &mut Selection<DisplayPoint>,
1119 times: Option<usize>,
1120 expand_to_surrounding_newline: bool,
1121 text_layout_details: &TextLayoutDetails,
1122 ) -> bool {
1123 if let Some(range) = self.range(
1124 map,
1125 selection.clone(),
1126 times,
1127 expand_to_surrounding_newline,
1128 text_layout_details,
1129 ) {
1130 selection.start = range.start;
1131 selection.end = range.end;
1132 true
1133 } else {
1134 false
1135 }
1136 }
1137}
1138
1139fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1140 for _ in 0..times {
1141 point = movement::saturating_left(map, point);
1142 if point.column() == 0 {
1143 break;
1144 }
1145 }
1146 point
1147}
1148
1149pub(crate) fn backspace(
1150 map: &DisplaySnapshot,
1151 mut point: DisplayPoint,
1152 times: usize,
1153) -> DisplayPoint {
1154 for _ in 0..times {
1155 point = movement::left(map, point);
1156 if point.is_zero() {
1157 break;
1158 }
1159 }
1160 point
1161}
1162
1163fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1164 for _ in 0..times {
1165 point = wrapping_right(map, point);
1166 if point == map.max_point() {
1167 break;
1168 }
1169 }
1170 point
1171}
1172
1173fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
1174 let max_column = map.line_len(point.row()).saturating_sub(1);
1175 if point.column() < max_column {
1176 *point.column_mut() += 1;
1177 } else if point.row() < map.max_point().row() {
1178 *point.row_mut() += 1;
1179 *point.column_mut() = 0;
1180 }
1181 point
1182}
1183
1184pub(crate) fn start_of_relative_buffer_row(
1185 map: &DisplaySnapshot,
1186 point: DisplayPoint,
1187 times: isize,
1188) -> DisplayPoint {
1189 let start = map.display_point_to_fold_point(point, Bias::Left);
1190 let target = start.row() as isize + times;
1191 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1192
1193 map.clip_point(
1194 map.fold_point_to_display_point(
1195 map.fold_snapshot
1196 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1197 ),
1198 Bias::Right,
1199 )
1200}
1201
1202fn up_down_buffer_rows(
1203 map: &DisplaySnapshot,
1204 point: DisplayPoint,
1205 mut goal: SelectionGoal,
1206 times: isize,
1207 text_layout_details: &TextLayoutDetails,
1208) -> (DisplayPoint, SelectionGoal) {
1209 let bias = if times < 0 { Bias::Left } else { Bias::Right };
1210 let start = map.display_point_to_fold_point(point, Bias::Left);
1211 let begin_folded_line = map.fold_point_to_display_point(
1212 map.fold_snapshot
1213 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1214 );
1215 let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1216
1217 let (goal_wrap, goal_x) = match goal {
1218 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1219 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1220 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1221 _ => {
1222 let x = map.x_for_display_point(point, text_layout_details);
1223 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1224 (select_nth_wrapped_row, x.0)
1225 }
1226 };
1227
1228 let target = start.row() as isize + times;
1229 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1230
1231 let mut begin_folded_line = map.fold_point_to_display_point(
1232 map.fold_snapshot
1233 .clip_point(FoldPoint::new(new_row, 0), bias),
1234 );
1235
1236 let mut i = 0;
1237 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1238 let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1239 if map
1240 .display_point_to_fold_point(next_folded_line, bias)
1241 .row()
1242 == new_row
1243 {
1244 i += 1;
1245 begin_folded_line = next_folded_line;
1246 } else {
1247 break;
1248 }
1249 }
1250
1251 let new_col = if i == goal_wrap {
1252 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1253 } else {
1254 map.line_len(begin_folded_line.row())
1255 };
1256
1257 (
1258 map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
1259 goal,
1260 )
1261}
1262
1263fn down_display(
1264 map: &DisplaySnapshot,
1265 mut point: DisplayPoint,
1266 mut goal: SelectionGoal,
1267 times: usize,
1268 text_layout_details: &TextLayoutDetails,
1269) -> (DisplayPoint, SelectionGoal) {
1270 for _ in 0..times {
1271 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1272 }
1273
1274 (point, goal)
1275}
1276
1277fn up_display(
1278 map: &DisplaySnapshot,
1279 mut point: DisplayPoint,
1280 mut goal: SelectionGoal,
1281 times: usize,
1282 text_layout_details: &TextLayoutDetails,
1283) -> (DisplayPoint, SelectionGoal) {
1284 for _ in 0..times {
1285 (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1286 }
1287
1288 (point, goal)
1289}
1290
1291pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1292 for _ in 0..times {
1293 let new_point = movement::saturating_right(map, point);
1294 if point == new_point {
1295 break;
1296 }
1297 point = new_point;
1298 }
1299 point
1300}
1301
1302pub(crate) fn next_char(
1303 map: &DisplaySnapshot,
1304 point: DisplayPoint,
1305 allow_cross_newline: bool,
1306) -> DisplayPoint {
1307 let mut new_point = point;
1308 let mut max_column = map.line_len(new_point.row());
1309 if !allow_cross_newline {
1310 max_column -= 1;
1311 }
1312 if new_point.column() < max_column {
1313 *new_point.column_mut() += 1;
1314 } else if new_point < map.max_point() && allow_cross_newline {
1315 *new_point.row_mut() += 1;
1316 *new_point.column_mut() = 0;
1317 }
1318 map.clip_ignoring_line_ends(new_point, Bias::Right)
1319}
1320
1321pub(crate) fn next_word_start(
1322 map: &DisplaySnapshot,
1323 mut point: DisplayPoint,
1324 ignore_punctuation: bool,
1325 times: usize,
1326) -> DisplayPoint {
1327 let classifier = map
1328 .buffer_snapshot
1329 .char_classifier_at(point.to_point(map))
1330 .ignore_punctuation(ignore_punctuation);
1331 for _ in 0..times {
1332 let mut crossed_newline = false;
1333 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1334 let left_kind = classifier.kind(left);
1335 let right_kind = classifier.kind(right);
1336 let at_newline = right == '\n';
1337
1338 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1339 || at_newline && crossed_newline
1340 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1341
1342 crossed_newline |= at_newline;
1343 found
1344 });
1345 if point == new_point {
1346 break;
1347 }
1348 point = new_point;
1349 }
1350 point
1351}
1352
1353pub(crate) fn next_word_end(
1354 map: &DisplaySnapshot,
1355 mut point: DisplayPoint,
1356 ignore_punctuation: bool,
1357 times: usize,
1358 allow_cross_newline: bool,
1359) -> DisplayPoint {
1360 let classifier = map
1361 .buffer_snapshot
1362 .char_classifier_at(point.to_point(map))
1363 .ignore_punctuation(ignore_punctuation);
1364 for _ in 0..times {
1365 let new_point = next_char(map, point, allow_cross_newline);
1366 let mut need_next_char = false;
1367 let new_point = movement::find_boundary_exclusive(
1368 map,
1369 new_point,
1370 FindRange::MultiLine,
1371 |left, right| {
1372 let left_kind = classifier.kind(left);
1373 let right_kind = classifier.kind(right);
1374 let at_newline = right == '\n';
1375
1376 if !allow_cross_newline && at_newline {
1377 need_next_char = true;
1378 return true;
1379 }
1380
1381 left_kind != right_kind && left_kind != CharKind::Whitespace
1382 },
1383 );
1384 let new_point = if need_next_char {
1385 next_char(map, new_point, true)
1386 } else {
1387 new_point
1388 };
1389 let new_point = map.clip_point(new_point, Bias::Left);
1390 if point == new_point {
1391 break;
1392 }
1393 point = new_point;
1394 }
1395 point
1396}
1397
1398fn previous_word_start(
1399 map: &DisplaySnapshot,
1400 mut point: DisplayPoint,
1401 ignore_punctuation: bool,
1402 times: usize,
1403) -> DisplayPoint {
1404 let classifier = map
1405 .buffer_snapshot
1406 .char_classifier_at(point.to_point(map))
1407 .ignore_punctuation(ignore_punctuation);
1408 for _ in 0..times {
1409 // This works even though find_preceding_boundary is called for every character in the line containing
1410 // cursor because the newline is checked only once.
1411 let new_point = movement::find_preceding_boundary_display_point(
1412 map,
1413 point,
1414 FindRange::MultiLine,
1415 |left, right| {
1416 let left_kind = classifier.kind(left);
1417 let right_kind = classifier.kind(right);
1418
1419 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1420 },
1421 );
1422 if point == new_point {
1423 break;
1424 }
1425 point = new_point;
1426 }
1427 point
1428}
1429
1430fn previous_word_end(
1431 map: &DisplaySnapshot,
1432 point: DisplayPoint,
1433 ignore_punctuation: bool,
1434 times: usize,
1435) -> DisplayPoint {
1436 let classifier = map
1437 .buffer_snapshot
1438 .char_classifier_at(point.to_point(map))
1439 .ignore_punctuation(ignore_punctuation);
1440 let mut point = point.to_point(map);
1441
1442 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1443 point.column += 1;
1444 }
1445 for _ in 0..times {
1446 let new_point = movement::find_preceding_boundary_point(
1447 &map.buffer_snapshot,
1448 point,
1449 FindRange::MultiLine,
1450 |left, right| {
1451 let left_kind = classifier.kind(left);
1452 let right_kind = classifier.kind(right);
1453 match (left_kind, right_kind) {
1454 (CharKind::Punctuation, CharKind::Whitespace)
1455 | (CharKind::Punctuation, CharKind::Word)
1456 | (CharKind::Word, CharKind::Whitespace)
1457 | (CharKind::Word, CharKind::Punctuation) => true,
1458 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1459 _ => false,
1460 }
1461 },
1462 );
1463 if new_point == point {
1464 break;
1465 }
1466 point = new_point;
1467 }
1468 movement::saturating_left(map, point.to_display_point(map))
1469}
1470
1471fn next_subword_start(
1472 map: &DisplaySnapshot,
1473 mut point: DisplayPoint,
1474 ignore_punctuation: bool,
1475 times: usize,
1476) -> DisplayPoint {
1477 let classifier = map
1478 .buffer_snapshot
1479 .char_classifier_at(point.to_point(map))
1480 .ignore_punctuation(ignore_punctuation);
1481 for _ in 0..times {
1482 let mut crossed_newline = false;
1483 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1484 let left_kind = classifier.kind(left);
1485 let right_kind = classifier.kind(right);
1486 let at_newline = right == '\n';
1487
1488 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1489 let is_subword_start =
1490 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1491
1492 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1493 || at_newline && crossed_newline
1494 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1495
1496 crossed_newline |= at_newline;
1497 found
1498 });
1499 if point == new_point {
1500 break;
1501 }
1502 point = new_point;
1503 }
1504 point
1505}
1506
1507pub(crate) fn next_subword_end(
1508 map: &DisplaySnapshot,
1509 mut point: DisplayPoint,
1510 ignore_punctuation: bool,
1511 times: usize,
1512 allow_cross_newline: bool,
1513) -> DisplayPoint {
1514 let classifier = map
1515 .buffer_snapshot
1516 .char_classifier_at(point.to_point(map))
1517 .ignore_punctuation(ignore_punctuation);
1518 for _ in 0..times {
1519 let new_point = next_char(map, point, allow_cross_newline);
1520
1521 let mut crossed_newline = false;
1522 let mut need_backtrack = false;
1523 let new_point =
1524 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1525 let left_kind = classifier.kind(left);
1526 let right_kind = classifier.kind(right);
1527 let at_newline = right == '\n';
1528
1529 if !allow_cross_newline && at_newline {
1530 return true;
1531 }
1532
1533 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1534 let is_subword_end =
1535 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1536
1537 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1538
1539 if found && (is_word_end || is_subword_end) {
1540 need_backtrack = true;
1541 }
1542
1543 crossed_newline |= at_newline;
1544 found
1545 });
1546 let mut new_point = map.clip_point(new_point, Bias::Left);
1547 if need_backtrack {
1548 *new_point.column_mut() -= 1;
1549 }
1550 if point == new_point {
1551 break;
1552 }
1553 point = new_point;
1554 }
1555 point
1556}
1557
1558fn previous_subword_start(
1559 map: &DisplaySnapshot,
1560 mut point: DisplayPoint,
1561 ignore_punctuation: bool,
1562 times: usize,
1563) -> DisplayPoint {
1564 let classifier = map
1565 .buffer_snapshot
1566 .char_classifier_at(point.to_point(map))
1567 .ignore_punctuation(ignore_punctuation);
1568 for _ in 0..times {
1569 let mut crossed_newline = false;
1570 // This works even though find_preceding_boundary is called for every character in the line containing
1571 // cursor because the newline is checked only once.
1572 let new_point = movement::find_preceding_boundary_display_point(
1573 map,
1574 point,
1575 FindRange::MultiLine,
1576 |left, right| {
1577 let left_kind = classifier.kind(left);
1578 let right_kind = classifier.kind(right);
1579 let at_newline = right == '\n';
1580
1581 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1582 let is_subword_start =
1583 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1584
1585 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1586 || at_newline && crossed_newline
1587 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1588
1589 crossed_newline |= at_newline;
1590
1591 found
1592 },
1593 );
1594 if point == new_point {
1595 break;
1596 }
1597 point = new_point;
1598 }
1599 point
1600}
1601
1602fn previous_subword_end(
1603 map: &DisplaySnapshot,
1604 point: DisplayPoint,
1605 ignore_punctuation: bool,
1606 times: usize,
1607) -> DisplayPoint {
1608 let classifier = map
1609 .buffer_snapshot
1610 .char_classifier_at(point.to_point(map))
1611 .ignore_punctuation(ignore_punctuation);
1612 let mut point = point.to_point(map);
1613
1614 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1615 point.column += 1;
1616 }
1617 for _ in 0..times {
1618 let new_point = movement::find_preceding_boundary_point(
1619 &map.buffer_snapshot,
1620 point,
1621 FindRange::MultiLine,
1622 |left, right| {
1623 let left_kind = classifier.kind(left);
1624 let right_kind = classifier.kind(right);
1625
1626 let is_subword_end =
1627 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1628
1629 if is_subword_end {
1630 return true;
1631 }
1632
1633 match (left_kind, right_kind) {
1634 (CharKind::Word, CharKind::Whitespace)
1635 | (CharKind::Word, CharKind::Punctuation) => true,
1636 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1637 _ => false,
1638 }
1639 },
1640 );
1641 if new_point == point {
1642 break;
1643 }
1644 point = new_point;
1645 }
1646 movement::saturating_left(map, point.to_display_point(map))
1647}
1648
1649pub(crate) fn first_non_whitespace(
1650 map: &DisplaySnapshot,
1651 display_lines: bool,
1652 from: DisplayPoint,
1653) -> DisplayPoint {
1654 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1655 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1656 for (ch, offset) in map.buffer_chars_at(start_offset) {
1657 if ch == '\n' {
1658 return from;
1659 }
1660
1661 start_offset = offset;
1662
1663 if classifier.kind(ch) != CharKind::Whitespace {
1664 break;
1665 }
1666 }
1667
1668 start_offset.to_display_point(map)
1669}
1670
1671pub(crate) fn last_non_whitespace(
1672 map: &DisplaySnapshot,
1673 from: DisplayPoint,
1674 count: usize,
1675) -> DisplayPoint {
1676 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1677 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1678
1679 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1680 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1681 if classifier.kind(ch) != CharKind::Whitespace {
1682 return end_of_line.to_display_point(map);
1683 }
1684 }
1685
1686 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1687 if ch == '\n' {
1688 break;
1689 }
1690 end_of_line = offset;
1691 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1692 break;
1693 }
1694 }
1695
1696 end_of_line.to_display_point(map)
1697}
1698
1699pub(crate) fn start_of_line(
1700 map: &DisplaySnapshot,
1701 display_lines: bool,
1702 point: DisplayPoint,
1703) -> DisplayPoint {
1704 if display_lines {
1705 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1706 } else {
1707 map.prev_line_boundary(point.to_point(map)).1
1708 }
1709}
1710
1711pub(crate) fn end_of_line(
1712 map: &DisplaySnapshot,
1713 display_lines: bool,
1714 mut point: DisplayPoint,
1715 times: usize,
1716) -> DisplayPoint {
1717 if times > 1 {
1718 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1719 }
1720 if display_lines {
1721 map.clip_point(
1722 DisplayPoint::new(point.row(), map.line_len(point.row())),
1723 Bias::Left,
1724 )
1725 } else {
1726 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1727 }
1728}
1729
1730fn sentence_backwards(
1731 map: &DisplaySnapshot,
1732 point: DisplayPoint,
1733 mut times: usize,
1734) -> DisplayPoint {
1735 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1736 let mut chars = map.reverse_buffer_chars_at(start).peekable();
1737
1738 let mut was_newline = map
1739 .buffer_chars_at(start)
1740 .next()
1741 .is_some_and(|(c, _)| c == '\n');
1742
1743 while let Some((ch, offset)) = chars.next() {
1744 let start_of_next_sentence = if was_newline && ch == '\n' {
1745 Some(offset + ch.len_utf8())
1746 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1747 Some(next_non_blank(map, offset + ch.len_utf8()))
1748 } else if ch == '.' || ch == '?' || ch == '!' {
1749 start_of_next_sentence(map, offset + ch.len_utf8())
1750 } else {
1751 None
1752 };
1753
1754 if let Some(start_of_next_sentence) = start_of_next_sentence {
1755 if start_of_next_sentence < start {
1756 times = times.saturating_sub(1);
1757 }
1758 if times == 0 || offset == 0 {
1759 return map.clip_point(
1760 start_of_next_sentence
1761 .to_offset(&map.buffer_snapshot)
1762 .to_display_point(map),
1763 Bias::Left,
1764 );
1765 }
1766 }
1767 if was_newline {
1768 start = offset;
1769 }
1770 was_newline = ch == '\n';
1771 }
1772
1773 DisplayPoint::zero()
1774}
1775
1776fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1777 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1778 let mut chars = map.buffer_chars_at(start).peekable();
1779
1780 let mut was_newline = map
1781 .reverse_buffer_chars_at(start)
1782 .next()
1783 .is_some_and(|(c, _)| c == '\n')
1784 && chars.peek().is_some_and(|(c, _)| *c == '\n');
1785
1786 while let Some((ch, offset)) = chars.next() {
1787 if was_newline && ch == '\n' {
1788 continue;
1789 }
1790 let start_of_next_sentence = if was_newline {
1791 Some(next_non_blank(map, offset))
1792 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1793 Some(next_non_blank(map, offset + ch.len_utf8()))
1794 } else if ch == '.' || ch == '?' || ch == '!' {
1795 start_of_next_sentence(map, offset + ch.len_utf8())
1796 } else {
1797 None
1798 };
1799
1800 if let Some(start_of_next_sentence) = start_of_next_sentence {
1801 times = times.saturating_sub(1);
1802 if times == 0 {
1803 return map.clip_point(
1804 start_of_next_sentence
1805 .to_offset(&map.buffer_snapshot)
1806 .to_display_point(map),
1807 Bias::Right,
1808 );
1809 }
1810 }
1811
1812 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1813 }
1814
1815 map.max_point()
1816}
1817
1818fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1819 for (c, o) in map.buffer_chars_at(start) {
1820 if c == '\n' || !c.is_whitespace() {
1821 return o;
1822 }
1823 }
1824
1825 map.buffer_snapshot.len()
1826}
1827
1828// given the offset after a ., !, or ? find the start of the next sentence.
1829// if this is not a sentence boundary, returns None.
1830fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1831 let chars = map.buffer_chars_at(end_of_sentence);
1832 let mut seen_space = false;
1833
1834 for (char, offset) in chars {
1835 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1836 continue;
1837 }
1838
1839 if char == '\n' && seen_space {
1840 return Some(offset);
1841 } else if char.is_whitespace() {
1842 seen_space = true;
1843 } else if seen_space {
1844 return Some(offset);
1845 } else {
1846 return None;
1847 }
1848 }
1849
1850 Some(map.buffer_snapshot.len())
1851}
1852
1853fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1854 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1855 *new_point.column_mut() = point.column();
1856 map.clip_point(new_point, Bias::Left)
1857}
1858
1859fn end_of_document(
1860 map: &DisplaySnapshot,
1861 point: DisplayPoint,
1862 line: Option<usize>,
1863) -> DisplayPoint {
1864 let new_row = if let Some(line) = line {
1865 (line - 1) as u32
1866 } else {
1867 map.buffer_snapshot.max_row().0
1868 };
1869
1870 let new_point = Point::new(new_row, point.column());
1871 map.clip_point(new_point.to_display_point(map), Bias::Left)
1872}
1873
1874fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
1875 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
1876 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
1877
1878 if head > outer.start && head < inner.start {
1879 let mut offset = inner.end.to_offset(map, Bias::Left);
1880 for c in map.buffer_snapshot.chars_at(offset) {
1881 if c == '/' || c == '\n' || c == '>' {
1882 return Some(offset.to_display_point(map));
1883 }
1884 offset += c.len_utf8();
1885 }
1886 } else {
1887 let mut offset = outer.start.to_offset(map, Bias::Left);
1888 for c in map.buffer_snapshot.chars_at(offset) {
1889 offset += c.len_utf8();
1890 if c == '<' || c == '\n' {
1891 return Some(offset.to_display_point(map));
1892 }
1893 }
1894 }
1895
1896 return None;
1897}
1898
1899fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1900 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1901 let display_point = map.clip_at_line_end(display_point);
1902 let point = display_point.to_point(map);
1903 let offset = point.to_offset(&map.buffer_snapshot);
1904
1905 // Ensure the range is contained by the current line.
1906 let mut line_end = map.next_line_boundary(point).0;
1907 if line_end == point {
1908 line_end = map.max_point().to_point(map);
1909 }
1910
1911 let line_range = map.prev_line_boundary(point).0..line_end;
1912 let visible_line_range =
1913 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1914 let ranges = map
1915 .buffer_snapshot
1916 .bracket_ranges(visible_line_range.clone());
1917 if let Some(ranges) = ranges {
1918 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1919 ..line_range.end.to_offset(&map.buffer_snapshot);
1920 let mut closest_pair_destination = None;
1921 let mut closest_distance = usize::MAX;
1922
1923 for (open_range, close_range) in ranges {
1924 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
1925 if offset > open_range.start && offset < close_range.start {
1926 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1927 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
1928 return display_point;
1929 }
1930 if let Some(tag) = matching_tag(map, display_point) {
1931 return tag;
1932 }
1933 } else if close_range.contains(&offset) {
1934 return open_range.start.to_display_point(map);
1935 } else if open_range.contains(&offset) {
1936 return (close_range.end - 1).to_display_point(map);
1937 }
1938 }
1939
1940 if open_range.start >= offset && line_range.contains(&open_range.start) {
1941 let distance = open_range.start - offset;
1942 if distance < closest_distance {
1943 closest_pair_destination = Some(close_range.end - 1);
1944 closest_distance = distance;
1945 continue;
1946 }
1947 }
1948
1949 if close_range.start >= offset && line_range.contains(&close_range.start) {
1950 let distance = close_range.start - offset;
1951 if distance < closest_distance {
1952 closest_pair_destination = Some(open_range.start);
1953 closest_distance = distance;
1954 continue;
1955 }
1956 }
1957
1958 continue;
1959 }
1960
1961 closest_pair_destination
1962 .map(|destination| destination.to_display_point(map))
1963 .unwrap_or(display_point)
1964 } else {
1965 display_point
1966 }
1967}
1968
1969fn unmatched_forward(
1970 map: &DisplaySnapshot,
1971 mut display_point: DisplayPoint,
1972 char: char,
1973 times: usize,
1974) -> DisplayPoint {
1975 for _ in 0..times {
1976 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
1977 let point = display_point.to_point(map);
1978 let offset = point.to_offset(&map.buffer_snapshot);
1979
1980 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
1981 let Some(ranges) = ranges else { break };
1982 let mut closest_closing_destination = None;
1983 let mut closest_distance = usize::MAX;
1984
1985 for (_, close_range) in ranges {
1986 if close_range.start > offset {
1987 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1988 if Some(char) == chars.next() {
1989 let distance = close_range.start - offset;
1990 if distance < closest_distance {
1991 closest_closing_destination = Some(close_range.start);
1992 closest_distance = distance;
1993 continue;
1994 }
1995 }
1996 }
1997 }
1998
1999 let new_point = closest_closing_destination
2000 .map(|destination| destination.to_display_point(map))
2001 .unwrap_or(display_point);
2002 if new_point == display_point {
2003 break;
2004 }
2005 display_point = new_point;
2006 }
2007 return display_point;
2008}
2009
2010fn unmatched_backward(
2011 map: &DisplaySnapshot,
2012 mut display_point: DisplayPoint,
2013 char: char,
2014 times: usize,
2015) -> DisplayPoint {
2016 for _ in 0..times {
2017 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2018 let point = display_point.to_point(map);
2019 let offset = point.to_offset(&map.buffer_snapshot);
2020
2021 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2022 let Some(ranges) = ranges else {
2023 break;
2024 };
2025
2026 let mut closest_starting_destination = None;
2027 let mut closest_distance = usize::MAX;
2028
2029 for (start_range, _) in ranges {
2030 if start_range.start < offset {
2031 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2032 if Some(char) == chars.next() {
2033 let distance = offset - start_range.start;
2034 if distance < closest_distance {
2035 closest_starting_destination = Some(start_range.start);
2036 closest_distance = distance;
2037 continue;
2038 }
2039 }
2040 }
2041 }
2042
2043 let new_point = closest_starting_destination
2044 .map(|destination| destination.to_display_point(map))
2045 .unwrap_or(display_point);
2046 if new_point == display_point {
2047 break;
2048 } else {
2049 display_point = new_point;
2050 }
2051 }
2052 display_point
2053}
2054
2055fn find_forward(
2056 map: &DisplaySnapshot,
2057 from: DisplayPoint,
2058 before: bool,
2059 target: char,
2060 times: usize,
2061 mode: FindRange,
2062 smartcase: bool,
2063) -> Option<DisplayPoint> {
2064 let mut to = from;
2065 let mut found = false;
2066
2067 for _ in 0..times {
2068 found = false;
2069 let new_to = find_boundary(map, to, mode, |_, right| {
2070 found = is_character_match(target, right, smartcase);
2071 found
2072 });
2073 if to == new_to {
2074 break;
2075 }
2076 to = new_to;
2077 }
2078
2079 if found {
2080 if before && to.column() > 0 {
2081 *to.column_mut() -= 1;
2082 Some(map.clip_point(to, Bias::Left))
2083 } else {
2084 Some(to)
2085 }
2086 } else {
2087 None
2088 }
2089}
2090
2091fn find_backward(
2092 map: &DisplaySnapshot,
2093 from: DisplayPoint,
2094 after: bool,
2095 target: char,
2096 times: usize,
2097 mode: FindRange,
2098 smartcase: bool,
2099) -> DisplayPoint {
2100 let mut to = from;
2101
2102 for _ in 0..times {
2103 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2104 is_character_match(target, right, smartcase)
2105 });
2106 if to == new_to {
2107 break;
2108 }
2109 to = new_to;
2110 }
2111
2112 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2113 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2114 if after {
2115 *to.column_mut() += 1;
2116 map.clip_point(to, Bias::Right)
2117 } else {
2118 to
2119 }
2120 } else {
2121 from
2122 }
2123}
2124
2125fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2126 if smartcase {
2127 if target.is_uppercase() {
2128 target == other
2129 } else {
2130 target == other.to_ascii_lowercase()
2131 }
2132 } else {
2133 target == other
2134 }
2135}
2136
2137fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2138 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2139 first_non_whitespace(map, false, correct_line)
2140}
2141
2142fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2143 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2144 first_non_whitespace(map, false, correct_line)
2145}
2146
2147fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2148 let correct_line = start_of_relative_buffer_row(map, point, 0);
2149 right(map, correct_line, times.saturating_sub(1))
2150}
2151
2152pub(crate) fn next_line_end(
2153 map: &DisplaySnapshot,
2154 mut point: DisplayPoint,
2155 times: usize,
2156) -> DisplayPoint {
2157 if times > 1 {
2158 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2159 }
2160 end_of_line(map, false, point, 1)
2161}
2162
2163fn window_top(
2164 map: &DisplaySnapshot,
2165 point: DisplayPoint,
2166 text_layout_details: &TextLayoutDetails,
2167 mut times: usize,
2168) -> (DisplayPoint, SelectionGoal) {
2169 let first_visible_line = text_layout_details
2170 .scroll_anchor
2171 .anchor
2172 .to_display_point(map);
2173
2174 if first_visible_line.row() != DisplayRow(0)
2175 && text_layout_details.vertical_scroll_margin as usize > times
2176 {
2177 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2178 }
2179
2180 if let Some(visible_rows) = text_layout_details.visible_rows {
2181 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2182 let new_row = (first_visible_line.row().0 + (times as u32))
2183 .min(bottom_row)
2184 .min(map.max_point().row().0);
2185 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2186
2187 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2188 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2189 } else {
2190 let new_row =
2191 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2192 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2193
2194 let new_point = DisplayPoint::new(new_row, new_col);
2195 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2196 }
2197}
2198
2199fn window_middle(
2200 map: &DisplaySnapshot,
2201 point: DisplayPoint,
2202 text_layout_details: &TextLayoutDetails,
2203) -> (DisplayPoint, SelectionGoal) {
2204 if let Some(visible_rows) = text_layout_details.visible_rows {
2205 let first_visible_line = text_layout_details
2206 .scroll_anchor
2207 .anchor
2208 .to_display_point(map);
2209
2210 let max_visible_rows =
2211 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2212
2213 let new_row =
2214 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2215 let new_row = DisplayRow(new_row);
2216 let new_col = point.column().min(map.line_len(new_row));
2217 let new_point = DisplayPoint::new(new_row, new_col);
2218 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2219 } else {
2220 (point, SelectionGoal::None)
2221 }
2222}
2223
2224fn window_bottom(
2225 map: &DisplaySnapshot,
2226 point: DisplayPoint,
2227 text_layout_details: &TextLayoutDetails,
2228 mut times: usize,
2229) -> (DisplayPoint, SelectionGoal) {
2230 if let Some(visible_rows) = text_layout_details.visible_rows {
2231 let first_visible_line = text_layout_details
2232 .scroll_anchor
2233 .anchor
2234 .to_display_point(map);
2235 let bottom_row = first_visible_line.row().0
2236 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2237 if bottom_row < map.max_point().row().0
2238 && text_layout_details.vertical_scroll_margin as usize > times
2239 {
2240 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2241 }
2242 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2243 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2244 {
2245 first_visible_line.row()
2246 } else {
2247 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2248 };
2249 let new_col = point.column().min(map.line_len(new_row));
2250 let new_point = DisplayPoint::new(new_row, new_col);
2251 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2252 } else {
2253 (point, SelectionGoal::None)
2254 }
2255}
2256
2257fn method_motion(
2258 map: &DisplaySnapshot,
2259 mut display_point: DisplayPoint,
2260 times: usize,
2261 direction: Direction,
2262 is_start: bool,
2263) -> DisplayPoint {
2264 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2265 return display_point;
2266 };
2267
2268 for _ in 0..times {
2269 let point = map.display_point_to_point(display_point, Bias::Left);
2270 let offset = point.to_offset(&map.buffer_snapshot);
2271 let range = if direction == Direction::Prev {
2272 0..offset
2273 } else {
2274 offset..buffer.len()
2275 };
2276
2277 let possibilities = buffer
2278 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2279 .filter_map(|(range, object)| {
2280 if !matches!(object, language::TextObject::AroundFunction) {
2281 return None;
2282 }
2283
2284 let relevant = if is_start { range.start } else { range.end };
2285 if direction == Direction::Prev && relevant < offset {
2286 Some(relevant)
2287 } else if direction == Direction::Next && relevant > offset + 1 {
2288 Some(relevant)
2289 } else {
2290 None
2291 }
2292 });
2293
2294 let dest = if direction == Direction::Prev {
2295 possibilities.max().unwrap_or(offset)
2296 } else {
2297 possibilities.min().unwrap_or(offset)
2298 };
2299 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2300 if new_point == display_point {
2301 break;
2302 }
2303 display_point = new_point;
2304 }
2305 display_point
2306}
2307
2308fn comment_motion(
2309 map: &DisplaySnapshot,
2310 mut display_point: DisplayPoint,
2311 times: usize,
2312 direction: Direction,
2313) -> DisplayPoint {
2314 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2315 return display_point;
2316 };
2317
2318 for _ in 0..times {
2319 let point = map.display_point_to_point(display_point, Bias::Left);
2320 let offset = point.to_offset(&map.buffer_snapshot);
2321 let range = if direction == Direction::Prev {
2322 0..offset
2323 } else {
2324 offset..buffer.len()
2325 };
2326
2327 let possibilities = buffer
2328 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2329 .filter_map(|(range, object)| {
2330 if !matches!(object, language::TextObject::AroundComment) {
2331 return None;
2332 }
2333
2334 let relevant = if direction == Direction::Prev {
2335 range.start
2336 } else {
2337 range.end
2338 };
2339 if direction == Direction::Prev && relevant < offset {
2340 Some(relevant)
2341 } else if direction == Direction::Next && relevant > offset + 1 {
2342 Some(relevant)
2343 } else {
2344 None
2345 }
2346 });
2347
2348 let dest = if direction == Direction::Prev {
2349 possibilities.max().unwrap_or(offset)
2350 } else {
2351 possibilities.min().unwrap_or(offset)
2352 };
2353 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2354 if new_point == display_point {
2355 break;
2356 }
2357 display_point = new_point;
2358 }
2359
2360 display_point
2361}
2362
2363fn section_motion(
2364 map: &DisplaySnapshot,
2365 mut display_point: DisplayPoint,
2366 times: usize,
2367 direction: Direction,
2368 is_start: bool,
2369) -> DisplayPoint {
2370 if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
2371 for _ in 0..times {
2372 let offset = map
2373 .display_point_to_point(display_point, Bias::Left)
2374 .to_offset(&map.buffer_snapshot);
2375 let range = if direction == Direction::Prev {
2376 0..offset
2377 } else {
2378 offset..buffer.len()
2379 };
2380
2381 // we set a max start depth here because we want a section to only be "top level"
2382 // similar to vim's default of '{' in the first column.
2383 // (and without it, ]] at the start of editor.rs is -very- slow)
2384 let mut possibilities = buffer
2385 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2386 .filter(|(_, object)| {
2387 matches!(
2388 object,
2389 language::TextObject::AroundClass | language::TextObject::AroundFunction
2390 )
2391 })
2392 .collect::<Vec<_>>();
2393 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2394 let mut prev_end = None;
2395 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2396 if t == language::TextObject::AroundFunction
2397 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2398 {
2399 return None;
2400 }
2401 prev_end = Some(range.end);
2402
2403 let relevant = if is_start { range.start } else { range.end };
2404 if direction == Direction::Prev && relevant < offset {
2405 Some(relevant)
2406 } else if direction == Direction::Next && relevant > offset + 1 {
2407 Some(relevant)
2408 } else {
2409 None
2410 }
2411 });
2412
2413 let offset = if direction == Direction::Prev {
2414 possibilities.max().unwrap_or(0)
2415 } else {
2416 possibilities.min().unwrap_or(buffer.len())
2417 };
2418
2419 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2420 if new_point == display_point {
2421 break;
2422 }
2423 display_point = new_point;
2424 }
2425 return display_point;
2426 };
2427
2428 for _ in 0..times {
2429 let point = map.display_point_to_point(display_point, Bias::Left);
2430 let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2431 return display_point;
2432 };
2433 let next_point = match (direction, is_start) {
2434 (Direction::Prev, true) => {
2435 let mut start = excerpt.start_anchor().to_display_point(&map);
2436 if start >= display_point && start.row() > DisplayRow(0) {
2437 let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2438 return display_point;
2439 };
2440 start = excerpt.start_anchor().to_display_point(&map);
2441 }
2442 start
2443 }
2444 (Direction::Prev, false) => {
2445 let mut start = excerpt.start_anchor().to_display_point(&map);
2446 if start.row() > DisplayRow(0) {
2447 *start.row_mut() -= 1;
2448 }
2449 map.clip_point(start, Bias::Left)
2450 }
2451 (Direction::Next, true) => {
2452 let mut end = excerpt.end_anchor().to_display_point(&map);
2453 *end.row_mut() += 1;
2454 map.clip_point(end, Bias::Right)
2455 }
2456 (Direction::Next, false) => {
2457 let mut end = excerpt.end_anchor().to_display_point(&map);
2458 *end.column_mut() = 0;
2459 if end <= display_point {
2460 *end.row_mut() += 1;
2461 let point_end = map.display_point_to_point(end, Bias::Right);
2462 let Some(excerpt) =
2463 map.buffer_snapshot.excerpt_containing(point_end..point_end)
2464 else {
2465 return display_point;
2466 };
2467 end = excerpt.end_anchor().to_display_point(&map);
2468 *end.column_mut() = 0;
2469 }
2470 end
2471 }
2472 };
2473 if next_point == display_point {
2474 break;
2475 }
2476 display_point = next_point;
2477 }
2478
2479 display_point
2480}
2481
2482#[cfg(test)]
2483mod test {
2484
2485 use crate::{
2486 state::Mode,
2487 test::{NeovimBackedTestContext, VimTestContext},
2488 };
2489 use editor::display_map::Inlay;
2490 use indoc::indoc;
2491
2492 #[gpui::test]
2493 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2494 let mut cx = NeovimBackedTestContext::new(cx).await;
2495
2496 let initial_state = indoc! {r"ˇabc
2497 def
2498
2499 paragraph
2500 the second
2501
2502
2503
2504 third and
2505 final"};
2506
2507 // goes down once
2508 cx.set_shared_state(initial_state).await;
2509 cx.simulate_shared_keystrokes("}").await;
2510 cx.shared_state().await.assert_eq(indoc! {r"abc
2511 def
2512 ˇ
2513 paragraph
2514 the second
2515
2516
2517
2518 third and
2519 final"});
2520
2521 // goes up once
2522 cx.simulate_shared_keystrokes("{").await;
2523 cx.shared_state().await.assert_eq(initial_state);
2524
2525 // goes down twice
2526 cx.simulate_shared_keystrokes("2 }").await;
2527 cx.shared_state().await.assert_eq(indoc! {r"abc
2528 def
2529
2530 paragraph
2531 the second
2532 ˇ
2533
2534
2535 third and
2536 final"});
2537
2538 // goes down over multiple blanks
2539 cx.simulate_shared_keystrokes("}").await;
2540 cx.shared_state().await.assert_eq(indoc! {r"abc
2541 def
2542
2543 paragraph
2544 the second
2545
2546
2547
2548 third and
2549 finaˇl"});
2550
2551 // goes up twice
2552 cx.simulate_shared_keystrokes("2 {").await;
2553 cx.shared_state().await.assert_eq(indoc! {r"abc
2554 def
2555 ˇ
2556 paragraph
2557 the second
2558
2559
2560
2561 third and
2562 final"});
2563 }
2564
2565 #[gpui::test]
2566 async fn test_matching(cx: &mut gpui::TestAppContext) {
2567 let mut cx = NeovimBackedTestContext::new(cx).await;
2568
2569 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2570 do(something(with<Types>.and_arrays[0, 2]))
2571 }"})
2572 .await;
2573 cx.simulate_shared_keystrokes("%").await;
2574 cx.shared_state()
2575 .await
2576 .assert_eq(indoc! {r"func (a stringˇ) {
2577 do(something(with<Types>.and_arrays[0, 2]))
2578 }"});
2579
2580 // test it works on the last character of the line
2581 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2582 do(something(with<Types>.and_arrays[0, 2]))
2583 }"})
2584 .await;
2585 cx.simulate_shared_keystrokes("%").await;
2586 cx.shared_state()
2587 .await
2588 .assert_eq(indoc! {r"func (a string) {
2589 do(something(with<Types>.and_arrays[0, 2]))
2590 ˇ}"});
2591
2592 // test it works on immediate nesting
2593 cx.set_shared_state("ˇ{()}").await;
2594 cx.simulate_shared_keystrokes("%").await;
2595 cx.shared_state().await.assert_eq("{()ˇ}");
2596 cx.simulate_shared_keystrokes("%").await;
2597 cx.shared_state().await.assert_eq("ˇ{()}");
2598
2599 // test it works on immediate nesting inside braces
2600 cx.set_shared_state("{\n ˇ{()}\n}").await;
2601 cx.simulate_shared_keystrokes("%").await;
2602 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
2603
2604 // test it jumps to the next paren on a line
2605 cx.set_shared_state("func ˇboop() {\n}").await;
2606 cx.simulate_shared_keystrokes("%").await;
2607 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2608 }
2609
2610 #[gpui::test]
2611 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2612 let mut cx = NeovimBackedTestContext::new(cx).await;
2613
2614 // test it works with curly braces
2615 cx.set_shared_state(indoc! {r"func (a string) {
2616 do(something(with<Types>.anˇd_arrays[0, 2]))
2617 }"})
2618 .await;
2619 cx.simulate_shared_keystrokes("] }").await;
2620 cx.shared_state()
2621 .await
2622 .assert_eq(indoc! {r"func (a string) {
2623 do(something(with<Types>.and_arrays[0, 2]))
2624 ˇ}"});
2625
2626 // test it works with brackets
2627 cx.set_shared_state(indoc! {r"func (a string) {
2628 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2629 }"})
2630 .await;
2631 cx.simulate_shared_keystrokes("] )").await;
2632 cx.shared_state()
2633 .await
2634 .assert_eq(indoc! {r"func (a string) {
2635 do(something(with<Types>.and_arrays[0, 2])ˇ)
2636 }"});
2637
2638 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2639 .await;
2640 cx.simulate_shared_keystrokes("] )").await;
2641 cx.shared_state()
2642 .await
2643 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2644
2645 // test it works on immediate nesting
2646 cx.set_shared_state("{ˇ {}{}}").await;
2647 cx.simulate_shared_keystrokes("] }").await;
2648 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2649 cx.set_shared_state("(ˇ ()())").await;
2650 cx.simulate_shared_keystrokes("] )").await;
2651 cx.shared_state().await.assert_eq("( ()()ˇ)");
2652
2653 // test it works on immediate nesting inside braces
2654 cx.set_shared_state("{\n ˇ {()}\n}").await;
2655 cx.simulate_shared_keystrokes("] }").await;
2656 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
2657 cx.set_shared_state("(\n ˇ {()}\n)").await;
2658 cx.simulate_shared_keystrokes("] )").await;
2659 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
2660 }
2661
2662 #[gpui::test]
2663 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2664 let mut cx = NeovimBackedTestContext::new(cx).await;
2665
2666 // test it works with curly braces
2667 cx.set_shared_state(indoc! {r"func (a string) {
2668 do(something(with<Types>.anˇd_arrays[0, 2]))
2669 }"})
2670 .await;
2671 cx.simulate_shared_keystrokes("[ {").await;
2672 cx.shared_state()
2673 .await
2674 .assert_eq(indoc! {r"func (a string) ˇ{
2675 do(something(with<Types>.and_arrays[0, 2]))
2676 }"});
2677
2678 // test it works with brackets
2679 cx.set_shared_state(indoc! {r"func (a string) {
2680 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2681 }"})
2682 .await;
2683 cx.simulate_shared_keystrokes("[ (").await;
2684 cx.shared_state()
2685 .await
2686 .assert_eq(indoc! {r"func (a string) {
2687 doˇ(something(with<Types>.and_arrays[0, 2]))
2688 }"});
2689
2690 // test it works on immediate nesting
2691 cx.set_shared_state("{{}{} ˇ }").await;
2692 cx.simulate_shared_keystrokes("[ {").await;
2693 cx.shared_state().await.assert_eq("ˇ{{}{} }");
2694 cx.set_shared_state("(()() ˇ )").await;
2695 cx.simulate_shared_keystrokes("[ (").await;
2696 cx.shared_state().await.assert_eq("ˇ(()() )");
2697
2698 // test it works on immediate nesting inside braces
2699 cx.set_shared_state("{\n {()} ˇ\n}").await;
2700 cx.simulate_shared_keystrokes("[ {").await;
2701 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
2702 cx.set_shared_state("(\n {()} ˇ\n)").await;
2703 cx.simulate_shared_keystrokes("[ (").await;
2704 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
2705 }
2706
2707 #[gpui::test]
2708 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2709 let mut cx = NeovimBackedTestContext::new_html(cx).await;
2710
2711 cx.neovim.exec("set filetype=html").await;
2712
2713 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2714 cx.simulate_shared_keystrokes("%").await;
2715 cx.shared_state()
2716 .await
2717 .assert_eq(indoc! {r"<body><ˇ/body>"});
2718 cx.simulate_shared_keystrokes("%").await;
2719
2720 // test jumping backwards
2721 cx.shared_state()
2722 .await
2723 .assert_eq(indoc! {r"<ˇbody></body>"});
2724
2725 // test self-closing tags
2726 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2727 cx.simulate_shared_keystrokes("%").await;
2728 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2729
2730 // test tag with attributes
2731 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2732 </div>
2733 "})
2734 .await;
2735 cx.simulate_shared_keystrokes("%").await;
2736 cx.shared_state()
2737 .await
2738 .assert_eq(indoc! {r"<div class='test' id='main'>
2739 <ˇ/div>
2740 "});
2741
2742 // test multi-line self-closing tag
2743 cx.set_shared_state(indoc! {r#"<a>
2744 <br
2745 test = "test"
2746 /ˇ>
2747 </a>"#})
2748 .await;
2749 cx.simulate_shared_keystrokes("%").await;
2750 cx.shared_state().await.assert_eq(indoc! {r#"<a>
2751 ˇ<br
2752 test = "test"
2753 />
2754 </a>"#});
2755 }
2756
2757 #[gpui::test]
2758 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2759 let mut cx = NeovimBackedTestContext::new(cx).await;
2760
2761 // f and F
2762 cx.set_shared_state("ˇone two three four").await;
2763 cx.simulate_shared_keystrokes("f o").await;
2764 cx.shared_state().await.assert_eq("one twˇo three four");
2765 cx.simulate_shared_keystrokes(",").await;
2766 cx.shared_state().await.assert_eq("ˇone two three four");
2767 cx.simulate_shared_keystrokes("2 ;").await;
2768 cx.shared_state().await.assert_eq("one two three fˇour");
2769 cx.simulate_shared_keystrokes("shift-f e").await;
2770 cx.shared_state().await.assert_eq("one two threˇe four");
2771 cx.simulate_shared_keystrokes("2 ;").await;
2772 cx.shared_state().await.assert_eq("onˇe two three four");
2773 cx.simulate_shared_keystrokes(",").await;
2774 cx.shared_state().await.assert_eq("one two thrˇee four");
2775
2776 // t and T
2777 cx.set_shared_state("ˇone two three four").await;
2778 cx.simulate_shared_keystrokes("t o").await;
2779 cx.shared_state().await.assert_eq("one tˇwo three four");
2780 cx.simulate_shared_keystrokes(",").await;
2781 cx.shared_state().await.assert_eq("oˇne two three four");
2782 cx.simulate_shared_keystrokes("2 ;").await;
2783 cx.shared_state().await.assert_eq("one two three ˇfour");
2784 cx.simulate_shared_keystrokes("shift-t e").await;
2785 cx.shared_state().await.assert_eq("one two threeˇ four");
2786 cx.simulate_shared_keystrokes("3 ;").await;
2787 cx.shared_state().await.assert_eq("oneˇ two three four");
2788 cx.simulate_shared_keystrokes(",").await;
2789 cx.shared_state().await.assert_eq("one two thˇree four");
2790 }
2791
2792 #[gpui::test]
2793 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2794 let mut cx = NeovimBackedTestContext::new(cx).await;
2795 let initial_state = indoc! {r"something(ˇfoo)"};
2796 cx.set_shared_state(initial_state).await;
2797 cx.simulate_shared_keystrokes("}").await;
2798 cx.shared_state().await.assert_eq("something(fooˇ)");
2799 }
2800
2801 #[gpui::test]
2802 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2803 let mut cx = NeovimBackedTestContext::new(cx).await;
2804 cx.set_shared_state("ˇone\n two\nthree").await;
2805 cx.simulate_shared_keystrokes("enter").await;
2806 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
2807 }
2808
2809 #[gpui::test]
2810 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2811 let mut cx = NeovimBackedTestContext::new(cx).await;
2812 cx.set_shared_state("ˇ one\n two \nthree").await;
2813 cx.simulate_shared_keystrokes("g _").await;
2814 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
2815
2816 cx.set_shared_state("ˇ one \n two \nthree").await;
2817 cx.simulate_shared_keystrokes("g _").await;
2818 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
2819 cx.simulate_shared_keystrokes("2 g _").await;
2820 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2821 }
2822
2823 #[gpui::test]
2824 async fn test_window_top(cx: &mut gpui::TestAppContext) {
2825 let mut cx = NeovimBackedTestContext::new(cx).await;
2826 let initial_state = indoc! {r"abc
2827 def
2828 paragraph
2829 the second
2830 third ˇand
2831 final"};
2832
2833 cx.set_shared_state(initial_state).await;
2834 cx.simulate_shared_keystrokes("shift-h").await;
2835 cx.shared_state().await.assert_eq(indoc! {r"abˇc
2836 def
2837 paragraph
2838 the second
2839 third and
2840 final"});
2841
2842 // clip point
2843 cx.set_shared_state(indoc! {r"
2844 1 2 3
2845 4 5 6
2846 7 8 ˇ9
2847 "})
2848 .await;
2849 cx.simulate_shared_keystrokes("shift-h").await;
2850 cx.shared_state().await.assert_eq(indoc! {"
2851 1 2 ˇ3
2852 4 5 6
2853 7 8 9
2854 "});
2855
2856 cx.set_shared_state(indoc! {r"
2857 1 2 3
2858 4 5 6
2859 ˇ7 8 9
2860 "})
2861 .await;
2862 cx.simulate_shared_keystrokes("shift-h").await;
2863 cx.shared_state().await.assert_eq(indoc! {"
2864 ˇ1 2 3
2865 4 5 6
2866 7 8 9
2867 "});
2868
2869 cx.set_shared_state(indoc! {r"
2870 1 2 3
2871 4 5 ˇ6
2872 7 8 9"})
2873 .await;
2874 cx.simulate_shared_keystrokes("9 shift-h").await;
2875 cx.shared_state().await.assert_eq(indoc! {"
2876 1 2 3
2877 4 5 6
2878 7 8 ˇ9"});
2879 }
2880
2881 #[gpui::test]
2882 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2883 let mut cx = NeovimBackedTestContext::new(cx).await;
2884 let initial_state = indoc! {r"abˇc
2885 def
2886 paragraph
2887 the second
2888 third and
2889 final"};
2890
2891 cx.set_shared_state(initial_state).await;
2892 cx.simulate_shared_keystrokes("shift-m").await;
2893 cx.shared_state().await.assert_eq(indoc! {r"abc
2894 def
2895 paˇragraph
2896 the second
2897 third and
2898 final"});
2899
2900 cx.set_shared_state(indoc! {r"
2901 1 2 3
2902 4 5 6
2903 7 8 ˇ9
2904 "})
2905 .await;
2906 cx.simulate_shared_keystrokes("shift-m").await;
2907 cx.shared_state().await.assert_eq(indoc! {"
2908 1 2 3
2909 4 5 ˇ6
2910 7 8 9
2911 "});
2912 cx.set_shared_state(indoc! {r"
2913 1 2 3
2914 4 5 6
2915 ˇ7 8 9
2916 "})
2917 .await;
2918 cx.simulate_shared_keystrokes("shift-m").await;
2919 cx.shared_state().await.assert_eq(indoc! {"
2920 1 2 3
2921 ˇ4 5 6
2922 7 8 9
2923 "});
2924 cx.set_shared_state(indoc! {r"
2925 ˇ1 2 3
2926 4 5 6
2927 7 8 9
2928 "})
2929 .await;
2930 cx.simulate_shared_keystrokes("shift-m").await;
2931 cx.shared_state().await.assert_eq(indoc! {"
2932 1 2 3
2933 ˇ4 5 6
2934 7 8 9
2935 "});
2936 cx.set_shared_state(indoc! {r"
2937 1 2 3
2938 ˇ4 5 6
2939 7 8 9
2940 "})
2941 .await;
2942 cx.simulate_shared_keystrokes("shift-m").await;
2943 cx.shared_state().await.assert_eq(indoc! {"
2944 1 2 3
2945 ˇ4 5 6
2946 7 8 9
2947 "});
2948 cx.set_shared_state(indoc! {r"
2949 1 2 3
2950 4 5 ˇ6
2951 7 8 9
2952 "})
2953 .await;
2954 cx.simulate_shared_keystrokes("shift-m").await;
2955 cx.shared_state().await.assert_eq(indoc! {"
2956 1 2 3
2957 4 5 ˇ6
2958 7 8 9
2959 "});
2960 }
2961
2962 #[gpui::test]
2963 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2964 let mut cx = NeovimBackedTestContext::new(cx).await;
2965 let initial_state = indoc! {r"abc
2966 deˇf
2967 paragraph
2968 the second
2969 third and
2970 final"};
2971
2972 cx.set_shared_state(initial_state).await;
2973 cx.simulate_shared_keystrokes("shift-l").await;
2974 cx.shared_state().await.assert_eq(indoc! {r"abc
2975 def
2976 paragraph
2977 the second
2978 third and
2979 fiˇnal"});
2980
2981 cx.set_shared_state(indoc! {r"
2982 1 2 3
2983 4 5 ˇ6
2984 7 8 9
2985 "})
2986 .await;
2987 cx.simulate_shared_keystrokes("shift-l").await;
2988 cx.shared_state().await.assert_eq(indoc! {"
2989 1 2 3
2990 4 5 6
2991 7 8 9
2992 ˇ"});
2993
2994 cx.set_shared_state(indoc! {r"
2995 1 2 3
2996 ˇ4 5 6
2997 7 8 9
2998 "})
2999 .await;
3000 cx.simulate_shared_keystrokes("shift-l").await;
3001 cx.shared_state().await.assert_eq(indoc! {"
3002 1 2 3
3003 4 5 6
3004 7 8 9
3005 ˇ"});
3006
3007 cx.set_shared_state(indoc! {r"
3008 1 2 ˇ3
3009 4 5 6
3010 7 8 9
3011 "})
3012 .await;
3013 cx.simulate_shared_keystrokes("shift-l").await;
3014 cx.shared_state().await.assert_eq(indoc! {"
3015 1 2 3
3016 4 5 6
3017 7 8 9
3018 ˇ"});
3019
3020 cx.set_shared_state(indoc! {r"
3021 ˇ1 2 3
3022 4 5 6
3023 7 8 9
3024 "})
3025 .await;
3026 cx.simulate_shared_keystrokes("shift-l").await;
3027 cx.shared_state().await.assert_eq(indoc! {"
3028 1 2 3
3029 4 5 6
3030 7 8 9
3031 ˇ"});
3032
3033 cx.set_shared_state(indoc! {r"
3034 1 2 3
3035 4 5 ˇ6
3036 7 8 9
3037 "})
3038 .await;
3039 cx.simulate_shared_keystrokes("9 shift-l").await;
3040 cx.shared_state().await.assert_eq(indoc! {"
3041 1 2 ˇ3
3042 4 5 6
3043 7 8 9
3044 "});
3045 }
3046
3047 #[gpui::test]
3048 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3049 let mut cx = NeovimBackedTestContext::new(cx).await;
3050 cx.set_shared_state(indoc! {r"
3051 456 5ˇ67 678
3052 "})
3053 .await;
3054 cx.simulate_shared_keystrokes("g e").await;
3055 cx.shared_state().await.assert_eq(indoc! {"
3056 45ˇ6 567 678
3057 "});
3058
3059 // Test times
3060 cx.set_shared_state(indoc! {r"
3061 123 234 345
3062 456 5ˇ67 678
3063 "})
3064 .await;
3065 cx.simulate_shared_keystrokes("4 g e").await;
3066 cx.shared_state().await.assert_eq(indoc! {"
3067 12ˇ3 234 345
3068 456 567 678
3069 "});
3070
3071 // With punctuation
3072 cx.set_shared_state(indoc! {r"
3073 123 234 345
3074 4;5.6 5ˇ67 678
3075 789 890 901
3076 "})
3077 .await;
3078 cx.simulate_shared_keystrokes("g e").await;
3079 cx.shared_state().await.assert_eq(indoc! {"
3080 123 234 345
3081 4;5.ˇ6 567 678
3082 789 890 901
3083 "});
3084
3085 // With punctuation and count
3086 cx.set_shared_state(indoc! {r"
3087 123 234 345
3088 4;5.6 5ˇ67 678
3089 789 890 901
3090 "})
3091 .await;
3092 cx.simulate_shared_keystrokes("5 g e").await;
3093 cx.shared_state().await.assert_eq(indoc! {"
3094 123 234 345
3095 ˇ4;5.6 567 678
3096 789 890 901
3097 "});
3098
3099 // newlines
3100 cx.set_shared_state(indoc! {r"
3101 123 234 345
3102
3103 78ˇ9 890 901
3104 "})
3105 .await;
3106 cx.simulate_shared_keystrokes("g e").await;
3107 cx.shared_state().await.assert_eq(indoc! {"
3108 123 234 345
3109 ˇ
3110 789 890 901
3111 "});
3112 cx.simulate_shared_keystrokes("g e").await;
3113 cx.shared_state().await.assert_eq(indoc! {"
3114 123 234 34ˇ5
3115
3116 789 890 901
3117 "});
3118
3119 // With punctuation
3120 cx.set_shared_state(indoc! {r"
3121 123 234 345
3122 4;5.ˇ6 567 678
3123 789 890 901
3124 "})
3125 .await;
3126 cx.simulate_shared_keystrokes("g shift-e").await;
3127 cx.shared_state().await.assert_eq(indoc! {"
3128 123 234 34ˇ5
3129 4;5.6 567 678
3130 789 890 901
3131 "});
3132 }
3133
3134 #[gpui::test]
3135 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3136 let mut cx = NeovimBackedTestContext::new(cx).await;
3137
3138 cx.set_shared_state(indoc! {"
3139 fn aˇ() {
3140 return
3141 }
3142 "})
3143 .await;
3144 cx.simulate_shared_keystrokes("v $ %").await;
3145 cx.shared_state().await.assert_eq(indoc! {"
3146 fn a«() {
3147 return
3148 }ˇ»
3149 "});
3150 }
3151
3152 #[gpui::test]
3153 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3154 let mut cx = VimTestContext::new(cx, true).await;
3155
3156 cx.set_state(
3157 indoc! {"
3158 struct Foo {
3159 ˇ
3160 }
3161 "},
3162 Mode::Normal,
3163 );
3164
3165 cx.update_editor(|editor, cx| {
3166 let range = editor.selections.newest_anchor().range();
3167 let inlay_text = " field: int,\n field2: string\n field3: float";
3168 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3169 editor.splice_inlays(vec![], vec![inlay], cx);
3170 });
3171
3172 cx.simulate_keystrokes("j");
3173 cx.assert_state(
3174 indoc! {"
3175 struct Foo {
3176
3177 ˇ}
3178 "},
3179 Mode::Normal,
3180 );
3181 }
3182}