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