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