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 | SentenceBackward
589 | SentenceForward
590 | StartOfParagraph
591 | EndOfParagraph
592 | WindowTop
593 | WindowMiddle
594 | WindowBottom
595 | NextSectionStart
596 | NextSectionEnd
597 | PreviousSectionStart
598 | PreviousSectionEnd
599 | NextMethodStart
600 | NextMethodEnd
601 | PreviousMethodStart
602 | PreviousMethodEnd
603 | NextComment
604 | PreviousComment
605 | Jump { line: true, .. } => true,
606 EndOfLine { .. }
607 | Matching
608 | UnmatchedForward { .. }
609 | UnmatchedBackward { .. }
610 | FindForward { .. }
611 | Left
612 | Backspace
613 | Right
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 start = map.display_point_to_fold_point(point, Bias::Left);
1210 let begin_folded_line = map.fold_point_to_display_point(
1211 map.fold_snapshot
1212 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1213 );
1214 let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1215
1216 let (goal_wrap, goal_x) = match goal {
1217 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1218 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1219 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1220 _ => {
1221 let x = map.x_for_display_point(point, text_layout_details);
1222 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1223 (select_nth_wrapped_row, x.0)
1224 }
1225 };
1226
1227 let target = start.row() as isize + times;
1228 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1229
1230 let mut begin_folded_line = map.fold_point_to_display_point(
1231 map.fold_snapshot
1232 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
1233 );
1234
1235 let mut i = 0;
1236 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1237 let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1238 if map
1239 .display_point_to_fold_point(next_folded_line, Bias::Right)
1240 .row()
1241 == new_row
1242 {
1243 i += 1;
1244 begin_folded_line = next_folded_line;
1245 } else {
1246 break;
1247 }
1248 }
1249
1250 let new_col = if i == goal_wrap {
1251 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1252 } else {
1253 map.line_len(begin_folded_line.row())
1254 };
1255
1256 (
1257 map.clip_point(
1258 DisplayPoint::new(begin_folded_line.row(), new_col),
1259 Bias::Left,
1260 ),
1261 goal,
1262 )
1263}
1264
1265fn down_display(
1266 map: &DisplaySnapshot,
1267 mut point: DisplayPoint,
1268 mut goal: SelectionGoal,
1269 times: usize,
1270 text_layout_details: &TextLayoutDetails,
1271) -> (DisplayPoint, SelectionGoal) {
1272 for _ in 0..times {
1273 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1274 }
1275
1276 (point, goal)
1277}
1278
1279fn up_display(
1280 map: &DisplaySnapshot,
1281 mut point: DisplayPoint,
1282 mut goal: SelectionGoal,
1283 times: usize,
1284 text_layout_details: &TextLayoutDetails,
1285) -> (DisplayPoint, SelectionGoal) {
1286 for _ in 0..times {
1287 (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1288 }
1289
1290 (point, goal)
1291}
1292
1293pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1294 for _ in 0..times {
1295 let new_point = movement::saturating_right(map, point);
1296 if point == new_point {
1297 break;
1298 }
1299 point = new_point;
1300 }
1301 point
1302}
1303
1304pub(crate) fn next_char(
1305 map: &DisplaySnapshot,
1306 point: DisplayPoint,
1307 allow_cross_newline: bool,
1308) -> DisplayPoint {
1309 let mut new_point = point;
1310 let mut max_column = map.line_len(new_point.row());
1311 if !allow_cross_newline {
1312 max_column -= 1;
1313 }
1314 if new_point.column() < max_column {
1315 *new_point.column_mut() += 1;
1316 } else if new_point < map.max_point() && allow_cross_newline {
1317 *new_point.row_mut() += 1;
1318 *new_point.column_mut() = 0;
1319 }
1320 map.clip_ignoring_line_ends(new_point, Bias::Right)
1321}
1322
1323pub(crate) fn next_word_start(
1324 map: &DisplaySnapshot,
1325 mut point: DisplayPoint,
1326 ignore_punctuation: bool,
1327 times: usize,
1328) -> DisplayPoint {
1329 let classifier = map
1330 .buffer_snapshot
1331 .char_classifier_at(point.to_point(map))
1332 .ignore_punctuation(ignore_punctuation);
1333 for _ in 0..times {
1334 let mut crossed_newline = false;
1335 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1336 let left_kind = classifier.kind(left);
1337 let right_kind = classifier.kind(right);
1338 let at_newline = right == '\n';
1339
1340 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1341 || at_newline && crossed_newline
1342 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1343
1344 crossed_newline |= at_newline;
1345 found
1346 });
1347 if point == new_point {
1348 break;
1349 }
1350 point = new_point;
1351 }
1352 point
1353}
1354
1355pub(crate) fn next_word_end(
1356 map: &DisplaySnapshot,
1357 mut point: DisplayPoint,
1358 ignore_punctuation: bool,
1359 times: usize,
1360 allow_cross_newline: bool,
1361) -> DisplayPoint {
1362 let classifier = map
1363 .buffer_snapshot
1364 .char_classifier_at(point.to_point(map))
1365 .ignore_punctuation(ignore_punctuation);
1366 for _ in 0..times {
1367 let new_point = next_char(map, point, allow_cross_newline);
1368 let mut need_next_char = false;
1369 let new_point = movement::find_boundary_exclusive(
1370 map,
1371 new_point,
1372 FindRange::MultiLine,
1373 |left, right| {
1374 let left_kind = classifier.kind(left);
1375 let right_kind = classifier.kind(right);
1376 let at_newline = right == '\n';
1377
1378 if !allow_cross_newline && at_newline {
1379 need_next_char = true;
1380 return true;
1381 }
1382
1383 left_kind != right_kind && left_kind != CharKind::Whitespace
1384 },
1385 );
1386 let new_point = if need_next_char {
1387 next_char(map, new_point, true)
1388 } else {
1389 new_point
1390 };
1391 let new_point = map.clip_point(new_point, Bias::Left);
1392 if point == new_point {
1393 break;
1394 }
1395 point = new_point;
1396 }
1397 point
1398}
1399
1400fn previous_word_start(
1401 map: &DisplaySnapshot,
1402 mut point: DisplayPoint,
1403 ignore_punctuation: bool,
1404 times: usize,
1405) -> DisplayPoint {
1406 let classifier = map
1407 .buffer_snapshot
1408 .char_classifier_at(point.to_point(map))
1409 .ignore_punctuation(ignore_punctuation);
1410 for _ in 0..times {
1411 // This works even though find_preceding_boundary is called for every character in the line containing
1412 // cursor because the newline is checked only once.
1413 let new_point = movement::find_preceding_boundary_display_point(
1414 map,
1415 point,
1416 FindRange::MultiLine,
1417 |left, right| {
1418 let left_kind = classifier.kind(left);
1419 let right_kind = classifier.kind(right);
1420
1421 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1422 },
1423 );
1424 if point == new_point {
1425 break;
1426 }
1427 point = new_point;
1428 }
1429 point
1430}
1431
1432fn previous_word_end(
1433 map: &DisplaySnapshot,
1434 point: DisplayPoint,
1435 ignore_punctuation: bool,
1436 times: usize,
1437) -> DisplayPoint {
1438 let classifier = map
1439 .buffer_snapshot
1440 .char_classifier_at(point.to_point(map))
1441 .ignore_punctuation(ignore_punctuation);
1442 let mut point = point.to_point(map);
1443
1444 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1445 point.column += 1;
1446 }
1447 for _ in 0..times {
1448 let new_point = movement::find_preceding_boundary_point(
1449 &map.buffer_snapshot,
1450 point,
1451 FindRange::MultiLine,
1452 |left, right| {
1453 let left_kind = classifier.kind(left);
1454 let right_kind = classifier.kind(right);
1455 match (left_kind, right_kind) {
1456 (CharKind::Punctuation, CharKind::Whitespace)
1457 | (CharKind::Punctuation, CharKind::Word)
1458 | (CharKind::Word, CharKind::Whitespace)
1459 | (CharKind::Word, CharKind::Punctuation) => true,
1460 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1461 _ => false,
1462 }
1463 },
1464 );
1465 if new_point == point {
1466 break;
1467 }
1468 point = new_point;
1469 }
1470 movement::saturating_left(map, point.to_display_point(map))
1471}
1472
1473fn next_subword_start(
1474 map: &DisplaySnapshot,
1475 mut point: DisplayPoint,
1476 ignore_punctuation: bool,
1477 times: usize,
1478) -> DisplayPoint {
1479 let classifier = map
1480 .buffer_snapshot
1481 .char_classifier_at(point.to_point(map))
1482 .ignore_punctuation(ignore_punctuation);
1483 for _ in 0..times {
1484 let mut crossed_newline = false;
1485 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1486 let left_kind = classifier.kind(left);
1487 let right_kind = classifier.kind(right);
1488 let at_newline = right == '\n';
1489
1490 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1491 let is_subword_start =
1492 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1493
1494 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1495 || at_newline && crossed_newline
1496 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1497
1498 crossed_newline |= at_newline;
1499 found
1500 });
1501 if point == new_point {
1502 break;
1503 }
1504 point = new_point;
1505 }
1506 point
1507}
1508
1509pub(crate) fn next_subword_end(
1510 map: &DisplaySnapshot,
1511 mut point: DisplayPoint,
1512 ignore_punctuation: bool,
1513 times: usize,
1514 allow_cross_newline: bool,
1515) -> DisplayPoint {
1516 let classifier = map
1517 .buffer_snapshot
1518 .char_classifier_at(point.to_point(map))
1519 .ignore_punctuation(ignore_punctuation);
1520 for _ in 0..times {
1521 let new_point = next_char(map, point, allow_cross_newline);
1522
1523 let mut crossed_newline = false;
1524 let mut need_backtrack = false;
1525 let new_point =
1526 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1527 let left_kind = classifier.kind(left);
1528 let right_kind = classifier.kind(right);
1529 let at_newline = right == '\n';
1530
1531 if !allow_cross_newline && at_newline {
1532 return true;
1533 }
1534
1535 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1536 let is_subword_end =
1537 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1538
1539 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1540
1541 if found && (is_word_end || is_subword_end) {
1542 need_backtrack = true;
1543 }
1544
1545 crossed_newline |= at_newline;
1546 found
1547 });
1548 let mut new_point = map.clip_point(new_point, Bias::Left);
1549 if need_backtrack {
1550 *new_point.column_mut() -= 1;
1551 }
1552 if point == new_point {
1553 break;
1554 }
1555 point = new_point;
1556 }
1557 point
1558}
1559
1560fn previous_subword_start(
1561 map: &DisplaySnapshot,
1562 mut point: DisplayPoint,
1563 ignore_punctuation: bool,
1564 times: usize,
1565) -> DisplayPoint {
1566 let classifier = map
1567 .buffer_snapshot
1568 .char_classifier_at(point.to_point(map))
1569 .ignore_punctuation(ignore_punctuation);
1570 for _ in 0..times {
1571 let mut crossed_newline = false;
1572 // This works even though find_preceding_boundary is called for every character in the line containing
1573 // cursor because the newline is checked only once.
1574 let new_point = movement::find_preceding_boundary_display_point(
1575 map,
1576 point,
1577 FindRange::MultiLine,
1578 |left, right| {
1579 let left_kind = classifier.kind(left);
1580 let right_kind = classifier.kind(right);
1581 let at_newline = right == '\n';
1582
1583 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1584 let is_subword_start =
1585 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1586
1587 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1588 || at_newline && crossed_newline
1589 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1590
1591 crossed_newline |= at_newline;
1592
1593 found
1594 },
1595 );
1596 if point == new_point {
1597 break;
1598 }
1599 point = new_point;
1600 }
1601 point
1602}
1603
1604fn previous_subword_end(
1605 map: &DisplaySnapshot,
1606 point: DisplayPoint,
1607 ignore_punctuation: bool,
1608 times: usize,
1609) -> DisplayPoint {
1610 let classifier = map
1611 .buffer_snapshot
1612 .char_classifier_at(point.to_point(map))
1613 .ignore_punctuation(ignore_punctuation);
1614 let mut point = point.to_point(map);
1615
1616 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1617 point.column += 1;
1618 }
1619 for _ in 0..times {
1620 let new_point = movement::find_preceding_boundary_point(
1621 &map.buffer_snapshot,
1622 point,
1623 FindRange::MultiLine,
1624 |left, right| {
1625 let left_kind = classifier.kind(left);
1626 let right_kind = classifier.kind(right);
1627
1628 let is_subword_end =
1629 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1630
1631 if is_subword_end {
1632 return true;
1633 }
1634
1635 match (left_kind, right_kind) {
1636 (CharKind::Word, CharKind::Whitespace)
1637 | (CharKind::Word, CharKind::Punctuation) => true,
1638 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1639 _ => false,
1640 }
1641 },
1642 );
1643 if new_point == point {
1644 break;
1645 }
1646 point = new_point;
1647 }
1648 movement::saturating_left(map, point.to_display_point(map))
1649}
1650
1651pub(crate) fn first_non_whitespace(
1652 map: &DisplaySnapshot,
1653 display_lines: bool,
1654 from: DisplayPoint,
1655) -> DisplayPoint {
1656 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1657 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1658 for (ch, offset) in map.buffer_chars_at(start_offset) {
1659 if ch == '\n' {
1660 return from;
1661 }
1662
1663 start_offset = offset;
1664
1665 if classifier.kind(ch) != CharKind::Whitespace {
1666 break;
1667 }
1668 }
1669
1670 start_offset.to_display_point(map)
1671}
1672
1673pub(crate) fn last_non_whitespace(
1674 map: &DisplaySnapshot,
1675 from: DisplayPoint,
1676 count: usize,
1677) -> DisplayPoint {
1678 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1679 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1680
1681 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1682 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1683 if classifier.kind(ch) != CharKind::Whitespace {
1684 return end_of_line.to_display_point(map);
1685 }
1686 }
1687
1688 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1689 if ch == '\n' {
1690 break;
1691 }
1692 end_of_line = offset;
1693 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1694 break;
1695 }
1696 }
1697
1698 end_of_line.to_display_point(map)
1699}
1700
1701pub(crate) fn start_of_line(
1702 map: &DisplaySnapshot,
1703 display_lines: bool,
1704 point: DisplayPoint,
1705) -> DisplayPoint {
1706 if display_lines {
1707 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1708 } else {
1709 map.prev_line_boundary(point.to_point(map)).1
1710 }
1711}
1712
1713pub(crate) fn end_of_line(
1714 map: &DisplaySnapshot,
1715 display_lines: bool,
1716 mut point: DisplayPoint,
1717 times: usize,
1718) -> DisplayPoint {
1719 if times > 1 {
1720 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1721 }
1722 if display_lines {
1723 map.clip_point(
1724 DisplayPoint::new(point.row(), map.line_len(point.row())),
1725 Bias::Left,
1726 )
1727 } else {
1728 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1729 }
1730}
1731
1732fn sentence_backwards(
1733 map: &DisplaySnapshot,
1734 point: DisplayPoint,
1735 mut times: usize,
1736) -> DisplayPoint {
1737 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1738 let mut chars = map.reverse_buffer_chars_at(start).peekable();
1739
1740 let mut was_newline = map
1741 .buffer_chars_at(start)
1742 .next()
1743 .is_some_and(|(c, _)| c == '\n');
1744
1745 while let Some((ch, offset)) = chars.next() {
1746 let start_of_next_sentence = if was_newline && ch == '\n' {
1747 Some(offset + ch.len_utf8())
1748 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1749 Some(next_non_blank(map, offset + ch.len_utf8()))
1750 } else if ch == '.' || ch == '?' || ch == '!' {
1751 start_of_next_sentence(map, offset + ch.len_utf8())
1752 } else {
1753 None
1754 };
1755
1756 if let Some(start_of_next_sentence) = start_of_next_sentence {
1757 if start_of_next_sentence < start {
1758 times = times.saturating_sub(1);
1759 }
1760 if times == 0 || offset == 0 {
1761 return map.clip_point(
1762 start_of_next_sentence
1763 .to_offset(&map.buffer_snapshot)
1764 .to_display_point(map),
1765 Bias::Left,
1766 );
1767 }
1768 }
1769 if was_newline {
1770 start = offset;
1771 }
1772 was_newline = ch == '\n';
1773 }
1774
1775 DisplayPoint::zero()
1776}
1777
1778fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1779 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1780 let mut chars = map.buffer_chars_at(start).peekable();
1781
1782 let mut was_newline = map
1783 .reverse_buffer_chars_at(start)
1784 .next()
1785 .is_some_and(|(c, _)| c == '\n')
1786 && chars.peek().is_some_and(|(c, _)| *c == '\n');
1787
1788 while let Some((ch, offset)) = chars.next() {
1789 if was_newline && ch == '\n' {
1790 continue;
1791 }
1792 let start_of_next_sentence = if was_newline {
1793 Some(next_non_blank(map, offset))
1794 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1795 Some(next_non_blank(map, offset + ch.len_utf8()))
1796 } else if ch == '.' || ch == '?' || ch == '!' {
1797 start_of_next_sentence(map, offset + ch.len_utf8())
1798 } else {
1799 None
1800 };
1801
1802 if let Some(start_of_next_sentence) = start_of_next_sentence {
1803 times = times.saturating_sub(1);
1804 if times == 0 {
1805 return map.clip_point(
1806 start_of_next_sentence
1807 .to_offset(&map.buffer_snapshot)
1808 .to_display_point(map),
1809 Bias::Right,
1810 );
1811 }
1812 }
1813
1814 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1815 }
1816
1817 map.max_point()
1818}
1819
1820fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1821 for (c, o) in map.buffer_chars_at(start) {
1822 if c == '\n' || !c.is_whitespace() {
1823 return o;
1824 }
1825 }
1826
1827 map.buffer_snapshot.len()
1828}
1829
1830// given the offset after a ., !, or ? find the start of the next sentence.
1831// if this is not a sentence boundary, returns None.
1832fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1833 let chars = map.buffer_chars_at(end_of_sentence);
1834 let mut seen_space = false;
1835
1836 for (char, offset) in chars {
1837 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1838 continue;
1839 }
1840
1841 if char == '\n' && seen_space {
1842 return Some(offset);
1843 } else if char.is_whitespace() {
1844 seen_space = true;
1845 } else if seen_space {
1846 return Some(offset);
1847 } else {
1848 return None;
1849 }
1850 }
1851
1852 Some(map.buffer_snapshot.len())
1853}
1854
1855fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1856 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1857 *new_point.column_mut() = point.column();
1858 map.clip_point(new_point, Bias::Left)
1859}
1860
1861fn end_of_document(
1862 map: &DisplaySnapshot,
1863 point: DisplayPoint,
1864 line: Option<usize>,
1865) -> DisplayPoint {
1866 let new_row = if let Some(line) = line {
1867 (line - 1) as u32
1868 } else {
1869 map.buffer_snapshot.max_row().0
1870 };
1871
1872 let new_point = Point::new(new_row, point.column());
1873 map.clip_point(new_point.to_display_point(map), Bias::Left)
1874}
1875
1876fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
1877 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
1878 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
1879
1880 if head > outer.start && head < inner.start {
1881 let mut offset = inner.end.to_offset(map, Bias::Left);
1882 for c in map.buffer_snapshot.chars_at(offset) {
1883 if c == '/' || c == '\n' || c == '>' {
1884 return Some(offset.to_display_point(map));
1885 }
1886 offset += c.len_utf8();
1887 }
1888 } else {
1889 let mut offset = outer.start.to_offset(map, Bias::Left);
1890 for c in map.buffer_snapshot.chars_at(offset) {
1891 offset += c.len_utf8();
1892 if c == '<' || c == '\n' {
1893 return Some(offset.to_display_point(map));
1894 }
1895 }
1896 }
1897
1898 return None;
1899}
1900
1901fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1902 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1903 let display_point = map.clip_at_line_end(display_point);
1904 let point = display_point.to_point(map);
1905 let offset = point.to_offset(&map.buffer_snapshot);
1906
1907 // Ensure the range is contained by the current line.
1908 let mut line_end = map.next_line_boundary(point).0;
1909 if line_end == point {
1910 line_end = map.max_point().to_point(map);
1911 }
1912
1913 let line_range = map.prev_line_boundary(point).0..line_end;
1914 let visible_line_range =
1915 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1916 let ranges = map
1917 .buffer_snapshot
1918 .bracket_ranges(visible_line_range.clone());
1919 if let Some(ranges) = ranges {
1920 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1921 ..line_range.end.to_offset(&map.buffer_snapshot);
1922 let mut closest_pair_destination = None;
1923 let mut closest_distance = usize::MAX;
1924
1925 for (open_range, close_range) in ranges {
1926 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
1927 if offset > open_range.start && offset < close_range.start {
1928 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1929 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
1930 return display_point;
1931 }
1932 if let Some(tag) = matching_tag(map, display_point) {
1933 return tag;
1934 }
1935 } else if close_range.contains(&offset) {
1936 return open_range.start.to_display_point(map);
1937 } else if open_range.contains(&offset) {
1938 return (close_range.end - 1).to_display_point(map);
1939 }
1940 }
1941
1942 if open_range.start >= offset && line_range.contains(&open_range.start) {
1943 let distance = open_range.start - offset;
1944 if distance < closest_distance {
1945 closest_pair_destination = Some(close_range.end - 1);
1946 closest_distance = distance;
1947 continue;
1948 }
1949 }
1950
1951 if close_range.start >= offset && line_range.contains(&close_range.start) {
1952 let distance = close_range.start - offset;
1953 if distance < closest_distance {
1954 closest_pair_destination = Some(open_range.start);
1955 closest_distance = distance;
1956 continue;
1957 }
1958 }
1959
1960 continue;
1961 }
1962
1963 closest_pair_destination
1964 .map(|destination| destination.to_display_point(map))
1965 .unwrap_or(display_point)
1966 } else {
1967 display_point
1968 }
1969}
1970
1971fn unmatched_forward(
1972 map: &DisplaySnapshot,
1973 mut display_point: DisplayPoint,
1974 char: char,
1975 times: usize,
1976) -> DisplayPoint {
1977 for _ in 0..times {
1978 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
1979 let point = display_point.to_point(map);
1980 let offset = point.to_offset(&map.buffer_snapshot);
1981
1982 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
1983 let Some(ranges) = ranges else { break };
1984 let mut closest_closing_destination = None;
1985 let mut closest_distance = usize::MAX;
1986
1987 for (_, close_range) in ranges {
1988 if close_range.start > offset {
1989 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1990 if Some(char) == chars.next() {
1991 let distance = close_range.start - offset;
1992 if distance < closest_distance {
1993 closest_closing_destination = Some(close_range.start);
1994 closest_distance = distance;
1995 continue;
1996 }
1997 }
1998 }
1999 }
2000
2001 let new_point = closest_closing_destination
2002 .map(|destination| destination.to_display_point(map))
2003 .unwrap_or(display_point);
2004 if new_point == display_point {
2005 break;
2006 }
2007 display_point = new_point;
2008 }
2009 return display_point;
2010}
2011
2012fn unmatched_backward(
2013 map: &DisplaySnapshot,
2014 mut display_point: DisplayPoint,
2015 char: char,
2016 times: usize,
2017) -> DisplayPoint {
2018 for _ in 0..times {
2019 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2020 let point = display_point.to_point(map);
2021 let offset = point.to_offset(&map.buffer_snapshot);
2022
2023 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2024 let Some(ranges) = ranges else {
2025 break;
2026 };
2027
2028 let mut closest_starting_destination = None;
2029 let mut closest_distance = usize::MAX;
2030
2031 for (start_range, _) in ranges {
2032 if start_range.start < offset {
2033 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2034 if Some(char) == chars.next() {
2035 let distance = offset - start_range.start;
2036 if distance < closest_distance {
2037 closest_starting_destination = Some(start_range.start);
2038 closest_distance = distance;
2039 continue;
2040 }
2041 }
2042 }
2043 }
2044
2045 let new_point = closest_starting_destination
2046 .map(|destination| destination.to_display_point(map))
2047 .unwrap_or(display_point);
2048 if new_point == display_point {
2049 break;
2050 } else {
2051 display_point = new_point;
2052 }
2053 }
2054 display_point
2055}
2056
2057fn find_forward(
2058 map: &DisplaySnapshot,
2059 from: DisplayPoint,
2060 before: bool,
2061 target: char,
2062 times: usize,
2063 mode: FindRange,
2064 smartcase: bool,
2065) -> Option<DisplayPoint> {
2066 let mut to = from;
2067 let mut found = false;
2068
2069 for _ in 0..times {
2070 found = false;
2071 let new_to = find_boundary(map, to, mode, |_, right| {
2072 found = is_character_match(target, right, smartcase);
2073 found
2074 });
2075 if to == new_to {
2076 break;
2077 }
2078 to = new_to;
2079 }
2080
2081 if found {
2082 if before && to.column() > 0 {
2083 *to.column_mut() -= 1;
2084 Some(map.clip_point(to, Bias::Left))
2085 } else {
2086 Some(to)
2087 }
2088 } else {
2089 None
2090 }
2091}
2092
2093fn find_backward(
2094 map: &DisplaySnapshot,
2095 from: DisplayPoint,
2096 after: bool,
2097 target: char,
2098 times: usize,
2099 mode: FindRange,
2100 smartcase: bool,
2101) -> DisplayPoint {
2102 let mut to = from;
2103
2104 for _ in 0..times {
2105 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2106 is_character_match(target, right, smartcase)
2107 });
2108 if to == new_to {
2109 break;
2110 }
2111 to = new_to;
2112 }
2113
2114 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2115 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2116 if after {
2117 *to.column_mut() += 1;
2118 map.clip_point(to, Bias::Right)
2119 } else {
2120 to
2121 }
2122 } else {
2123 from
2124 }
2125}
2126
2127fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2128 if smartcase {
2129 if target.is_uppercase() {
2130 target == other
2131 } else {
2132 target == other.to_ascii_lowercase()
2133 }
2134 } else {
2135 target == other
2136 }
2137}
2138
2139fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2140 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2141 first_non_whitespace(map, false, correct_line)
2142}
2143
2144fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2145 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2146 first_non_whitespace(map, false, correct_line)
2147}
2148
2149fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2150 let correct_line = start_of_relative_buffer_row(map, point, 0);
2151 right(map, correct_line, times.saturating_sub(1))
2152}
2153
2154pub(crate) fn next_line_end(
2155 map: &DisplaySnapshot,
2156 mut point: DisplayPoint,
2157 times: usize,
2158) -> DisplayPoint {
2159 if times > 1 {
2160 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2161 }
2162 end_of_line(map, false, point, 1)
2163}
2164
2165fn window_top(
2166 map: &DisplaySnapshot,
2167 point: DisplayPoint,
2168 text_layout_details: &TextLayoutDetails,
2169 mut times: usize,
2170) -> (DisplayPoint, SelectionGoal) {
2171 let first_visible_line = text_layout_details
2172 .scroll_anchor
2173 .anchor
2174 .to_display_point(map);
2175
2176 if first_visible_line.row() != DisplayRow(0)
2177 && text_layout_details.vertical_scroll_margin as usize > times
2178 {
2179 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2180 }
2181
2182 if let Some(visible_rows) = text_layout_details.visible_rows {
2183 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2184 let new_row = (first_visible_line.row().0 + (times as u32))
2185 .min(bottom_row)
2186 .min(map.max_point().row().0);
2187 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2188
2189 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2190 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2191 } else {
2192 let new_row =
2193 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2194 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2195
2196 let new_point = DisplayPoint::new(new_row, new_col);
2197 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2198 }
2199}
2200
2201fn window_middle(
2202 map: &DisplaySnapshot,
2203 point: DisplayPoint,
2204 text_layout_details: &TextLayoutDetails,
2205) -> (DisplayPoint, SelectionGoal) {
2206 if let Some(visible_rows) = text_layout_details.visible_rows {
2207 let first_visible_line = text_layout_details
2208 .scroll_anchor
2209 .anchor
2210 .to_display_point(map);
2211
2212 let max_visible_rows =
2213 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2214
2215 let new_row =
2216 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2217 let new_row = DisplayRow(new_row);
2218 let new_col = point.column().min(map.line_len(new_row));
2219 let new_point = DisplayPoint::new(new_row, new_col);
2220 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2221 } else {
2222 (point, SelectionGoal::None)
2223 }
2224}
2225
2226fn window_bottom(
2227 map: &DisplaySnapshot,
2228 point: DisplayPoint,
2229 text_layout_details: &TextLayoutDetails,
2230 mut times: usize,
2231) -> (DisplayPoint, SelectionGoal) {
2232 if let Some(visible_rows) = text_layout_details.visible_rows {
2233 let first_visible_line = text_layout_details
2234 .scroll_anchor
2235 .anchor
2236 .to_display_point(map);
2237 let bottom_row = first_visible_line.row().0
2238 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2239 if bottom_row < map.max_point().row().0
2240 && text_layout_details.vertical_scroll_margin as usize > times
2241 {
2242 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2243 }
2244 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2245 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2246 {
2247 first_visible_line.row()
2248 } else {
2249 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2250 };
2251 let new_col = point.column().min(map.line_len(new_row));
2252 let new_point = DisplayPoint::new(new_row, new_col);
2253 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2254 } else {
2255 (point, SelectionGoal::None)
2256 }
2257}
2258
2259fn method_motion(
2260 map: &DisplaySnapshot,
2261 mut display_point: DisplayPoint,
2262 times: usize,
2263 direction: Direction,
2264 is_start: bool,
2265) -> DisplayPoint {
2266 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2267 return display_point;
2268 };
2269
2270 for _ in 0..times {
2271 let point = map.display_point_to_point(display_point, Bias::Left);
2272 let offset = point.to_offset(&map.buffer_snapshot);
2273 let range = if direction == Direction::Prev {
2274 0..offset
2275 } else {
2276 offset..buffer.len()
2277 };
2278
2279 let possibilities = buffer
2280 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2281 .filter_map(|(range, object)| {
2282 if !matches!(object, language::TextObject::AroundFunction) {
2283 return None;
2284 }
2285
2286 let relevant = if is_start { range.start } else { range.end };
2287 if direction == Direction::Prev && relevant < offset {
2288 Some(relevant)
2289 } else if direction == Direction::Next && relevant > offset + 1 {
2290 Some(relevant)
2291 } else {
2292 None
2293 }
2294 });
2295
2296 let dest = if direction == Direction::Prev {
2297 possibilities.max().unwrap_or(offset)
2298 } else {
2299 possibilities.min().unwrap_or(offset)
2300 };
2301 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2302 if new_point == display_point {
2303 break;
2304 }
2305 display_point = new_point;
2306 }
2307 display_point
2308}
2309
2310fn comment_motion(
2311 map: &DisplaySnapshot,
2312 mut display_point: DisplayPoint,
2313 times: usize,
2314 direction: Direction,
2315) -> DisplayPoint {
2316 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2317 return display_point;
2318 };
2319
2320 for _ in 0..times {
2321 let point = map.display_point_to_point(display_point, Bias::Left);
2322 let offset = point.to_offset(&map.buffer_snapshot);
2323 let range = if direction == Direction::Prev {
2324 0..offset
2325 } else {
2326 offset..buffer.len()
2327 };
2328
2329 let possibilities = buffer
2330 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2331 .filter_map(|(range, object)| {
2332 if !matches!(object, language::TextObject::AroundComment) {
2333 return None;
2334 }
2335
2336 let relevant = if direction == Direction::Prev {
2337 range.start
2338 } else {
2339 range.end
2340 };
2341 if direction == Direction::Prev && relevant < offset {
2342 Some(relevant)
2343 } else if direction == Direction::Next && relevant > offset + 1 {
2344 Some(relevant)
2345 } else {
2346 None
2347 }
2348 });
2349
2350 let dest = if direction == Direction::Prev {
2351 possibilities.max().unwrap_or(offset)
2352 } else {
2353 possibilities.min().unwrap_or(offset)
2354 };
2355 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2356 if new_point == display_point {
2357 break;
2358 }
2359 display_point = new_point;
2360 }
2361
2362 display_point
2363}
2364
2365fn section_motion(
2366 map: &DisplaySnapshot,
2367 mut display_point: DisplayPoint,
2368 times: usize,
2369 direction: Direction,
2370 is_start: bool,
2371) -> DisplayPoint {
2372 if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
2373 for _ in 0..times {
2374 let offset = map
2375 .display_point_to_point(display_point, Bias::Left)
2376 .to_offset(&map.buffer_snapshot);
2377 let range = if direction == Direction::Prev {
2378 0..offset
2379 } else {
2380 offset..buffer.len()
2381 };
2382
2383 // we set a max start depth here because we want a section to only be "top level"
2384 // similar to vim's default of '{' in the first column.
2385 // (and without it, ]] at the start of editor.rs is -very- slow)
2386 let mut possibilities = buffer
2387 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2388 .filter(|(_, object)| {
2389 matches!(
2390 object,
2391 language::TextObject::AroundClass | language::TextObject::AroundFunction
2392 )
2393 })
2394 .collect::<Vec<_>>();
2395 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2396 let mut prev_end = None;
2397 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2398 if t == language::TextObject::AroundFunction
2399 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2400 {
2401 return None;
2402 }
2403 prev_end = Some(range.end);
2404
2405 let relevant = if is_start { range.start } else { range.end };
2406 if direction == Direction::Prev && relevant < offset {
2407 Some(relevant)
2408 } else if direction == Direction::Next && relevant > offset + 1 {
2409 Some(relevant)
2410 } else {
2411 None
2412 }
2413 });
2414
2415 let offset = if direction == Direction::Prev {
2416 possibilities.max().unwrap_or(0)
2417 } else {
2418 possibilities.min().unwrap_or(buffer.len())
2419 };
2420
2421 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2422 if new_point == display_point {
2423 break;
2424 }
2425 display_point = new_point;
2426 }
2427 return display_point;
2428 };
2429
2430 for _ in 0..times {
2431 let point = map.display_point_to_point(display_point, Bias::Left);
2432 let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2433 return display_point;
2434 };
2435 let next_point = match (direction, is_start) {
2436 (Direction::Prev, true) => {
2437 let mut start = excerpt.start_anchor().to_display_point(&map);
2438 if start >= display_point && start.row() > DisplayRow(0) {
2439 let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2440 return display_point;
2441 };
2442 start = excerpt.start_anchor().to_display_point(&map);
2443 }
2444 start
2445 }
2446 (Direction::Prev, false) => {
2447 let mut start = excerpt.start_anchor().to_display_point(&map);
2448 if start.row() > DisplayRow(0) {
2449 *start.row_mut() -= 1;
2450 }
2451 map.clip_point(start, Bias::Left)
2452 }
2453 (Direction::Next, true) => {
2454 let mut end = excerpt.end_anchor().to_display_point(&map);
2455 *end.row_mut() += 1;
2456 map.clip_point(end, Bias::Right)
2457 }
2458 (Direction::Next, false) => {
2459 let mut end = excerpt.end_anchor().to_display_point(&map);
2460 *end.column_mut() = 0;
2461 if end <= display_point {
2462 *end.row_mut() += 1;
2463 let point_end = map.display_point_to_point(end, Bias::Right);
2464 let Some(excerpt) =
2465 map.buffer_snapshot.excerpt_containing(point_end..point_end)
2466 else {
2467 return display_point;
2468 };
2469 end = excerpt.end_anchor().to_display_point(&map);
2470 *end.column_mut() = 0;
2471 }
2472 end
2473 }
2474 };
2475 if next_point == display_point {
2476 break;
2477 }
2478 display_point = next_point;
2479 }
2480
2481 display_point
2482}
2483
2484#[cfg(test)]
2485mod test {
2486
2487 use crate::test::NeovimBackedTestContext;
2488 use indoc::indoc;
2489
2490 #[gpui::test]
2491 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2492 let mut cx = NeovimBackedTestContext::new(cx).await;
2493
2494 let initial_state = indoc! {r"ˇabc
2495 def
2496
2497 paragraph
2498 the second
2499
2500
2501
2502 third and
2503 final"};
2504
2505 // goes down once
2506 cx.set_shared_state(initial_state).await;
2507 cx.simulate_shared_keystrokes("}").await;
2508 cx.shared_state().await.assert_eq(indoc! {r"abc
2509 def
2510 ˇ
2511 paragraph
2512 the second
2513
2514
2515
2516 third and
2517 final"});
2518
2519 // goes up once
2520 cx.simulate_shared_keystrokes("{").await;
2521 cx.shared_state().await.assert_eq(initial_state);
2522
2523 // goes down twice
2524 cx.simulate_shared_keystrokes("2 }").await;
2525 cx.shared_state().await.assert_eq(indoc! {r"abc
2526 def
2527
2528 paragraph
2529 the second
2530 ˇ
2531
2532
2533 third and
2534 final"});
2535
2536 // goes down over multiple blanks
2537 cx.simulate_shared_keystrokes("}").await;
2538 cx.shared_state().await.assert_eq(indoc! {r"abc
2539 def
2540
2541 paragraph
2542 the second
2543
2544
2545
2546 third and
2547 finaˇl"});
2548
2549 // goes up twice
2550 cx.simulate_shared_keystrokes("2 {").await;
2551 cx.shared_state().await.assert_eq(indoc! {r"abc
2552 def
2553 ˇ
2554 paragraph
2555 the second
2556
2557
2558
2559 third and
2560 final"});
2561 }
2562
2563 #[gpui::test]
2564 async fn test_matching(cx: &mut gpui::TestAppContext) {
2565 let mut cx = NeovimBackedTestContext::new(cx).await;
2566
2567 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2568 do(something(with<Types>.and_arrays[0, 2]))
2569 }"})
2570 .await;
2571 cx.simulate_shared_keystrokes("%").await;
2572 cx.shared_state()
2573 .await
2574 .assert_eq(indoc! {r"func (a stringˇ) {
2575 do(something(with<Types>.and_arrays[0, 2]))
2576 }"});
2577
2578 // test it works on the last character of the line
2579 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2580 do(something(with<Types>.and_arrays[0, 2]))
2581 }"})
2582 .await;
2583 cx.simulate_shared_keystrokes("%").await;
2584 cx.shared_state()
2585 .await
2586 .assert_eq(indoc! {r"func (a string) {
2587 do(something(with<Types>.and_arrays[0, 2]))
2588 ˇ}"});
2589
2590 // test it works on immediate nesting
2591 cx.set_shared_state("ˇ{()}").await;
2592 cx.simulate_shared_keystrokes("%").await;
2593 cx.shared_state().await.assert_eq("{()ˇ}");
2594 cx.simulate_shared_keystrokes("%").await;
2595 cx.shared_state().await.assert_eq("ˇ{()}");
2596
2597 // test it works on immediate nesting inside braces
2598 cx.set_shared_state("{\n ˇ{()}\n}").await;
2599 cx.simulate_shared_keystrokes("%").await;
2600 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
2601
2602 // test it jumps to the next paren on a line
2603 cx.set_shared_state("func ˇboop() {\n}").await;
2604 cx.simulate_shared_keystrokes("%").await;
2605 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2606 }
2607
2608 #[gpui::test]
2609 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2610 let mut cx = NeovimBackedTestContext::new(cx).await;
2611
2612 // test it works with curly braces
2613 cx.set_shared_state(indoc! {r"func (a string) {
2614 do(something(with<Types>.anˇd_arrays[0, 2]))
2615 }"})
2616 .await;
2617 cx.simulate_shared_keystrokes("] }").await;
2618 cx.shared_state()
2619 .await
2620 .assert_eq(indoc! {r"func (a string) {
2621 do(something(with<Types>.and_arrays[0, 2]))
2622 ˇ}"});
2623
2624 // test it works with brackets
2625 cx.set_shared_state(indoc! {r"func (a string) {
2626 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2627 }"})
2628 .await;
2629 cx.simulate_shared_keystrokes("] )").await;
2630 cx.shared_state()
2631 .await
2632 .assert_eq(indoc! {r"func (a string) {
2633 do(something(with<Types>.and_arrays[0, 2])ˇ)
2634 }"});
2635
2636 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2637 .await;
2638 cx.simulate_shared_keystrokes("] )").await;
2639 cx.shared_state()
2640 .await
2641 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2642
2643 // test it works on immediate nesting
2644 cx.set_shared_state("{ˇ {}{}}").await;
2645 cx.simulate_shared_keystrokes("] }").await;
2646 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2647 cx.set_shared_state("(ˇ ()())").await;
2648 cx.simulate_shared_keystrokes("] )").await;
2649 cx.shared_state().await.assert_eq("( ()()ˇ)");
2650
2651 // test it works on immediate nesting inside braces
2652 cx.set_shared_state("{\n ˇ {()}\n}").await;
2653 cx.simulate_shared_keystrokes("] }").await;
2654 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
2655 cx.set_shared_state("(\n ˇ {()}\n)").await;
2656 cx.simulate_shared_keystrokes("] )").await;
2657 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
2658 }
2659
2660 #[gpui::test]
2661 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2662 let mut cx = NeovimBackedTestContext::new(cx).await;
2663
2664 // test it works with curly braces
2665 cx.set_shared_state(indoc! {r"func (a string) {
2666 do(something(with<Types>.anˇd_arrays[0, 2]))
2667 }"})
2668 .await;
2669 cx.simulate_shared_keystrokes("[ {").await;
2670 cx.shared_state()
2671 .await
2672 .assert_eq(indoc! {r"func (a string) ˇ{
2673 do(something(with<Types>.and_arrays[0, 2]))
2674 }"});
2675
2676 // test it works with brackets
2677 cx.set_shared_state(indoc! {r"func (a string) {
2678 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2679 }"})
2680 .await;
2681 cx.simulate_shared_keystrokes("[ (").await;
2682 cx.shared_state()
2683 .await
2684 .assert_eq(indoc! {r"func (a string) {
2685 doˇ(something(with<Types>.and_arrays[0, 2]))
2686 }"});
2687
2688 // test it works on immediate nesting
2689 cx.set_shared_state("{{}{} ˇ }").await;
2690 cx.simulate_shared_keystrokes("[ {").await;
2691 cx.shared_state().await.assert_eq("ˇ{{}{} }");
2692 cx.set_shared_state("(()() ˇ )").await;
2693 cx.simulate_shared_keystrokes("[ (").await;
2694 cx.shared_state().await.assert_eq("ˇ(()() )");
2695
2696 // test it works on immediate nesting inside braces
2697 cx.set_shared_state("{\n {()} ˇ\n}").await;
2698 cx.simulate_shared_keystrokes("[ {").await;
2699 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
2700 cx.set_shared_state("(\n {()} ˇ\n)").await;
2701 cx.simulate_shared_keystrokes("[ (").await;
2702 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
2703 }
2704
2705 #[gpui::test]
2706 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2707 let mut cx = NeovimBackedTestContext::new_html(cx).await;
2708
2709 cx.neovim.exec("set filetype=html").await;
2710
2711 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2712 cx.simulate_shared_keystrokes("%").await;
2713 cx.shared_state()
2714 .await
2715 .assert_eq(indoc! {r"<body><ˇ/body>"});
2716 cx.simulate_shared_keystrokes("%").await;
2717
2718 // test jumping backwards
2719 cx.shared_state()
2720 .await
2721 .assert_eq(indoc! {r"<ˇbody></body>"});
2722
2723 // test self-closing tags
2724 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2725 cx.simulate_shared_keystrokes("%").await;
2726 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2727
2728 // test tag with attributes
2729 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2730 </div>
2731 "})
2732 .await;
2733 cx.simulate_shared_keystrokes("%").await;
2734 cx.shared_state()
2735 .await
2736 .assert_eq(indoc! {r"<div class='test' id='main'>
2737 <ˇ/div>
2738 "});
2739
2740 // test multi-line self-closing tag
2741 cx.set_shared_state(indoc! {r#"<a>
2742 <br
2743 test = "test"
2744 /ˇ>
2745 </a>"#})
2746 .await;
2747 cx.simulate_shared_keystrokes("%").await;
2748 cx.shared_state().await.assert_eq(indoc! {r#"<a>
2749 ˇ<br
2750 test = "test"
2751 />
2752 </a>"#});
2753 }
2754
2755 #[gpui::test]
2756 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2757 let mut cx = NeovimBackedTestContext::new(cx).await;
2758
2759 // f and F
2760 cx.set_shared_state("ˇone two three four").await;
2761 cx.simulate_shared_keystrokes("f o").await;
2762 cx.shared_state().await.assert_eq("one twˇo three four");
2763 cx.simulate_shared_keystrokes(",").await;
2764 cx.shared_state().await.assert_eq("ˇone two three four");
2765 cx.simulate_shared_keystrokes("2 ;").await;
2766 cx.shared_state().await.assert_eq("one two three fˇour");
2767 cx.simulate_shared_keystrokes("shift-f e").await;
2768 cx.shared_state().await.assert_eq("one two threˇe four");
2769 cx.simulate_shared_keystrokes("2 ;").await;
2770 cx.shared_state().await.assert_eq("onˇe two three four");
2771 cx.simulate_shared_keystrokes(",").await;
2772 cx.shared_state().await.assert_eq("one two thrˇee four");
2773
2774 // t and T
2775 cx.set_shared_state("ˇone two three four").await;
2776 cx.simulate_shared_keystrokes("t o").await;
2777 cx.shared_state().await.assert_eq("one tˇwo three four");
2778 cx.simulate_shared_keystrokes(",").await;
2779 cx.shared_state().await.assert_eq("oˇne two three four");
2780 cx.simulate_shared_keystrokes("2 ;").await;
2781 cx.shared_state().await.assert_eq("one two three ˇfour");
2782 cx.simulate_shared_keystrokes("shift-t e").await;
2783 cx.shared_state().await.assert_eq("one two threeˇ four");
2784 cx.simulate_shared_keystrokes("3 ;").await;
2785 cx.shared_state().await.assert_eq("oneˇ two three four");
2786 cx.simulate_shared_keystrokes(",").await;
2787 cx.shared_state().await.assert_eq("one two thˇree four");
2788 }
2789
2790 #[gpui::test]
2791 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2792 let mut cx = NeovimBackedTestContext::new(cx).await;
2793 let initial_state = indoc! {r"something(ˇfoo)"};
2794 cx.set_shared_state(initial_state).await;
2795 cx.simulate_shared_keystrokes("}").await;
2796 cx.shared_state().await.assert_eq("something(fooˇ)");
2797 }
2798
2799 #[gpui::test]
2800 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2801 let mut cx = NeovimBackedTestContext::new(cx).await;
2802 cx.set_shared_state("ˇone\n two\nthree").await;
2803 cx.simulate_shared_keystrokes("enter").await;
2804 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
2805 }
2806
2807 #[gpui::test]
2808 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2809 let mut cx = NeovimBackedTestContext::new(cx).await;
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
2814 cx.set_shared_state("ˇ one \n two \nthree").await;
2815 cx.simulate_shared_keystrokes("g _").await;
2816 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
2817 cx.simulate_shared_keystrokes("2 g _").await;
2818 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2819 }
2820
2821 #[gpui::test]
2822 async fn test_window_top(cx: &mut gpui::TestAppContext) {
2823 let mut cx = NeovimBackedTestContext::new(cx).await;
2824 let initial_state = indoc! {r"abc
2825 def
2826 paragraph
2827 the second
2828 third ˇand
2829 final"};
2830
2831 cx.set_shared_state(initial_state).await;
2832 cx.simulate_shared_keystrokes("shift-h").await;
2833 cx.shared_state().await.assert_eq(indoc! {r"abˇc
2834 def
2835 paragraph
2836 the second
2837 third and
2838 final"});
2839
2840 // clip point
2841 cx.set_shared_state(indoc! {r"
2842 1 2 3
2843 4 5 6
2844 7 8 ˇ9
2845 "})
2846 .await;
2847 cx.simulate_shared_keystrokes("shift-h").await;
2848 cx.shared_state().await.assert_eq(indoc! {"
2849 1 2 ˇ3
2850 4 5 6
2851 7 8 9
2852 "});
2853
2854 cx.set_shared_state(indoc! {r"
2855 1 2 3
2856 4 5 6
2857 ˇ7 8 9
2858 "})
2859 .await;
2860 cx.simulate_shared_keystrokes("shift-h").await;
2861 cx.shared_state().await.assert_eq(indoc! {"
2862 ˇ1 2 3
2863 4 5 6
2864 7 8 9
2865 "});
2866
2867 cx.set_shared_state(indoc! {r"
2868 1 2 3
2869 4 5 ˇ6
2870 7 8 9"})
2871 .await;
2872 cx.simulate_shared_keystrokes("9 shift-h").await;
2873 cx.shared_state().await.assert_eq(indoc! {"
2874 1 2 3
2875 4 5 6
2876 7 8 ˇ9"});
2877 }
2878
2879 #[gpui::test]
2880 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2881 let mut cx = NeovimBackedTestContext::new(cx).await;
2882 let initial_state = indoc! {r"abˇc
2883 def
2884 paragraph
2885 the second
2886 third and
2887 final"};
2888
2889 cx.set_shared_state(initial_state).await;
2890 cx.simulate_shared_keystrokes("shift-m").await;
2891 cx.shared_state().await.assert_eq(indoc! {r"abc
2892 def
2893 paˇragraph
2894 the second
2895 third and
2896 final"});
2897
2898 cx.set_shared_state(indoc! {r"
2899 1 2 3
2900 4 5 6
2901 7 8 ˇ9
2902 "})
2903 .await;
2904 cx.simulate_shared_keystrokes("shift-m").await;
2905 cx.shared_state().await.assert_eq(indoc! {"
2906 1 2 3
2907 4 5 ˇ6
2908 7 8 9
2909 "});
2910 cx.set_shared_state(indoc! {r"
2911 1 2 3
2912 4 5 6
2913 ˇ7 8 9
2914 "})
2915 .await;
2916 cx.simulate_shared_keystrokes("shift-m").await;
2917 cx.shared_state().await.assert_eq(indoc! {"
2918 1 2 3
2919 ˇ4 5 6
2920 7 8 9
2921 "});
2922 cx.set_shared_state(indoc! {r"
2923 ˇ1 2 3
2924 4 5 6
2925 7 8 9
2926 "})
2927 .await;
2928 cx.simulate_shared_keystrokes("shift-m").await;
2929 cx.shared_state().await.assert_eq(indoc! {"
2930 1 2 3
2931 ˇ4 5 6
2932 7 8 9
2933 "});
2934 cx.set_shared_state(indoc! {r"
2935 1 2 3
2936 ˇ4 5 6
2937 7 8 9
2938 "})
2939 .await;
2940 cx.simulate_shared_keystrokes("shift-m").await;
2941 cx.shared_state().await.assert_eq(indoc! {"
2942 1 2 3
2943 ˇ4 5 6
2944 7 8 9
2945 "});
2946 cx.set_shared_state(indoc! {r"
2947 1 2 3
2948 4 5 ˇ6
2949 7 8 9
2950 "})
2951 .await;
2952 cx.simulate_shared_keystrokes("shift-m").await;
2953 cx.shared_state().await.assert_eq(indoc! {"
2954 1 2 3
2955 4 5 ˇ6
2956 7 8 9
2957 "});
2958 }
2959
2960 #[gpui::test]
2961 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2962 let mut cx = NeovimBackedTestContext::new(cx).await;
2963 let initial_state = indoc! {r"abc
2964 deˇf
2965 paragraph
2966 the second
2967 third and
2968 final"};
2969
2970 cx.set_shared_state(initial_state).await;
2971 cx.simulate_shared_keystrokes("shift-l").await;
2972 cx.shared_state().await.assert_eq(indoc! {r"abc
2973 def
2974 paragraph
2975 the second
2976 third and
2977 fiˇnal"});
2978
2979 cx.set_shared_state(indoc! {r"
2980 1 2 3
2981 4 5 ˇ6
2982 7 8 9
2983 "})
2984 .await;
2985 cx.simulate_shared_keystrokes("shift-l").await;
2986 cx.shared_state().await.assert_eq(indoc! {"
2987 1 2 3
2988 4 5 6
2989 7 8 9
2990 ˇ"});
2991
2992 cx.set_shared_state(indoc! {r"
2993 1 2 3
2994 ˇ4 5 6
2995 7 8 9
2996 "})
2997 .await;
2998 cx.simulate_shared_keystrokes("shift-l").await;
2999 cx.shared_state().await.assert_eq(indoc! {"
3000 1 2 3
3001 4 5 6
3002 7 8 9
3003 ˇ"});
3004
3005 cx.set_shared_state(indoc! {r"
3006 1 2 ˇ3
3007 4 5 6
3008 7 8 9
3009 "})
3010 .await;
3011 cx.simulate_shared_keystrokes("shift-l").await;
3012 cx.shared_state().await.assert_eq(indoc! {"
3013 1 2 3
3014 4 5 6
3015 7 8 9
3016 ˇ"});
3017
3018 cx.set_shared_state(indoc! {r"
3019 ˇ1 2 3
3020 4 5 6
3021 7 8 9
3022 "})
3023 .await;
3024 cx.simulate_shared_keystrokes("shift-l").await;
3025 cx.shared_state().await.assert_eq(indoc! {"
3026 1 2 3
3027 4 5 6
3028 7 8 9
3029 ˇ"});
3030
3031 cx.set_shared_state(indoc! {r"
3032 1 2 3
3033 4 5 ˇ6
3034 7 8 9
3035 "})
3036 .await;
3037 cx.simulate_shared_keystrokes("9 shift-l").await;
3038 cx.shared_state().await.assert_eq(indoc! {"
3039 1 2 ˇ3
3040 4 5 6
3041 7 8 9
3042 "});
3043 }
3044
3045 #[gpui::test]
3046 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3047 let mut cx = NeovimBackedTestContext::new(cx).await;
3048 cx.set_shared_state(indoc! {r"
3049 456 5ˇ67 678
3050 "})
3051 .await;
3052 cx.simulate_shared_keystrokes("g e").await;
3053 cx.shared_state().await.assert_eq(indoc! {"
3054 45ˇ6 567 678
3055 "});
3056
3057 // Test times
3058 cx.set_shared_state(indoc! {r"
3059 123 234 345
3060 456 5ˇ67 678
3061 "})
3062 .await;
3063 cx.simulate_shared_keystrokes("4 g e").await;
3064 cx.shared_state().await.assert_eq(indoc! {"
3065 12ˇ3 234 345
3066 456 567 678
3067 "});
3068
3069 // With punctuation
3070 cx.set_shared_state(indoc! {r"
3071 123 234 345
3072 4;5.6 5ˇ67 678
3073 789 890 901
3074 "})
3075 .await;
3076 cx.simulate_shared_keystrokes("g e").await;
3077 cx.shared_state().await.assert_eq(indoc! {"
3078 123 234 345
3079 4;5.ˇ6 567 678
3080 789 890 901
3081 "});
3082
3083 // With punctuation and count
3084 cx.set_shared_state(indoc! {r"
3085 123 234 345
3086 4;5.6 5ˇ67 678
3087 789 890 901
3088 "})
3089 .await;
3090 cx.simulate_shared_keystrokes("5 g e").await;
3091 cx.shared_state().await.assert_eq(indoc! {"
3092 123 234 345
3093 ˇ4;5.6 567 678
3094 789 890 901
3095 "});
3096
3097 // newlines
3098 cx.set_shared_state(indoc! {r"
3099 123 234 345
3100
3101 78ˇ9 890 901
3102 "})
3103 .await;
3104 cx.simulate_shared_keystrokes("g e").await;
3105 cx.shared_state().await.assert_eq(indoc! {"
3106 123 234 345
3107 ˇ
3108 789 890 901
3109 "});
3110 cx.simulate_shared_keystrokes("g e").await;
3111 cx.shared_state().await.assert_eq(indoc! {"
3112 123 234 34ˇ5
3113
3114 789 890 901
3115 "});
3116
3117 // With punctuation
3118 cx.set_shared_state(indoc! {r"
3119 123 234 345
3120 4;5.ˇ6 567 678
3121 789 890 901
3122 "})
3123 .await;
3124 cx.simulate_shared_keystrokes("g shift-e").await;
3125 cx.shared_state().await.assert_eq(indoc! {"
3126 123 234 34ˇ5
3127 4;5.6 567 678
3128 789 890 901
3129 "});
3130 }
3131
3132 #[gpui::test]
3133 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3134 let mut cx = NeovimBackedTestContext::new(cx).await;
3135
3136 cx.set_shared_state(indoc! {"
3137 fn aˇ() {
3138 return
3139 }
3140 "})
3141 .await;
3142 cx.simulate_shared_keystrokes("v $ %").await;
3143 cx.shared_state().await.assert_eq(indoc! {"
3144 fn a«() {
3145 return
3146 }ˇ»
3147 "});
3148 }
3149}