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