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 if point == new_point {
1657 break;
1658 }
1659 point = new_point;
1660 }
1661 point
1662}
1663
1664fn previous_subword_start(
1665 map: &DisplaySnapshot,
1666 mut point: DisplayPoint,
1667 ignore_punctuation: bool,
1668 times: usize,
1669) -> DisplayPoint {
1670 let classifier = map
1671 .buffer_snapshot
1672 .char_classifier_at(point.to_point(map))
1673 .ignore_punctuation(ignore_punctuation);
1674 for _ in 0..times {
1675 let mut crossed_newline = false;
1676 // This works even though find_preceding_boundary is called for every character in the line containing
1677 // cursor because the newline is checked only once.
1678 let new_point = movement::find_preceding_boundary_display_point(
1679 map,
1680 point,
1681 FindRange::MultiLine,
1682 |left, right| {
1683 let left_kind = classifier.kind(left);
1684 let right_kind = classifier.kind(right);
1685 let at_newline = right == '\n';
1686
1687 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1688 let is_subword_start =
1689 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1690
1691 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1692 || at_newline && crossed_newline
1693 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1694
1695 crossed_newline |= at_newline;
1696
1697 found
1698 },
1699 );
1700 if point == new_point {
1701 break;
1702 }
1703 point = new_point;
1704 }
1705 point
1706}
1707
1708fn previous_subword_end(
1709 map: &DisplaySnapshot,
1710 point: DisplayPoint,
1711 ignore_punctuation: bool,
1712 times: usize,
1713) -> DisplayPoint {
1714 let classifier = map
1715 .buffer_snapshot
1716 .char_classifier_at(point.to_point(map))
1717 .ignore_punctuation(ignore_punctuation);
1718 let mut point = point.to_point(map);
1719
1720 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1721 point.column += 1;
1722 }
1723 for _ in 0..times {
1724 let new_point = movement::find_preceding_boundary_point(
1725 &map.buffer_snapshot,
1726 point,
1727 FindRange::MultiLine,
1728 |left, right| {
1729 let left_kind = classifier.kind(left);
1730 let right_kind = classifier.kind(right);
1731
1732 let is_subword_end =
1733 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1734
1735 if is_subword_end {
1736 return true;
1737 }
1738
1739 match (left_kind, right_kind) {
1740 (CharKind::Word, CharKind::Whitespace)
1741 | (CharKind::Word, CharKind::Punctuation) => true,
1742 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1743 _ => false,
1744 }
1745 },
1746 );
1747 if new_point == point {
1748 break;
1749 }
1750 point = new_point;
1751 }
1752 movement::saturating_left(map, point.to_display_point(map))
1753}
1754
1755pub(crate) fn first_non_whitespace(
1756 map: &DisplaySnapshot,
1757 display_lines: bool,
1758 from: DisplayPoint,
1759) -> DisplayPoint {
1760 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1761 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1762 for (ch, offset) in map.buffer_chars_at(start_offset) {
1763 if ch == '\n' {
1764 return from;
1765 }
1766
1767 start_offset = offset;
1768
1769 if classifier.kind(ch) != CharKind::Whitespace {
1770 break;
1771 }
1772 }
1773
1774 start_offset.to_display_point(map)
1775}
1776
1777pub(crate) fn last_non_whitespace(
1778 map: &DisplaySnapshot,
1779 from: DisplayPoint,
1780 count: usize,
1781) -> DisplayPoint {
1782 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1783 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1784
1785 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1786 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1787 if classifier.kind(ch) != CharKind::Whitespace {
1788 return end_of_line.to_display_point(map);
1789 }
1790 }
1791
1792 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1793 if ch == '\n' {
1794 break;
1795 }
1796 end_of_line = offset;
1797 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1798 break;
1799 }
1800 }
1801
1802 end_of_line.to_display_point(map)
1803}
1804
1805pub(crate) fn start_of_line(
1806 map: &DisplaySnapshot,
1807 display_lines: bool,
1808 point: DisplayPoint,
1809) -> DisplayPoint {
1810 if display_lines {
1811 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1812 } else {
1813 map.prev_line_boundary(point.to_point(map)).1
1814 }
1815}
1816
1817pub(crate) fn end_of_line(
1818 map: &DisplaySnapshot,
1819 display_lines: bool,
1820 mut point: DisplayPoint,
1821 times: usize,
1822) -> DisplayPoint {
1823 if times > 1 {
1824 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1825 }
1826 if display_lines {
1827 map.clip_point(
1828 DisplayPoint::new(point.row(), map.line_len(point.row())),
1829 Bias::Left,
1830 )
1831 } else {
1832 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1833 }
1834}
1835
1836fn sentence_backwards(
1837 map: &DisplaySnapshot,
1838 point: DisplayPoint,
1839 mut times: usize,
1840) -> DisplayPoint {
1841 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1842 let mut chars = map.reverse_buffer_chars_at(start).peekable();
1843
1844 let mut was_newline = map
1845 .buffer_chars_at(start)
1846 .next()
1847 .is_some_and(|(c, _)| c == '\n');
1848
1849 while let Some((ch, offset)) = chars.next() {
1850 let start_of_next_sentence = if was_newline && ch == '\n' {
1851 Some(offset + ch.len_utf8())
1852 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1853 Some(next_non_blank(map, offset + ch.len_utf8()))
1854 } else if ch == '.' || ch == '?' || ch == '!' {
1855 start_of_next_sentence(map, offset + ch.len_utf8())
1856 } else {
1857 None
1858 };
1859
1860 if let Some(start_of_next_sentence) = start_of_next_sentence {
1861 if start_of_next_sentence < start {
1862 times = times.saturating_sub(1);
1863 }
1864 if times == 0 || offset == 0 {
1865 return map.clip_point(
1866 start_of_next_sentence
1867 .to_offset(&map.buffer_snapshot)
1868 .to_display_point(map),
1869 Bias::Left,
1870 );
1871 }
1872 }
1873 if was_newline {
1874 start = offset;
1875 }
1876 was_newline = ch == '\n';
1877 }
1878
1879 DisplayPoint::zero()
1880}
1881
1882fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1883 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1884 let mut chars = map.buffer_chars_at(start).peekable();
1885
1886 let mut was_newline = map
1887 .reverse_buffer_chars_at(start)
1888 .next()
1889 .is_some_and(|(c, _)| c == '\n')
1890 && chars.peek().is_some_and(|(c, _)| *c == '\n');
1891
1892 while let Some((ch, offset)) = chars.next() {
1893 if was_newline && ch == '\n' {
1894 continue;
1895 }
1896 let start_of_next_sentence = if was_newline {
1897 Some(next_non_blank(map, offset))
1898 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1899 Some(next_non_blank(map, offset + ch.len_utf8()))
1900 } else if ch == '.' || ch == '?' || ch == '!' {
1901 start_of_next_sentence(map, offset + ch.len_utf8())
1902 } else {
1903 None
1904 };
1905
1906 if let Some(start_of_next_sentence) = start_of_next_sentence {
1907 times = times.saturating_sub(1);
1908 if times == 0 {
1909 return map.clip_point(
1910 start_of_next_sentence
1911 .to_offset(&map.buffer_snapshot)
1912 .to_display_point(map),
1913 Bias::Right,
1914 );
1915 }
1916 }
1917
1918 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1919 }
1920
1921 map.max_point()
1922}
1923
1924fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1925 for (c, o) in map.buffer_chars_at(start) {
1926 if c == '\n' || !c.is_whitespace() {
1927 return o;
1928 }
1929 }
1930
1931 map.buffer_snapshot.len()
1932}
1933
1934// given the offset after a ., !, or ? find the start of the next sentence.
1935// if this is not a sentence boundary, returns None.
1936fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1937 let chars = map.buffer_chars_at(end_of_sentence);
1938 let mut seen_space = false;
1939
1940 for (char, offset) in chars {
1941 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1942 continue;
1943 }
1944
1945 if char == '\n' && seen_space {
1946 return Some(offset);
1947 } else if char.is_whitespace() {
1948 seen_space = true;
1949 } else if seen_space {
1950 return Some(offset);
1951 } else {
1952 return None;
1953 }
1954 }
1955
1956 Some(map.buffer_snapshot.len())
1957}
1958
1959fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1960 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1961 *new_point.column_mut() = point.column();
1962 map.clip_point(new_point, Bias::Left)
1963}
1964
1965fn end_of_document(
1966 map: &DisplaySnapshot,
1967 point: DisplayPoint,
1968 line: Option<usize>,
1969) -> DisplayPoint {
1970 let new_row = if let Some(line) = line {
1971 (line - 1) as u32
1972 } else {
1973 map.buffer_snapshot.max_row().0
1974 };
1975
1976 let new_point = Point::new(new_row, point.column());
1977 map.clip_point(new_point.to_display_point(map), Bias::Left)
1978}
1979
1980fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
1981 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
1982 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
1983
1984 if head > outer.start && head < inner.start {
1985 let mut offset = inner.end.to_offset(map, Bias::Left);
1986 for c in map.buffer_snapshot.chars_at(offset) {
1987 if c == '/' || c == '\n' || c == '>' {
1988 return Some(offset.to_display_point(map));
1989 }
1990 offset += c.len_utf8();
1991 }
1992 } else {
1993 let mut offset = outer.start.to_offset(map, Bias::Left);
1994 for c in map.buffer_snapshot.chars_at(offset) {
1995 offset += c.len_utf8();
1996 if c == '<' || c == '\n' {
1997 return Some(offset.to_display_point(map));
1998 }
1999 }
2000 }
2001
2002 return None;
2003}
2004
2005fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2006 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2007 let display_point = map.clip_at_line_end(display_point);
2008 let point = display_point.to_point(map);
2009 let offset = point.to_offset(&map.buffer_snapshot);
2010
2011 // Ensure the range is contained by the current line.
2012 let mut line_end = map.next_line_boundary(point).0;
2013 if line_end == point {
2014 line_end = map.max_point().to_point(map);
2015 }
2016
2017 let line_range = map.prev_line_boundary(point).0..line_end;
2018 let visible_line_range =
2019 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2020 let ranges = map
2021 .buffer_snapshot
2022 .bracket_ranges(visible_line_range.clone());
2023 if let Some(ranges) = ranges {
2024 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2025 ..line_range.end.to_offset(&map.buffer_snapshot);
2026 let mut closest_pair_destination = None;
2027 let mut closest_distance = usize::MAX;
2028
2029 for (open_range, close_range) in ranges {
2030 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2031 if offset > open_range.start && offset < close_range.start {
2032 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2033 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2034 return display_point;
2035 }
2036 if let Some(tag) = matching_tag(map, display_point) {
2037 return tag;
2038 }
2039 } else if close_range.contains(&offset) {
2040 return open_range.start.to_display_point(map);
2041 } else if open_range.contains(&offset) {
2042 return (close_range.end - 1).to_display_point(map);
2043 }
2044 }
2045
2046 if (open_range.contains(&offset) || open_range.start >= offset)
2047 && line_range.contains(&open_range.start)
2048 {
2049 let distance = open_range.start.saturating_sub(offset);
2050 if distance < closest_distance {
2051 closest_pair_destination = Some(close_range.start);
2052 closest_distance = distance;
2053 continue;
2054 }
2055 }
2056
2057 if (close_range.contains(&offset) || close_range.start >= offset)
2058 && line_range.contains(&close_range.start)
2059 {
2060 let distance = close_range.start.saturating_sub(offset);
2061 if distance < closest_distance {
2062 closest_pair_destination = Some(open_range.start);
2063 closest_distance = distance;
2064 continue;
2065 }
2066 }
2067
2068 continue;
2069 }
2070
2071 closest_pair_destination
2072 .map(|destination| destination.to_display_point(map))
2073 .unwrap_or(display_point)
2074 } else {
2075 display_point
2076 }
2077}
2078
2079fn unmatched_forward(
2080 map: &DisplaySnapshot,
2081 mut display_point: DisplayPoint,
2082 char: char,
2083 times: usize,
2084) -> DisplayPoint {
2085 for _ in 0..times {
2086 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2087 let point = display_point.to_point(map);
2088 let offset = point.to_offset(&map.buffer_snapshot);
2089
2090 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2091 let Some(ranges) = ranges else { break };
2092 let mut closest_closing_destination = None;
2093 let mut closest_distance = usize::MAX;
2094
2095 for (_, close_range) in ranges {
2096 if close_range.start > offset {
2097 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2098 if Some(char) == chars.next() {
2099 let distance = close_range.start - offset;
2100 if distance < closest_distance {
2101 closest_closing_destination = Some(close_range.start);
2102 closest_distance = distance;
2103 continue;
2104 }
2105 }
2106 }
2107 }
2108
2109 let new_point = closest_closing_destination
2110 .map(|destination| destination.to_display_point(map))
2111 .unwrap_or(display_point);
2112 if new_point == display_point {
2113 break;
2114 }
2115 display_point = new_point;
2116 }
2117 return display_point;
2118}
2119
2120fn unmatched_backward(
2121 map: &DisplaySnapshot,
2122 mut display_point: DisplayPoint,
2123 char: char,
2124 times: usize,
2125) -> DisplayPoint {
2126 for _ in 0..times {
2127 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2128 let point = display_point.to_point(map);
2129 let offset = point.to_offset(&map.buffer_snapshot);
2130
2131 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2132 let Some(ranges) = ranges else {
2133 break;
2134 };
2135
2136 let mut closest_starting_destination = None;
2137 let mut closest_distance = usize::MAX;
2138
2139 for (start_range, _) in ranges {
2140 if start_range.start < offset {
2141 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2142 if Some(char) == chars.next() {
2143 let distance = offset - start_range.start;
2144 if distance < closest_distance {
2145 closest_starting_destination = Some(start_range.start);
2146 closest_distance = distance;
2147 continue;
2148 }
2149 }
2150 }
2151 }
2152
2153 let new_point = closest_starting_destination
2154 .map(|destination| destination.to_display_point(map))
2155 .unwrap_or(display_point);
2156 if new_point == display_point {
2157 break;
2158 } else {
2159 display_point = new_point;
2160 }
2161 }
2162 display_point
2163}
2164
2165fn find_forward(
2166 map: &DisplaySnapshot,
2167 from: DisplayPoint,
2168 before: bool,
2169 target: char,
2170 times: usize,
2171 mode: FindRange,
2172 smartcase: bool,
2173) -> Option<DisplayPoint> {
2174 let mut to = from;
2175 let mut found = false;
2176
2177 for _ in 0..times {
2178 found = false;
2179 let new_to = find_boundary(map, to, mode, |_, right| {
2180 found = is_character_match(target, right, smartcase);
2181 found
2182 });
2183 if to == new_to {
2184 break;
2185 }
2186 to = new_to;
2187 }
2188
2189 if found {
2190 if before && to.column() > 0 {
2191 *to.column_mut() -= 1;
2192 Some(map.clip_point(to, Bias::Left))
2193 } else {
2194 Some(to)
2195 }
2196 } else {
2197 None
2198 }
2199}
2200
2201fn find_backward(
2202 map: &DisplaySnapshot,
2203 from: DisplayPoint,
2204 after: bool,
2205 target: char,
2206 times: usize,
2207 mode: FindRange,
2208 smartcase: bool,
2209) -> DisplayPoint {
2210 let mut to = from;
2211
2212 for _ in 0..times {
2213 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2214 is_character_match(target, right, smartcase)
2215 });
2216 if to == new_to {
2217 break;
2218 }
2219 to = new_to;
2220 }
2221
2222 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2223 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2224 if after {
2225 *to.column_mut() += 1;
2226 map.clip_point(to, Bias::Right)
2227 } else {
2228 to
2229 }
2230 } else {
2231 from
2232 }
2233}
2234
2235fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2236 if smartcase {
2237 if target.is_uppercase() {
2238 target == other
2239 } else {
2240 target == other.to_ascii_lowercase()
2241 }
2242 } else {
2243 target == other
2244 }
2245}
2246
2247fn sneak(
2248 map: &DisplaySnapshot,
2249 from: DisplayPoint,
2250 first_target: char,
2251 second_target: char,
2252 times: usize,
2253 smartcase: bool,
2254) -> Option<DisplayPoint> {
2255 let mut to = from;
2256 let mut found = false;
2257
2258 for _ in 0..times {
2259 found = false;
2260 let new_to = find_boundary(
2261 map,
2262 movement::right(map, to),
2263 FindRange::MultiLine,
2264 |left, right| {
2265 found = is_character_match(first_target, left, smartcase)
2266 && is_character_match(second_target, right, smartcase);
2267 found
2268 },
2269 );
2270 if to == new_to {
2271 break;
2272 }
2273 to = new_to;
2274 }
2275
2276 if found {
2277 Some(movement::left(map, to))
2278 } else {
2279 None
2280 }
2281}
2282
2283fn sneak_backward(
2284 map: &DisplaySnapshot,
2285 from: DisplayPoint,
2286 first_target: char,
2287 second_target: char,
2288 times: usize,
2289 smartcase: bool,
2290) -> Option<DisplayPoint> {
2291 let mut to = from;
2292 let mut found = false;
2293
2294 for _ in 0..times {
2295 found = false;
2296 let new_to =
2297 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2298 found = is_character_match(first_target, left, smartcase)
2299 && is_character_match(second_target, right, smartcase);
2300 found
2301 });
2302 if to == new_to {
2303 break;
2304 }
2305 to = new_to;
2306 }
2307
2308 if found {
2309 Some(movement::left(map, to))
2310 } else {
2311 None
2312 }
2313}
2314
2315fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2316 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2317 first_non_whitespace(map, false, correct_line)
2318}
2319
2320fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2321 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2322 first_non_whitespace(map, false, correct_line)
2323}
2324
2325fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2326 let correct_line = start_of_relative_buffer_row(map, point, 0);
2327 right(map, correct_line, times.saturating_sub(1))
2328}
2329
2330pub(crate) fn next_line_end(
2331 map: &DisplaySnapshot,
2332 mut point: DisplayPoint,
2333 times: usize,
2334) -> DisplayPoint {
2335 if times > 1 {
2336 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2337 }
2338 end_of_line(map, false, point, 1)
2339}
2340
2341fn window_top(
2342 map: &DisplaySnapshot,
2343 point: DisplayPoint,
2344 text_layout_details: &TextLayoutDetails,
2345 mut times: usize,
2346) -> (DisplayPoint, SelectionGoal) {
2347 let first_visible_line = text_layout_details
2348 .scroll_anchor
2349 .anchor
2350 .to_display_point(map);
2351
2352 if first_visible_line.row() != DisplayRow(0)
2353 && text_layout_details.vertical_scroll_margin as usize > times
2354 {
2355 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2356 }
2357
2358 if let Some(visible_rows) = text_layout_details.visible_rows {
2359 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2360 let new_row = (first_visible_line.row().0 + (times as u32))
2361 .min(bottom_row)
2362 .min(map.max_point().row().0);
2363 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2364
2365 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2366 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2367 } else {
2368 let new_row =
2369 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2370 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2371
2372 let new_point = DisplayPoint::new(new_row, new_col);
2373 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2374 }
2375}
2376
2377fn window_middle(
2378 map: &DisplaySnapshot,
2379 point: DisplayPoint,
2380 text_layout_details: &TextLayoutDetails,
2381) -> (DisplayPoint, SelectionGoal) {
2382 if let Some(visible_rows) = text_layout_details.visible_rows {
2383 let first_visible_line = text_layout_details
2384 .scroll_anchor
2385 .anchor
2386 .to_display_point(map);
2387
2388 let max_visible_rows =
2389 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2390
2391 let new_row =
2392 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2393 let new_row = DisplayRow(new_row);
2394 let new_col = point.column().min(map.line_len(new_row));
2395 let new_point = DisplayPoint::new(new_row, new_col);
2396 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2397 } else {
2398 (point, SelectionGoal::None)
2399 }
2400}
2401
2402fn window_bottom(
2403 map: &DisplaySnapshot,
2404 point: DisplayPoint,
2405 text_layout_details: &TextLayoutDetails,
2406 mut times: usize,
2407) -> (DisplayPoint, SelectionGoal) {
2408 if let Some(visible_rows) = text_layout_details.visible_rows {
2409 let first_visible_line = text_layout_details
2410 .scroll_anchor
2411 .anchor
2412 .to_display_point(map);
2413 let bottom_row = first_visible_line.row().0
2414 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2415 if bottom_row < map.max_point().row().0
2416 && text_layout_details.vertical_scroll_margin as usize > times
2417 {
2418 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2419 }
2420 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2421 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2422 {
2423 first_visible_line.row()
2424 } else {
2425 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2426 };
2427 let new_col = point.column().min(map.line_len(new_row));
2428 let new_point = DisplayPoint::new(new_row, new_col);
2429 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2430 } else {
2431 (point, SelectionGoal::None)
2432 }
2433}
2434
2435fn method_motion(
2436 map: &DisplaySnapshot,
2437 mut display_point: DisplayPoint,
2438 times: usize,
2439 direction: Direction,
2440 is_start: bool,
2441) -> DisplayPoint {
2442 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2443 return display_point;
2444 };
2445
2446 for _ in 0..times {
2447 let point = map.display_point_to_point(display_point, Bias::Left);
2448 let offset = point.to_offset(&map.buffer_snapshot);
2449 let range = if direction == Direction::Prev {
2450 0..offset
2451 } else {
2452 offset..buffer.len()
2453 };
2454
2455 let possibilities = buffer
2456 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2457 .filter_map(|(range, object)| {
2458 if !matches!(object, language::TextObject::AroundFunction) {
2459 return None;
2460 }
2461
2462 let relevant = if is_start { range.start } else { range.end };
2463 if direction == Direction::Prev && relevant < offset {
2464 Some(relevant)
2465 } else if direction == Direction::Next && relevant > offset + 1 {
2466 Some(relevant)
2467 } else {
2468 None
2469 }
2470 });
2471
2472 let dest = if direction == Direction::Prev {
2473 possibilities.max().unwrap_or(offset)
2474 } else {
2475 possibilities.min().unwrap_or(offset)
2476 };
2477 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2478 if new_point == display_point {
2479 break;
2480 }
2481 display_point = new_point;
2482 }
2483 display_point
2484}
2485
2486fn comment_motion(
2487 map: &DisplaySnapshot,
2488 mut display_point: DisplayPoint,
2489 times: usize,
2490 direction: Direction,
2491) -> DisplayPoint {
2492 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2493 return display_point;
2494 };
2495
2496 for _ in 0..times {
2497 let point = map.display_point_to_point(display_point, Bias::Left);
2498 let offset = point.to_offset(&map.buffer_snapshot);
2499 let range = if direction == Direction::Prev {
2500 0..offset
2501 } else {
2502 offset..buffer.len()
2503 };
2504
2505 let possibilities = buffer
2506 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2507 .filter_map(|(range, object)| {
2508 if !matches!(object, language::TextObject::AroundComment) {
2509 return None;
2510 }
2511
2512 let relevant = if direction == Direction::Prev {
2513 range.start
2514 } else {
2515 range.end
2516 };
2517 if direction == Direction::Prev && relevant < offset {
2518 Some(relevant)
2519 } else if direction == Direction::Next && relevant > offset + 1 {
2520 Some(relevant)
2521 } else {
2522 None
2523 }
2524 });
2525
2526 let dest = if direction == Direction::Prev {
2527 possibilities.max().unwrap_or(offset)
2528 } else {
2529 possibilities.min().unwrap_or(offset)
2530 };
2531 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2532 if new_point == display_point {
2533 break;
2534 }
2535 display_point = new_point;
2536 }
2537
2538 display_point
2539}
2540
2541fn section_motion(
2542 map: &DisplaySnapshot,
2543 mut display_point: DisplayPoint,
2544 times: usize,
2545 direction: Direction,
2546 is_start: bool,
2547) -> DisplayPoint {
2548 if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
2549 for _ in 0..times {
2550 let offset = map
2551 .display_point_to_point(display_point, Bias::Left)
2552 .to_offset(&map.buffer_snapshot);
2553 let range = if direction == Direction::Prev {
2554 0..offset
2555 } else {
2556 offset..buffer.len()
2557 };
2558
2559 // we set a max start depth here because we want a section to only be "top level"
2560 // similar to vim's default of '{' in the first column.
2561 // (and without it, ]] at the start of editor.rs is -very- slow)
2562 let mut possibilities = buffer
2563 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2564 .filter(|(_, object)| {
2565 matches!(
2566 object,
2567 language::TextObject::AroundClass | language::TextObject::AroundFunction
2568 )
2569 })
2570 .collect::<Vec<_>>();
2571 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2572 let mut prev_end = None;
2573 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2574 if t == language::TextObject::AroundFunction
2575 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2576 {
2577 return None;
2578 }
2579 prev_end = Some(range.end);
2580
2581 let relevant = if is_start { range.start } else { range.end };
2582 if direction == Direction::Prev && relevant < offset {
2583 Some(relevant)
2584 } else if direction == Direction::Next && relevant > offset + 1 {
2585 Some(relevant)
2586 } else {
2587 None
2588 }
2589 });
2590
2591 let offset = if direction == Direction::Prev {
2592 possibilities.max().unwrap_or(0)
2593 } else {
2594 possibilities.min().unwrap_or(buffer.len())
2595 };
2596
2597 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2598 if new_point == display_point {
2599 break;
2600 }
2601 display_point = new_point;
2602 }
2603 return display_point;
2604 };
2605
2606 for _ in 0..times {
2607 let point = map.display_point_to_point(display_point, Bias::Left);
2608 let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2609 return display_point;
2610 };
2611 let next_point = match (direction, is_start) {
2612 (Direction::Prev, true) => {
2613 let mut start = excerpt.start_anchor().to_display_point(&map);
2614 if start >= display_point && start.row() > DisplayRow(0) {
2615 let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2616 return display_point;
2617 };
2618 start = excerpt.start_anchor().to_display_point(&map);
2619 }
2620 start
2621 }
2622 (Direction::Prev, false) => {
2623 let mut start = excerpt.start_anchor().to_display_point(&map);
2624 if start.row() > DisplayRow(0) {
2625 *start.row_mut() -= 1;
2626 }
2627 map.clip_point(start, Bias::Left)
2628 }
2629 (Direction::Next, true) => {
2630 let mut end = excerpt.end_anchor().to_display_point(&map);
2631 *end.row_mut() += 1;
2632 map.clip_point(end, Bias::Right)
2633 }
2634 (Direction::Next, false) => {
2635 let mut end = excerpt.end_anchor().to_display_point(&map);
2636 *end.column_mut() = 0;
2637 if end <= display_point {
2638 *end.row_mut() += 1;
2639 let point_end = map.display_point_to_point(end, Bias::Right);
2640 let Some(excerpt) =
2641 map.buffer_snapshot.excerpt_containing(point_end..point_end)
2642 else {
2643 return display_point;
2644 };
2645 end = excerpt.end_anchor().to_display_point(&map);
2646 *end.column_mut() = 0;
2647 }
2648 end
2649 }
2650 };
2651 if next_point == display_point {
2652 break;
2653 }
2654 display_point = next_point;
2655 }
2656
2657 display_point
2658}
2659
2660#[cfg(test)]
2661mod test {
2662
2663 use crate::{
2664 state::Mode,
2665 test::{NeovimBackedTestContext, VimTestContext},
2666 };
2667 use editor::display_map::Inlay;
2668 use indoc::indoc;
2669
2670 #[gpui::test]
2671 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2672 let mut cx = NeovimBackedTestContext::new(cx).await;
2673
2674 let initial_state = indoc! {r"ˇabc
2675 def
2676
2677 paragraph
2678 the second
2679
2680
2681
2682 third and
2683 final"};
2684
2685 // goes down once
2686 cx.set_shared_state(initial_state).await;
2687 cx.simulate_shared_keystrokes("}").await;
2688 cx.shared_state().await.assert_eq(indoc! {r"abc
2689 def
2690 ˇ
2691 paragraph
2692 the second
2693
2694
2695
2696 third and
2697 final"});
2698
2699 // goes up once
2700 cx.simulate_shared_keystrokes("{").await;
2701 cx.shared_state().await.assert_eq(initial_state);
2702
2703 // goes down twice
2704 cx.simulate_shared_keystrokes("2 }").await;
2705 cx.shared_state().await.assert_eq(indoc! {r"abc
2706 def
2707
2708 paragraph
2709 the second
2710 ˇ
2711
2712
2713 third and
2714 final"});
2715
2716 // goes down over multiple blanks
2717 cx.simulate_shared_keystrokes("}").await;
2718 cx.shared_state().await.assert_eq(indoc! {r"abc
2719 def
2720
2721 paragraph
2722 the second
2723
2724
2725
2726 third and
2727 finaˇl"});
2728
2729 // goes up twice
2730 cx.simulate_shared_keystrokes("2 {").await;
2731 cx.shared_state().await.assert_eq(indoc! {r"abc
2732 def
2733 ˇ
2734 paragraph
2735 the second
2736
2737
2738
2739 third and
2740 final"});
2741 }
2742
2743 #[gpui::test]
2744 async fn test_matching(cx: &mut gpui::TestAppContext) {
2745 let mut cx = NeovimBackedTestContext::new(cx).await;
2746
2747 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2748 do(something(with<Types>.and_arrays[0, 2]))
2749 }"})
2750 .await;
2751 cx.simulate_shared_keystrokes("%").await;
2752 cx.shared_state()
2753 .await
2754 .assert_eq(indoc! {r"func (a stringˇ) {
2755 do(something(with<Types>.and_arrays[0, 2]))
2756 }"});
2757
2758 // test it works on the last character of the line
2759 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2760 do(something(with<Types>.and_arrays[0, 2]))
2761 }"})
2762 .await;
2763 cx.simulate_shared_keystrokes("%").await;
2764 cx.shared_state()
2765 .await
2766 .assert_eq(indoc! {r"func (a string) {
2767 do(something(with<Types>.and_arrays[0, 2]))
2768 ˇ}"});
2769
2770 // test it works on immediate nesting
2771 cx.set_shared_state("ˇ{()}").await;
2772 cx.simulate_shared_keystrokes("%").await;
2773 cx.shared_state().await.assert_eq("{()ˇ}");
2774 cx.simulate_shared_keystrokes("%").await;
2775 cx.shared_state().await.assert_eq("ˇ{()}");
2776
2777 // test it works on immediate nesting inside braces
2778 cx.set_shared_state("{\n ˇ{()}\n}").await;
2779 cx.simulate_shared_keystrokes("%").await;
2780 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
2781
2782 // test it jumps to the next paren on a line
2783 cx.set_shared_state("func ˇboop() {\n}").await;
2784 cx.simulate_shared_keystrokes("%").await;
2785 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2786 }
2787
2788 #[gpui::test]
2789 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2790 let mut cx = NeovimBackedTestContext::new(cx).await;
2791
2792 // test it works with curly braces
2793 cx.set_shared_state(indoc! {r"func (a string) {
2794 do(something(with<Types>.anˇd_arrays[0, 2]))
2795 }"})
2796 .await;
2797 cx.simulate_shared_keystrokes("] }").await;
2798 cx.shared_state()
2799 .await
2800 .assert_eq(indoc! {r"func (a string) {
2801 do(something(with<Types>.and_arrays[0, 2]))
2802 ˇ}"});
2803
2804 // test it works with brackets
2805 cx.set_shared_state(indoc! {r"func (a string) {
2806 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2807 }"})
2808 .await;
2809 cx.simulate_shared_keystrokes("] )").await;
2810 cx.shared_state()
2811 .await
2812 .assert_eq(indoc! {r"func (a string) {
2813 do(something(with<Types>.and_arrays[0, 2])ˇ)
2814 }"});
2815
2816 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2817 .await;
2818 cx.simulate_shared_keystrokes("] )").await;
2819 cx.shared_state()
2820 .await
2821 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2822
2823 // test it works on immediate nesting
2824 cx.set_shared_state("{ˇ {}{}}").await;
2825 cx.simulate_shared_keystrokes("] }").await;
2826 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2827 cx.set_shared_state("(ˇ ()())").await;
2828 cx.simulate_shared_keystrokes("] )").await;
2829 cx.shared_state().await.assert_eq("( ()()ˇ)");
2830
2831 // test it works on immediate nesting inside braces
2832 cx.set_shared_state("{\n ˇ {()}\n}").await;
2833 cx.simulate_shared_keystrokes("] }").await;
2834 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
2835 cx.set_shared_state("(\n ˇ {()}\n)").await;
2836 cx.simulate_shared_keystrokes("] )").await;
2837 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
2838 }
2839
2840 #[gpui::test]
2841 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2842 let mut cx = NeovimBackedTestContext::new(cx).await;
2843
2844 // test it works with curly braces
2845 cx.set_shared_state(indoc! {r"func (a string) {
2846 do(something(with<Types>.anˇd_arrays[0, 2]))
2847 }"})
2848 .await;
2849 cx.simulate_shared_keystrokes("[ {").await;
2850 cx.shared_state()
2851 .await
2852 .assert_eq(indoc! {r"func (a string) ˇ{
2853 do(something(with<Types>.and_arrays[0, 2]))
2854 }"});
2855
2856 // test it works with brackets
2857 cx.set_shared_state(indoc! {r"func (a string) {
2858 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2859 }"})
2860 .await;
2861 cx.simulate_shared_keystrokes("[ (").await;
2862 cx.shared_state()
2863 .await
2864 .assert_eq(indoc! {r"func (a string) {
2865 doˇ(something(with<Types>.and_arrays[0, 2]))
2866 }"});
2867
2868 // test it works on immediate nesting
2869 cx.set_shared_state("{{}{} ˇ }").await;
2870 cx.simulate_shared_keystrokes("[ {").await;
2871 cx.shared_state().await.assert_eq("ˇ{{}{} }");
2872 cx.set_shared_state("(()() ˇ )").await;
2873 cx.simulate_shared_keystrokes("[ (").await;
2874 cx.shared_state().await.assert_eq("ˇ(()() )");
2875
2876 // test it works on immediate nesting inside braces
2877 cx.set_shared_state("{\n {()} ˇ\n}").await;
2878 cx.simulate_shared_keystrokes("[ {").await;
2879 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
2880 cx.set_shared_state("(\n {()} ˇ\n)").await;
2881 cx.simulate_shared_keystrokes("[ (").await;
2882 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
2883 }
2884
2885 #[gpui::test]
2886 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2887 let mut cx = NeovimBackedTestContext::new_html(cx).await;
2888
2889 cx.neovim.exec("set filetype=html").await;
2890
2891 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2892 cx.simulate_shared_keystrokes("%").await;
2893 cx.shared_state()
2894 .await
2895 .assert_eq(indoc! {r"<body><ˇ/body>"});
2896 cx.simulate_shared_keystrokes("%").await;
2897
2898 // test jumping backwards
2899 cx.shared_state()
2900 .await
2901 .assert_eq(indoc! {r"<ˇbody></body>"});
2902
2903 // test self-closing tags
2904 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2905 cx.simulate_shared_keystrokes("%").await;
2906 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2907
2908 // test tag with attributes
2909 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2910 </div>
2911 "})
2912 .await;
2913 cx.simulate_shared_keystrokes("%").await;
2914 cx.shared_state()
2915 .await
2916 .assert_eq(indoc! {r"<div class='test' id='main'>
2917 <ˇ/div>
2918 "});
2919
2920 // test multi-line self-closing tag
2921 cx.set_shared_state(indoc! {r#"<a>
2922 <br
2923 test = "test"
2924 /ˇ>
2925 </a>"#})
2926 .await;
2927 cx.simulate_shared_keystrokes("%").await;
2928 cx.shared_state().await.assert_eq(indoc! {r#"<a>
2929 ˇ<br
2930 test = "test"
2931 />
2932 </a>"#});
2933 }
2934
2935 #[gpui::test]
2936 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2937 let mut cx = NeovimBackedTestContext::new(cx).await;
2938
2939 // f and F
2940 cx.set_shared_state("ˇone two three four").await;
2941 cx.simulate_shared_keystrokes("f o").await;
2942 cx.shared_state().await.assert_eq("one twˇo three four");
2943 cx.simulate_shared_keystrokes(",").await;
2944 cx.shared_state().await.assert_eq("ˇone two three four");
2945 cx.simulate_shared_keystrokes("2 ;").await;
2946 cx.shared_state().await.assert_eq("one two three fˇour");
2947 cx.simulate_shared_keystrokes("shift-f e").await;
2948 cx.shared_state().await.assert_eq("one two threˇe four");
2949 cx.simulate_shared_keystrokes("2 ;").await;
2950 cx.shared_state().await.assert_eq("onˇe two three four");
2951 cx.simulate_shared_keystrokes(",").await;
2952 cx.shared_state().await.assert_eq("one two thrˇee four");
2953
2954 // t and T
2955 cx.set_shared_state("ˇone two three four").await;
2956 cx.simulate_shared_keystrokes("t o").await;
2957 cx.shared_state().await.assert_eq("one tˇwo three four");
2958 cx.simulate_shared_keystrokes(",").await;
2959 cx.shared_state().await.assert_eq("oˇne two three four");
2960 cx.simulate_shared_keystrokes("2 ;").await;
2961 cx.shared_state().await.assert_eq("one two three ˇfour");
2962 cx.simulate_shared_keystrokes("shift-t e").await;
2963 cx.shared_state().await.assert_eq("one two threeˇ four");
2964 cx.simulate_shared_keystrokes("3 ;").await;
2965 cx.shared_state().await.assert_eq("oneˇ two three four");
2966 cx.simulate_shared_keystrokes(",").await;
2967 cx.shared_state().await.assert_eq("one two thˇree four");
2968 }
2969
2970 #[gpui::test]
2971 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2972 let mut cx = NeovimBackedTestContext::new(cx).await;
2973 let initial_state = indoc! {r"something(ˇfoo)"};
2974 cx.set_shared_state(initial_state).await;
2975 cx.simulate_shared_keystrokes("}").await;
2976 cx.shared_state().await.assert_eq("something(fooˇ)");
2977 }
2978
2979 #[gpui::test]
2980 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2981 let mut cx = NeovimBackedTestContext::new(cx).await;
2982 cx.set_shared_state("ˇone\n two\nthree").await;
2983 cx.simulate_shared_keystrokes("enter").await;
2984 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
2985 }
2986
2987 #[gpui::test]
2988 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2989 let mut cx = NeovimBackedTestContext::new(cx).await;
2990 cx.set_shared_state("ˇ one\n two \nthree").await;
2991 cx.simulate_shared_keystrokes("g _").await;
2992 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
2993
2994 cx.set_shared_state("ˇ one \n two \nthree").await;
2995 cx.simulate_shared_keystrokes("g _").await;
2996 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
2997 cx.simulate_shared_keystrokes("2 g _").await;
2998 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2999 }
3000
3001 #[gpui::test]
3002 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3003 let mut cx = NeovimBackedTestContext::new(cx).await;
3004 let initial_state = indoc! {r"abc
3005 def
3006 paragraph
3007 the second
3008 third ˇand
3009 final"};
3010
3011 cx.set_shared_state(initial_state).await;
3012 cx.simulate_shared_keystrokes("shift-h").await;
3013 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3014 def
3015 paragraph
3016 the second
3017 third and
3018 final"});
3019
3020 // clip point
3021 cx.set_shared_state(indoc! {r"
3022 1 2 3
3023 4 5 6
3024 7 8 ˇ9
3025 "})
3026 .await;
3027 cx.simulate_shared_keystrokes("shift-h").await;
3028 cx.shared_state().await.assert_eq(indoc! {"
3029 1 2 ˇ3
3030 4 5 6
3031 7 8 9
3032 "});
3033
3034 cx.set_shared_state(indoc! {r"
3035 1 2 3
3036 4 5 6
3037 ˇ7 8 9
3038 "})
3039 .await;
3040 cx.simulate_shared_keystrokes("shift-h").await;
3041 cx.shared_state().await.assert_eq(indoc! {"
3042 ˇ1 2 3
3043 4 5 6
3044 7 8 9
3045 "});
3046
3047 cx.set_shared_state(indoc! {r"
3048 1 2 3
3049 4 5 ˇ6
3050 7 8 9"})
3051 .await;
3052 cx.simulate_shared_keystrokes("9 shift-h").await;
3053 cx.shared_state().await.assert_eq(indoc! {"
3054 1 2 3
3055 4 5 6
3056 7 8 ˇ9"});
3057 }
3058
3059 #[gpui::test]
3060 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3061 let mut cx = NeovimBackedTestContext::new(cx).await;
3062 let initial_state = indoc! {r"abˇc
3063 def
3064 paragraph
3065 the second
3066 third and
3067 final"};
3068
3069 cx.set_shared_state(initial_state).await;
3070 cx.simulate_shared_keystrokes("shift-m").await;
3071 cx.shared_state().await.assert_eq(indoc! {r"abc
3072 def
3073 paˇragraph
3074 the second
3075 third and
3076 final"});
3077
3078 cx.set_shared_state(indoc! {r"
3079 1 2 3
3080 4 5 6
3081 7 8 ˇ9
3082 "})
3083 .await;
3084 cx.simulate_shared_keystrokes("shift-m").await;
3085 cx.shared_state().await.assert_eq(indoc! {"
3086 1 2 3
3087 4 5 ˇ6
3088 7 8 9
3089 "});
3090 cx.set_shared_state(indoc! {r"
3091 1 2 3
3092 4 5 6
3093 ˇ7 8 9
3094 "})
3095 .await;
3096 cx.simulate_shared_keystrokes("shift-m").await;
3097 cx.shared_state().await.assert_eq(indoc! {"
3098 1 2 3
3099 ˇ4 5 6
3100 7 8 9
3101 "});
3102 cx.set_shared_state(indoc! {r"
3103 ˇ1 2 3
3104 4 5 6
3105 7 8 9
3106 "})
3107 .await;
3108 cx.simulate_shared_keystrokes("shift-m").await;
3109 cx.shared_state().await.assert_eq(indoc! {"
3110 1 2 3
3111 ˇ4 5 6
3112 7 8 9
3113 "});
3114 cx.set_shared_state(indoc! {r"
3115 1 2 3
3116 ˇ4 5 6
3117 7 8 9
3118 "})
3119 .await;
3120 cx.simulate_shared_keystrokes("shift-m").await;
3121 cx.shared_state().await.assert_eq(indoc! {"
3122 1 2 3
3123 ˇ4 5 6
3124 7 8 9
3125 "});
3126 cx.set_shared_state(indoc! {r"
3127 1 2 3
3128 4 5 ˇ6
3129 7 8 9
3130 "})
3131 .await;
3132 cx.simulate_shared_keystrokes("shift-m").await;
3133 cx.shared_state().await.assert_eq(indoc! {"
3134 1 2 3
3135 4 5 ˇ6
3136 7 8 9
3137 "});
3138 }
3139
3140 #[gpui::test]
3141 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3142 let mut cx = NeovimBackedTestContext::new(cx).await;
3143 let initial_state = indoc! {r"abc
3144 deˇf
3145 paragraph
3146 the second
3147 third and
3148 final"};
3149
3150 cx.set_shared_state(initial_state).await;
3151 cx.simulate_shared_keystrokes("shift-l").await;
3152 cx.shared_state().await.assert_eq(indoc! {r"abc
3153 def
3154 paragraph
3155 the second
3156 third and
3157 fiˇnal"});
3158
3159 cx.set_shared_state(indoc! {r"
3160 1 2 3
3161 4 5 ˇ6
3162 7 8 9
3163 "})
3164 .await;
3165 cx.simulate_shared_keystrokes("shift-l").await;
3166 cx.shared_state().await.assert_eq(indoc! {"
3167 1 2 3
3168 4 5 6
3169 7 8 9
3170 ˇ"});
3171
3172 cx.set_shared_state(indoc! {r"
3173 1 2 3
3174 ˇ4 5 6
3175 7 8 9
3176 "})
3177 .await;
3178 cx.simulate_shared_keystrokes("shift-l").await;
3179 cx.shared_state().await.assert_eq(indoc! {"
3180 1 2 3
3181 4 5 6
3182 7 8 9
3183 ˇ"});
3184
3185 cx.set_shared_state(indoc! {r"
3186 1 2 ˇ3
3187 4 5 6
3188 7 8 9
3189 "})
3190 .await;
3191 cx.simulate_shared_keystrokes("shift-l").await;
3192 cx.shared_state().await.assert_eq(indoc! {"
3193 1 2 3
3194 4 5 6
3195 7 8 9
3196 ˇ"});
3197
3198 cx.set_shared_state(indoc! {r"
3199 ˇ1 2 3
3200 4 5 6
3201 7 8 9
3202 "})
3203 .await;
3204 cx.simulate_shared_keystrokes("shift-l").await;
3205 cx.shared_state().await.assert_eq(indoc! {"
3206 1 2 3
3207 4 5 6
3208 7 8 9
3209 ˇ"});
3210
3211 cx.set_shared_state(indoc! {r"
3212 1 2 3
3213 4 5 ˇ6
3214 7 8 9
3215 "})
3216 .await;
3217 cx.simulate_shared_keystrokes("9 shift-l").await;
3218 cx.shared_state().await.assert_eq(indoc! {"
3219 1 2 ˇ3
3220 4 5 6
3221 7 8 9
3222 "});
3223 }
3224
3225 #[gpui::test]
3226 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3227 let mut cx = NeovimBackedTestContext::new(cx).await;
3228 cx.set_shared_state(indoc! {r"
3229 456 5ˇ67 678
3230 "})
3231 .await;
3232 cx.simulate_shared_keystrokes("g e").await;
3233 cx.shared_state().await.assert_eq(indoc! {"
3234 45ˇ6 567 678
3235 "});
3236
3237 // Test times
3238 cx.set_shared_state(indoc! {r"
3239 123 234 345
3240 456 5ˇ67 678
3241 "})
3242 .await;
3243 cx.simulate_shared_keystrokes("4 g e").await;
3244 cx.shared_state().await.assert_eq(indoc! {"
3245 12ˇ3 234 345
3246 456 567 678
3247 "});
3248
3249 // With punctuation
3250 cx.set_shared_state(indoc! {r"
3251 123 234 345
3252 4;5.6 5ˇ67 678
3253 789 890 901
3254 "})
3255 .await;
3256 cx.simulate_shared_keystrokes("g e").await;
3257 cx.shared_state().await.assert_eq(indoc! {"
3258 123 234 345
3259 4;5.ˇ6 567 678
3260 789 890 901
3261 "});
3262
3263 // With punctuation and count
3264 cx.set_shared_state(indoc! {r"
3265 123 234 345
3266 4;5.6 5ˇ67 678
3267 789 890 901
3268 "})
3269 .await;
3270 cx.simulate_shared_keystrokes("5 g e").await;
3271 cx.shared_state().await.assert_eq(indoc! {"
3272 123 234 345
3273 ˇ4;5.6 567 678
3274 789 890 901
3275 "});
3276
3277 // newlines
3278 cx.set_shared_state(indoc! {r"
3279 123 234 345
3280
3281 78ˇ9 890 901
3282 "})
3283 .await;
3284 cx.simulate_shared_keystrokes("g e").await;
3285 cx.shared_state().await.assert_eq(indoc! {"
3286 123 234 345
3287 ˇ
3288 789 890 901
3289 "});
3290 cx.simulate_shared_keystrokes("g e").await;
3291 cx.shared_state().await.assert_eq(indoc! {"
3292 123 234 34ˇ5
3293
3294 789 890 901
3295 "});
3296
3297 // With punctuation
3298 cx.set_shared_state(indoc! {r"
3299 123 234 345
3300 4;5.ˇ6 567 678
3301 789 890 901
3302 "})
3303 .await;
3304 cx.simulate_shared_keystrokes("g shift-e").await;
3305 cx.shared_state().await.assert_eq(indoc! {"
3306 123 234 34ˇ5
3307 4;5.6 567 678
3308 789 890 901
3309 "});
3310 }
3311
3312 #[gpui::test]
3313 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3314 let mut cx = NeovimBackedTestContext::new(cx).await;
3315
3316 cx.set_shared_state(indoc! {"
3317 fn aˇ() {
3318 return
3319 }
3320 "})
3321 .await;
3322 cx.simulate_shared_keystrokes("v $ %").await;
3323 cx.shared_state().await.assert_eq(indoc! {"
3324 fn a«() {
3325 return
3326 }ˇ»
3327 "});
3328 }
3329
3330 #[gpui::test]
3331 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3332 let mut cx = VimTestContext::new(cx, true).await;
3333
3334 cx.set_state(
3335 indoc! {"
3336 struct Foo {
3337 ˇ
3338 }
3339 "},
3340 Mode::Normal,
3341 );
3342
3343 cx.update_editor(|editor, cx| {
3344 let range = editor.selections.newest_anchor().range();
3345 let inlay_text = " field: int,\n field2: string\n field3: float";
3346 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3347 editor.splice_inlays(vec![], vec![inlay], cx);
3348 });
3349
3350 cx.simulate_keystrokes("j");
3351 cx.assert_state(
3352 indoc! {"
3353 struct Foo {
3354
3355 ˇ}
3356 "},
3357 Mode::Normal,
3358 );
3359 }
3360}