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.start >= offset && line_range.contains(&open_range.start) {
2047 let distance = open_range.start - offset;
2048 if distance < closest_distance {
2049 closest_pair_destination = Some(close_range.end - 1);
2050 closest_distance = distance;
2051 continue;
2052 }
2053 }
2054
2055 if close_range.start >= offset && line_range.contains(&close_range.start) {
2056 let distance = close_range.start - offset;
2057 if distance < closest_distance {
2058 closest_pair_destination = Some(open_range.start);
2059 closest_distance = distance;
2060 continue;
2061 }
2062 }
2063
2064 continue;
2065 }
2066
2067 closest_pair_destination
2068 .map(|destination| destination.to_display_point(map))
2069 .unwrap_or(display_point)
2070 } else {
2071 display_point
2072 }
2073}
2074
2075fn unmatched_forward(
2076 map: &DisplaySnapshot,
2077 mut display_point: DisplayPoint,
2078 char: char,
2079 times: usize,
2080) -> DisplayPoint {
2081 for _ in 0..times {
2082 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2083 let point = display_point.to_point(map);
2084 let offset = point.to_offset(&map.buffer_snapshot);
2085
2086 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2087 let Some(ranges) = ranges else { break };
2088 let mut closest_closing_destination = None;
2089 let mut closest_distance = usize::MAX;
2090
2091 for (_, close_range) in ranges {
2092 if close_range.start > offset {
2093 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2094 if Some(char) == chars.next() {
2095 let distance = close_range.start - offset;
2096 if distance < closest_distance {
2097 closest_closing_destination = Some(close_range.start);
2098 closest_distance = distance;
2099 continue;
2100 }
2101 }
2102 }
2103 }
2104
2105 let new_point = closest_closing_destination
2106 .map(|destination| destination.to_display_point(map))
2107 .unwrap_or(display_point);
2108 if new_point == display_point {
2109 break;
2110 }
2111 display_point = new_point;
2112 }
2113 return display_point;
2114}
2115
2116fn unmatched_backward(
2117 map: &DisplaySnapshot,
2118 mut display_point: DisplayPoint,
2119 char: char,
2120 times: usize,
2121) -> DisplayPoint {
2122 for _ in 0..times {
2123 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2124 let point = display_point.to_point(map);
2125 let offset = point.to_offset(&map.buffer_snapshot);
2126
2127 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2128 let Some(ranges) = ranges else {
2129 break;
2130 };
2131
2132 let mut closest_starting_destination = None;
2133 let mut closest_distance = usize::MAX;
2134
2135 for (start_range, _) in ranges {
2136 if start_range.start < offset {
2137 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2138 if Some(char) == chars.next() {
2139 let distance = offset - start_range.start;
2140 if distance < closest_distance {
2141 closest_starting_destination = Some(start_range.start);
2142 closest_distance = distance;
2143 continue;
2144 }
2145 }
2146 }
2147 }
2148
2149 let new_point = closest_starting_destination
2150 .map(|destination| destination.to_display_point(map))
2151 .unwrap_or(display_point);
2152 if new_point == display_point {
2153 break;
2154 } else {
2155 display_point = new_point;
2156 }
2157 }
2158 display_point
2159}
2160
2161fn find_forward(
2162 map: &DisplaySnapshot,
2163 from: DisplayPoint,
2164 before: bool,
2165 target: char,
2166 times: usize,
2167 mode: FindRange,
2168 smartcase: bool,
2169) -> Option<DisplayPoint> {
2170 let mut to = from;
2171 let mut found = false;
2172
2173 for _ in 0..times {
2174 found = false;
2175 let new_to = find_boundary(map, to, mode, |_, right| {
2176 found = is_character_match(target, right, smartcase);
2177 found
2178 });
2179 if to == new_to {
2180 break;
2181 }
2182 to = new_to;
2183 }
2184
2185 if found {
2186 if before && to.column() > 0 {
2187 *to.column_mut() -= 1;
2188 Some(map.clip_point(to, Bias::Left))
2189 } else {
2190 Some(to)
2191 }
2192 } else {
2193 None
2194 }
2195}
2196
2197fn find_backward(
2198 map: &DisplaySnapshot,
2199 from: DisplayPoint,
2200 after: bool,
2201 target: char,
2202 times: usize,
2203 mode: FindRange,
2204 smartcase: bool,
2205) -> DisplayPoint {
2206 let mut to = from;
2207
2208 for _ in 0..times {
2209 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2210 is_character_match(target, right, smartcase)
2211 });
2212 if to == new_to {
2213 break;
2214 }
2215 to = new_to;
2216 }
2217
2218 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2219 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2220 if after {
2221 *to.column_mut() += 1;
2222 map.clip_point(to, Bias::Right)
2223 } else {
2224 to
2225 }
2226 } else {
2227 from
2228 }
2229}
2230
2231fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2232 if smartcase {
2233 if target.is_uppercase() {
2234 target == other
2235 } else {
2236 target == other.to_ascii_lowercase()
2237 }
2238 } else {
2239 target == other
2240 }
2241}
2242
2243fn sneak(
2244 map: &DisplaySnapshot,
2245 from: DisplayPoint,
2246 first_target: char,
2247 second_target: char,
2248 times: usize,
2249 smartcase: bool,
2250) -> Option<DisplayPoint> {
2251 let mut to = from;
2252 let mut found = false;
2253
2254 for _ in 0..times {
2255 found = false;
2256 let new_to = find_boundary(
2257 map,
2258 movement::right(map, to),
2259 FindRange::MultiLine,
2260 |left, right| {
2261 found = is_character_match(first_target, left, smartcase)
2262 && is_character_match(second_target, right, smartcase);
2263 found
2264 },
2265 );
2266 if to == new_to {
2267 break;
2268 }
2269 to = new_to;
2270 }
2271
2272 if found {
2273 Some(movement::left(map, to))
2274 } else {
2275 None
2276 }
2277}
2278
2279fn sneak_backward(
2280 map: &DisplaySnapshot,
2281 from: DisplayPoint,
2282 first_target: char,
2283 second_target: char,
2284 times: usize,
2285 smartcase: bool,
2286) -> Option<DisplayPoint> {
2287 let mut to = from;
2288 let mut found = false;
2289
2290 for _ in 0..times {
2291 found = false;
2292 let new_to =
2293 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2294 found = is_character_match(first_target, left, smartcase)
2295 && is_character_match(second_target, right, smartcase);
2296 found
2297 });
2298 if to == new_to {
2299 break;
2300 }
2301 to = new_to;
2302 }
2303
2304 if found {
2305 Some(movement::left(map, to))
2306 } else {
2307 None
2308 }
2309}
2310
2311fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2312 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2313 first_non_whitespace(map, false, correct_line)
2314}
2315
2316fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2317 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2318 first_non_whitespace(map, false, correct_line)
2319}
2320
2321fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2322 let correct_line = start_of_relative_buffer_row(map, point, 0);
2323 right(map, correct_line, times.saturating_sub(1))
2324}
2325
2326pub(crate) fn next_line_end(
2327 map: &DisplaySnapshot,
2328 mut point: DisplayPoint,
2329 times: usize,
2330) -> DisplayPoint {
2331 if times > 1 {
2332 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2333 }
2334 end_of_line(map, false, point, 1)
2335}
2336
2337fn window_top(
2338 map: &DisplaySnapshot,
2339 point: DisplayPoint,
2340 text_layout_details: &TextLayoutDetails,
2341 mut times: usize,
2342) -> (DisplayPoint, SelectionGoal) {
2343 let first_visible_line = text_layout_details
2344 .scroll_anchor
2345 .anchor
2346 .to_display_point(map);
2347
2348 if first_visible_line.row() != DisplayRow(0)
2349 && text_layout_details.vertical_scroll_margin as usize > times
2350 {
2351 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2352 }
2353
2354 if let Some(visible_rows) = text_layout_details.visible_rows {
2355 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2356 let new_row = (first_visible_line.row().0 + (times as u32))
2357 .min(bottom_row)
2358 .min(map.max_point().row().0);
2359 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2360
2361 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2362 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2363 } else {
2364 let new_row =
2365 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2366 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2367
2368 let new_point = DisplayPoint::new(new_row, new_col);
2369 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2370 }
2371}
2372
2373fn window_middle(
2374 map: &DisplaySnapshot,
2375 point: DisplayPoint,
2376 text_layout_details: &TextLayoutDetails,
2377) -> (DisplayPoint, SelectionGoal) {
2378 if let Some(visible_rows) = text_layout_details.visible_rows {
2379 let first_visible_line = text_layout_details
2380 .scroll_anchor
2381 .anchor
2382 .to_display_point(map);
2383
2384 let max_visible_rows =
2385 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2386
2387 let new_row =
2388 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2389 let new_row = DisplayRow(new_row);
2390 let new_col = point.column().min(map.line_len(new_row));
2391 let new_point = DisplayPoint::new(new_row, new_col);
2392 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2393 } else {
2394 (point, SelectionGoal::None)
2395 }
2396}
2397
2398fn window_bottom(
2399 map: &DisplaySnapshot,
2400 point: DisplayPoint,
2401 text_layout_details: &TextLayoutDetails,
2402 mut times: usize,
2403) -> (DisplayPoint, SelectionGoal) {
2404 if let Some(visible_rows) = text_layout_details.visible_rows {
2405 let first_visible_line = text_layout_details
2406 .scroll_anchor
2407 .anchor
2408 .to_display_point(map);
2409 let bottom_row = first_visible_line.row().0
2410 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2411 if bottom_row < map.max_point().row().0
2412 && text_layout_details.vertical_scroll_margin as usize > times
2413 {
2414 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2415 }
2416 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2417 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2418 {
2419 first_visible_line.row()
2420 } else {
2421 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2422 };
2423 let new_col = point.column().min(map.line_len(new_row));
2424 let new_point = DisplayPoint::new(new_row, new_col);
2425 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2426 } else {
2427 (point, SelectionGoal::None)
2428 }
2429}
2430
2431fn method_motion(
2432 map: &DisplaySnapshot,
2433 mut display_point: DisplayPoint,
2434 times: usize,
2435 direction: Direction,
2436 is_start: bool,
2437) -> DisplayPoint {
2438 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2439 return display_point;
2440 };
2441
2442 for _ in 0..times {
2443 let point = map.display_point_to_point(display_point, Bias::Left);
2444 let offset = point.to_offset(&map.buffer_snapshot);
2445 let range = if direction == Direction::Prev {
2446 0..offset
2447 } else {
2448 offset..buffer.len()
2449 };
2450
2451 let possibilities = buffer
2452 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2453 .filter_map(|(range, object)| {
2454 if !matches!(object, language::TextObject::AroundFunction) {
2455 return None;
2456 }
2457
2458 let relevant = if is_start { range.start } else { range.end };
2459 if direction == Direction::Prev && relevant < offset {
2460 Some(relevant)
2461 } else if direction == Direction::Next && relevant > offset + 1 {
2462 Some(relevant)
2463 } else {
2464 None
2465 }
2466 });
2467
2468 let dest = if direction == Direction::Prev {
2469 possibilities.max().unwrap_or(offset)
2470 } else {
2471 possibilities.min().unwrap_or(offset)
2472 };
2473 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2474 if new_point == display_point {
2475 break;
2476 }
2477 display_point = new_point;
2478 }
2479 display_point
2480}
2481
2482fn comment_motion(
2483 map: &DisplaySnapshot,
2484 mut display_point: DisplayPoint,
2485 times: usize,
2486 direction: Direction,
2487) -> DisplayPoint {
2488 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2489 return display_point;
2490 };
2491
2492 for _ in 0..times {
2493 let point = map.display_point_to_point(display_point, Bias::Left);
2494 let offset = point.to_offset(&map.buffer_snapshot);
2495 let range = if direction == Direction::Prev {
2496 0..offset
2497 } else {
2498 offset..buffer.len()
2499 };
2500
2501 let possibilities = buffer
2502 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2503 .filter_map(|(range, object)| {
2504 if !matches!(object, language::TextObject::AroundComment) {
2505 return None;
2506 }
2507
2508 let relevant = if direction == Direction::Prev {
2509 range.start
2510 } else {
2511 range.end
2512 };
2513 if direction == Direction::Prev && relevant < offset {
2514 Some(relevant)
2515 } else if direction == Direction::Next && relevant > offset + 1 {
2516 Some(relevant)
2517 } else {
2518 None
2519 }
2520 });
2521
2522 let dest = if direction == Direction::Prev {
2523 possibilities.max().unwrap_or(offset)
2524 } else {
2525 possibilities.min().unwrap_or(offset)
2526 };
2527 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2528 if new_point == display_point {
2529 break;
2530 }
2531 display_point = new_point;
2532 }
2533
2534 display_point
2535}
2536
2537fn section_motion(
2538 map: &DisplaySnapshot,
2539 mut display_point: DisplayPoint,
2540 times: usize,
2541 direction: Direction,
2542 is_start: bool,
2543) -> DisplayPoint {
2544 if let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() {
2545 for _ in 0..times {
2546 let offset = map
2547 .display_point_to_point(display_point, Bias::Left)
2548 .to_offset(&map.buffer_snapshot);
2549 let range = if direction == Direction::Prev {
2550 0..offset
2551 } else {
2552 offset..buffer.len()
2553 };
2554
2555 // we set a max start depth here because we want a section to only be "top level"
2556 // similar to vim's default of '{' in the first column.
2557 // (and without it, ]] at the start of editor.rs is -very- slow)
2558 let mut possibilities = buffer
2559 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2560 .filter(|(_, object)| {
2561 matches!(
2562 object,
2563 language::TextObject::AroundClass | language::TextObject::AroundFunction
2564 )
2565 })
2566 .collect::<Vec<_>>();
2567 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2568 let mut prev_end = None;
2569 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2570 if t == language::TextObject::AroundFunction
2571 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2572 {
2573 return None;
2574 }
2575 prev_end = Some(range.end);
2576
2577 let relevant = if is_start { range.start } else { range.end };
2578 if direction == Direction::Prev && relevant < offset {
2579 Some(relevant)
2580 } else if direction == Direction::Next && relevant > offset + 1 {
2581 Some(relevant)
2582 } else {
2583 None
2584 }
2585 });
2586
2587 let offset = if direction == Direction::Prev {
2588 possibilities.max().unwrap_or(0)
2589 } else {
2590 possibilities.min().unwrap_or(buffer.len())
2591 };
2592
2593 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2594 if new_point == display_point {
2595 break;
2596 }
2597 display_point = new_point;
2598 }
2599 return display_point;
2600 };
2601
2602 for _ in 0..times {
2603 let point = map.display_point_to_point(display_point, Bias::Left);
2604 let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2605 return display_point;
2606 };
2607 let next_point = match (direction, is_start) {
2608 (Direction::Prev, true) => {
2609 let mut start = excerpt.start_anchor().to_display_point(&map);
2610 if start >= display_point && start.row() > DisplayRow(0) {
2611 let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2612 return display_point;
2613 };
2614 start = excerpt.start_anchor().to_display_point(&map);
2615 }
2616 start
2617 }
2618 (Direction::Prev, false) => {
2619 let mut start = excerpt.start_anchor().to_display_point(&map);
2620 if start.row() > DisplayRow(0) {
2621 *start.row_mut() -= 1;
2622 }
2623 map.clip_point(start, Bias::Left)
2624 }
2625 (Direction::Next, true) => {
2626 let mut end = excerpt.end_anchor().to_display_point(&map);
2627 *end.row_mut() += 1;
2628 map.clip_point(end, Bias::Right)
2629 }
2630 (Direction::Next, false) => {
2631 let mut end = excerpt.end_anchor().to_display_point(&map);
2632 *end.column_mut() = 0;
2633 if end <= display_point {
2634 *end.row_mut() += 1;
2635 let point_end = map.display_point_to_point(end, Bias::Right);
2636 let Some(excerpt) =
2637 map.buffer_snapshot.excerpt_containing(point_end..point_end)
2638 else {
2639 return display_point;
2640 };
2641 end = excerpt.end_anchor().to_display_point(&map);
2642 *end.column_mut() = 0;
2643 }
2644 end
2645 }
2646 };
2647 if next_point == display_point {
2648 break;
2649 }
2650 display_point = next_point;
2651 }
2652
2653 display_point
2654}
2655
2656#[cfg(test)]
2657mod test {
2658
2659 use crate::{
2660 state::Mode,
2661 test::{NeovimBackedTestContext, VimTestContext},
2662 };
2663 use editor::display_map::Inlay;
2664 use indoc::indoc;
2665
2666 #[gpui::test]
2667 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2668 let mut cx = NeovimBackedTestContext::new(cx).await;
2669
2670 let initial_state = indoc! {r"ˇabc
2671 def
2672
2673 paragraph
2674 the second
2675
2676
2677
2678 third and
2679 final"};
2680
2681 // goes down once
2682 cx.set_shared_state(initial_state).await;
2683 cx.simulate_shared_keystrokes("}").await;
2684 cx.shared_state().await.assert_eq(indoc! {r"abc
2685 def
2686 ˇ
2687 paragraph
2688 the second
2689
2690
2691
2692 third and
2693 final"});
2694
2695 // goes up once
2696 cx.simulate_shared_keystrokes("{").await;
2697 cx.shared_state().await.assert_eq(initial_state);
2698
2699 // goes down twice
2700 cx.simulate_shared_keystrokes("2 }").await;
2701 cx.shared_state().await.assert_eq(indoc! {r"abc
2702 def
2703
2704 paragraph
2705 the second
2706 ˇ
2707
2708
2709 third and
2710 final"});
2711
2712 // goes down over multiple blanks
2713 cx.simulate_shared_keystrokes("}").await;
2714 cx.shared_state().await.assert_eq(indoc! {r"abc
2715 def
2716
2717 paragraph
2718 the second
2719
2720
2721
2722 third and
2723 finaˇl"});
2724
2725 // goes up twice
2726 cx.simulate_shared_keystrokes("2 {").await;
2727 cx.shared_state().await.assert_eq(indoc! {r"abc
2728 def
2729 ˇ
2730 paragraph
2731 the second
2732
2733
2734
2735 third and
2736 final"});
2737 }
2738
2739 #[gpui::test]
2740 async fn test_matching(cx: &mut gpui::TestAppContext) {
2741 let mut cx = NeovimBackedTestContext::new(cx).await;
2742
2743 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2744 do(something(with<Types>.and_arrays[0, 2]))
2745 }"})
2746 .await;
2747 cx.simulate_shared_keystrokes("%").await;
2748 cx.shared_state()
2749 .await
2750 .assert_eq(indoc! {r"func (a stringˇ) {
2751 do(something(with<Types>.and_arrays[0, 2]))
2752 }"});
2753
2754 // test it works on the last character of the line
2755 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2756 do(something(with<Types>.and_arrays[0, 2]))
2757 }"})
2758 .await;
2759 cx.simulate_shared_keystrokes("%").await;
2760 cx.shared_state()
2761 .await
2762 .assert_eq(indoc! {r"func (a string) {
2763 do(something(with<Types>.and_arrays[0, 2]))
2764 ˇ}"});
2765
2766 // test it works on immediate nesting
2767 cx.set_shared_state("ˇ{()}").await;
2768 cx.simulate_shared_keystrokes("%").await;
2769 cx.shared_state().await.assert_eq("{()ˇ}");
2770 cx.simulate_shared_keystrokes("%").await;
2771 cx.shared_state().await.assert_eq("ˇ{()}");
2772
2773 // test it works on immediate nesting inside braces
2774 cx.set_shared_state("{\n ˇ{()}\n}").await;
2775 cx.simulate_shared_keystrokes("%").await;
2776 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
2777
2778 // test it jumps to the next paren on a line
2779 cx.set_shared_state("func ˇboop() {\n}").await;
2780 cx.simulate_shared_keystrokes("%").await;
2781 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2782 }
2783
2784 #[gpui::test]
2785 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2786 let mut cx = NeovimBackedTestContext::new(cx).await;
2787
2788 // test it works with curly braces
2789 cx.set_shared_state(indoc! {r"func (a string) {
2790 do(something(with<Types>.anˇd_arrays[0, 2]))
2791 }"})
2792 .await;
2793 cx.simulate_shared_keystrokes("] }").await;
2794 cx.shared_state()
2795 .await
2796 .assert_eq(indoc! {r"func (a string) {
2797 do(something(with<Types>.and_arrays[0, 2]))
2798 ˇ}"});
2799
2800 // test it works with brackets
2801 cx.set_shared_state(indoc! {r"func (a string) {
2802 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2803 }"})
2804 .await;
2805 cx.simulate_shared_keystrokes("] )").await;
2806 cx.shared_state()
2807 .await
2808 .assert_eq(indoc! {r"func (a string) {
2809 do(something(with<Types>.and_arrays[0, 2])ˇ)
2810 }"});
2811
2812 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2813 .await;
2814 cx.simulate_shared_keystrokes("] )").await;
2815 cx.shared_state()
2816 .await
2817 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2818
2819 // test it works on immediate nesting
2820 cx.set_shared_state("{ˇ {}{}}").await;
2821 cx.simulate_shared_keystrokes("] }").await;
2822 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2823 cx.set_shared_state("(ˇ ()())").await;
2824 cx.simulate_shared_keystrokes("] )").await;
2825 cx.shared_state().await.assert_eq("( ()()ˇ)");
2826
2827 // test it works on immediate nesting inside braces
2828 cx.set_shared_state("{\n ˇ {()}\n}").await;
2829 cx.simulate_shared_keystrokes("] }").await;
2830 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
2831 cx.set_shared_state("(\n ˇ {()}\n)").await;
2832 cx.simulate_shared_keystrokes("] )").await;
2833 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
2834 }
2835
2836 #[gpui::test]
2837 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2838 let mut cx = NeovimBackedTestContext::new(cx).await;
2839
2840 // test it works with curly braces
2841 cx.set_shared_state(indoc! {r"func (a string) {
2842 do(something(with<Types>.anˇd_arrays[0, 2]))
2843 }"})
2844 .await;
2845 cx.simulate_shared_keystrokes("[ {").await;
2846 cx.shared_state()
2847 .await
2848 .assert_eq(indoc! {r"func (a string) ˇ{
2849 do(something(with<Types>.and_arrays[0, 2]))
2850 }"});
2851
2852 // test it works with brackets
2853 cx.set_shared_state(indoc! {r"func (a string) {
2854 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2855 }"})
2856 .await;
2857 cx.simulate_shared_keystrokes("[ (").await;
2858 cx.shared_state()
2859 .await
2860 .assert_eq(indoc! {r"func (a string) {
2861 doˇ(something(with<Types>.and_arrays[0, 2]))
2862 }"});
2863
2864 // test it works on immediate nesting
2865 cx.set_shared_state("{{}{} ˇ }").await;
2866 cx.simulate_shared_keystrokes("[ {").await;
2867 cx.shared_state().await.assert_eq("ˇ{{}{} }");
2868 cx.set_shared_state("(()() ˇ )").await;
2869 cx.simulate_shared_keystrokes("[ (").await;
2870 cx.shared_state().await.assert_eq("ˇ(()() )");
2871
2872 // test it works on immediate nesting inside braces
2873 cx.set_shared_state("{\n {()} ˇ\n}").await;
2874 cx.simulate_shared_keystrokes("[ {").await;
2875 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
2876 cx.set_shared_state("(\n {()} ˇ\n)").await;
2877 cx.simulate_shared_keystrokes("[ (").await;
2878 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
2879 }
2880
2881 #[gpui::test]
2882 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2883 let mut cx = NeovimBackedTestContext::new_html(cx).await;
2884
2885 cx.neovim.exec("set filetype=html").await;
2886
2887 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2888 cx.simulate_shared_keystrokes("%").await;
2889 cx.shared_state()
2890 .await
2891 .assert_eq(indoc! {r"<body><ˇ/body>"});
2892 cx.simulate_shared_keystrokes("%").await;
2893
2894 // test jumping backwards
2895 cx.shared_state()
2896 .await
2897 .assert_eq(indoc! {r"<ˇbody></body>"});
2898
2899 // test self-closing tags
2900 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2901 cx.simulate_shared_keystrokes("%").await;
2902 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2903
2904 // test tag with attributes
2905 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2906 </div>
2907 "})
2908 .await;
2909 cx.simulate_shared_keystrokes("%").await;
2910 cx.shared_state()
2911 .await
2912 .assert_eq(indoc! {r"<div class='test' id='main'>
2913 <ˇ/div>
2914 "});
2915
2916 // test multi-line self-closing tag
2917 cx.set_shared_state(indoc! {r#"<a>
2918 <br
2919 test = "test"
2920 /ˇ>
2921 </a>"#})
2922 .await;
2923 cx.simulate_shared_keystrokes("%").await;
2924 cx.shared_state().await.assert_eq(indoc! {r#"<a>
2925 ˇ<br
2926 test = "test"
2927 />
2928 </a>"#});
2929 }
2930
2931 #[gpui::test]
2932 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2933 let mut cx = NeovimBackedTestContext::new(cx).await;
2934
2935 // f and F
2936 cx.set_shared_state("ˇone two three four").await;
2937 cx.simulate_shared_keystrokes("f o").await;
2938 cx.shared_state().await.assert_eq("one twˇo three four");
2939 cx.simulate_shared_keystrokes(",").await;
2940 cx.shared_state().await.assert_eq("ˇone two three four");
2941 cx.simulate_shared_keystrokes("2 ;").await;
2942 cx.shared_state().await.assert_eq("one two three fˇour");
2943 cx.simulate_shared_keystrokes("shift-f e").await;
2944 cx.shared_state().await.assert_eq("one two threˇe four");
2945 cx.simulate_shared_keystrokes("2 ;").await;
2946 cx.shared_state().await.assert_eq("onˇe two three four");
2947 cx.simulate_shared_keystrokes(",").await;
2948 cx.shared_state().await.assert_eq("one two thrˇee four");
2949
2950 // t and T
2951 cx.set_shared_state("ˇone two three four").await;
2952 cx.simulate_shared_keystrokes("t o").await;
2953 cx.shared_state().await.assert_eq("one tˇwo three four");
2954 cx.simulate_shared_keystrokes(",").await;
2955 cx.shared_state().await.assert_eq("oˇne two three four");
2956 cx.simulate_shared_keystrokes("2 ;").await;
2957 cx.shared_state().await.assert_eq("one two three ˇfour");
2958 cx.simulate_shared_keystrokes("shift-t e").await;
2959 cx.shared_state().await.assert_eq("one two threeˇ four");
2960 cx.simulate_shared_keystrokes("3 ;").await;
2961 cx.shared_state().await.assert_eq("oneˇ two three four");
2962 cx.simulate_shared_keystrokes(",").await;
2963 cx.shared_state().await.assert_eq("one two thˇree four");
2964 }
2965
2966 #[gpui::test]
2967 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2968 let mut cx = NeovimBackedTestContext::new(cx).await;
2969 let initial_state = indoc! {r"something(ˇfoo)"};
2970 cx.set_shared_state(initial_state).await;
2971 cx.simulate_shared_keystrokes("}").await;
2972 cx.shared_state().await.assert_eq("something(fooˇ)");
2973 }
2974
2975 #[gpui::test]
2976 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2977 let mut cx = NeovimBackedTestContext::new(cx).await;
2978 cx.set_shared_state("ˇone\n two\nthree").await;
2979 cx.simulate_shared_keystrokes("enter").await;
2980 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
2981 }
2982
2983 #[gpui::test]
2984 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2985 let mut cx = NeovimBackedTestContext::new(cx).await;
2986 cx.set_shared_state("ˇ one\n two \nthree").await;
2987 cx.simulate_shared_keystrokes("g _").await;
2988 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
2989
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 cx.simulate_shared_keystrokes("2 g _").await;
2994 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2995 }
2996
2997 #[gpui::test]
2998 async fn test_window_top(cx: &mut gpui::TestAppContext) {
2999 let mut cx = NeovimBackedTestContext::new(cx).await;
3000 let initial_state = indoc! {r"abc
3001 def
3002 paragraph
3003 the second
3004 third ˇand
3005 final"};
3006
3007 cx.set_shared_state(initial_state).await;
3008 cx.simulate_shared_keystrokes("shift-h").await;
3009 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3010 def
3011 paragraph
3012 the second
3013 third and
3014 final"});
3015
3016 // clip point
3017 cx.set_shared_state(indoc! {r"
3018 1 2 3
3019 4 5 6
3020 7 8 ˇ9
3021 "})
3022 .await;
3023 cx.simulate_shared_keystrokes("shift-h").await;
3024 cx.shared_state().await.assert_eq(indoc! {"
3025 1 2 ˇ3
3026 4 5 6
3027 7 8 9
3028 "});
3029
3030 cx.set_shared_state(indoc! {r"
3031 1 2 3
3032 4 5 6
3033 ˇ7 8 9
3034 "})
3035 .await;
3036 cx.simulate_shared_keystrokes("shift-h").await;
3037 cx.shared_state().await.assert_eq(indoc! {"
3038 ˇ1 2 3
3039 4 5 6
3040 7 8 9
3041 "});
3042
3043 cx.set_shared_state(indoc! {r"
3044 1 2 3
3045 4 5 ˇ6
3046 7 8 9"})
3047 .await;
3048 cx.simulate_shared_keystrokes("9 shift-h").await;
3049 cx.shared_state().await.assert_eq(indoc! {"
3050 1 2 3
3051 4 5 6
3052 7 8 ˇ9"});
3053 }
3054
3055 #[gpui::test]
3056 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3057 let mut cx = NeovimBackedTestContext::new(cx).await;
3058 let initial_state = indoc! {r"abˇc
3059 def
3060 paragraph
3061 the second
3062 third and
3063 final"};
3064
3065 cx.set_shared_state(initial_state).await;
3066 cx.simulate_shared_keystrokes("shift-m").await;
3067 cx.shared_state().await.assert_eq(indoc! {r"abc
3068 def
3069 paˇragraph
3070 the second
3071 third and
3072 final"});
3073
3074 cx.set_shared_state(indoc! {r"
3075 1 2 3
3076 4 5 6
3077 7 8 ˇ9
3078 "})
3079 .await;
3080 cx.simulate_shared_keystrokes("shift-m").await;
3081 cx.shared_state().await.assert_eq(indoc! {"
3082 1 2 3
3083 4 5 ˇ6
3084 7 8 9
3085 "});
3086 cx.set_shared_state(indoc! {r"
3087 1 2 3
3088 4 5 6
3089 ˇ7 8 9
3090 "})
3091 .await;
3092 cx.simulate_shared_keystrokes("shift-m").await;
3093 cx.shared_state().await.assert_eq(indoc! {"
3094 1 2 3
3095 ˇ4 5 6
3096 7 8 9
3097 "});
3098 cx.set_shared_state(indoc! {r"
3099 ˇ1 2 3
3100 4 5 6
3101 7 8 9
3102 "})
3103 .await;
3104 cx.simulate_shared_keystrokes("shift-m").await;
3105 cx.shared_state().await.assert_eq(indoc! {"
3106 1 2 3
3107 ˇ4 5 6
3108 7 8 9
3109 "});
3110 cx.set_shared_state(indoc! {r"
3111 1 2 3
3112 ˇ4 5 6
3113 7 8 9
3114 "})
3115 .await;
3116 cx.simulate_shared_keystrokes("shift-m").await;
3117 cx.shared_state().await.assert_eq(indoc! {"
3118 1 2 3
3119 ˇ4 5 6
3120 7 8 9
3121 "});
3122 cx.set_shared_state(indoc! {r"
3123 1 2 3
3124 4 5 ˇ6
3125 7 8 9
3126 "})
3127 .await;
3128 cx.simulate_shared_keystrokes("shift-m").await;
3129 cx.shared_state().await.assert_eq(indoc! {"
3130 1 2 3
3131 4 5 ˇ6
3132 7 8 9
3133 "});
3134 }
3135
3136 #[gpui::test]
3137 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3138 let mut cx = NeovimBackedTestContext::new(cx).await;
3139 let initial_state = indoc! {r"abc
3140 deˇf
3141 paragraph
3142 the second
3143 third and
3144 final"};
3145
3146 cx.set_shared_state(initial_state).await;
3147 cx.simulate_shared_keystrokes("shift-l").await;
3148 cx.shared_state().await.assert_eq(indoc! {r"abc
3149 def
3150 paragraph
3151 the second
3152 third and
3153 fiˇnal"});
3154
3155 cx.set_shared_state(indoc! {r"
3156 1 2 3
3157 4 5 ˇ6
3158 7 8 9
3159 "})
3160 .await;
3161 cx.simulate_shared_keystrokes("shift-l").await;
3162 cx.shared_state().await.assert_eq(indoc! {"
3163 1 2 3
3164 4 5 6
3165 7 8 9
3166 ˇ"});
3167
3168 cx.set_shared_state(indoc! {r"
3169 1 2 3
3170 ˇ4 5 6
3171 7 8 9
3172 "})
3173 .await;
3174 cx.simulate_shared_keystrokes("shift-l").await;
3175 cx.shared_state().await.assert_eq(indoc! {"
3176 1 2 3
3177 4 5 6
3178 7 8 9
3179 ˇ"});
3180
3181 cx.set_shared_state(indoc! {r"
3182 1 2 ˇ3
3183 4 5 6
3184 7 8 9
3185 "})
3186 .await;
3187 cx.simulate_shared_keystrokes("shift-l").await;
3188 cx.shared_state().await.assert_eq(indoc! {"
3189 1 2 3
3190 4 5 6
3191 7 8 9
3192 ˇ"});
3193
3194 cx.set_shared_state(indoc! {r"
3195 ˇ1 2 3
3196 4 5 6
3197 7 8 9
3198 "})
3199 .await;
3200 cx.simulate_shared_keystrokes("shift-l").await;
3201 cx.shared_state().await.assert_eq(indoc! {"
3202 1 2 3
3203 4 5 6
3204 7 8 9
3205 ˇ"});
3206
3207 cx.set_shared_state(indoc! {r"
3208 1 2 3
3209 4 5 ˇ6
3210 7 8 9
3211 "})
3212 .await;
3213 cx.simulate_shared_keystrokes("9 shift-l").await;
3214 cx.shared_state().await.assert_eq(indoc! {"
3215 1 2 ˇ3
3216 4 5 6
3217 7 8 9
3218 "});
3219 }
3220
3221 #[gpui::test]
3222 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3223 let mut cx = NeovimBackedTestContext::new(cx).await;
3224 cx.set_shared_state(indoc! {r"
3225 456 5ˇ67 678
3226 "})
3227 .await;
3228 cx.simulate_shared_keystrokes("g e").await;
3229 cx.shared_state().await.assert_eq(indoc! {"
3230 45ˇ6 567 678
3231 "});
3232
3233 // Test times
3234 cx.set_shared_state(indoc! {r"
3235 123 234 345
3236 456 5ˇ67 678
3237 "})
3238 .await;
3239 cx.simulate_shared_keystrokes("4 g e").await;
3240 cx.shared_state().await.assert_eq(indoc! {"
3241 12ˇ3 234 345
3242 456 567 678
3243 "});
3244
3245 // With punctuation
3246 cx.set_shared_state(indoc! {r"
3247 123 234 345
3248 4;5.6 5ˇ67 678
3249 789 890 901
3250 "})
3251 .await;
3252 cx.simulate_shared_keystrokes("g e").await;
3253 cx.shared_state().await.assert_eq(indoc! {"
3254 123 234 345
3255 4;5.ˇ6 567 678
3256 789 890 901
3257 "});
3258
3259 // With punctuation and count
3260 cx.set_shared_state(indoc! {r"
3261 123 234 345
3262 4;5.6 5ˇ67 678
3263 789 890 901
3264 "})
3265 .await;
3266 cx.simulate_shared_keystrokes("5 g e").await;
3267 cx.shared_state().await.assert_eq(indoc! {"
3268 123 234 345
3269 ˇ4;5.6 567 678
3270 789 890 901
3271 "});
3272
3273 // newlines
3274 cx.set_shared_state(indoc! {r"
3275 123 234 345
3276
3277 78ˇ9 890 901
3278 "})
3279 .await;
3280 cx.simulate_shared_keystrokes("g e").await;
3281 cx.shared_state().await.assert_eq(indoc! {"
3282 123 234 345
3283 ˇ
3284 789 890 901
3285 "});
3286 cx.simulate_shared_keystrokes("g e").await;
3287 cx.shared_state().await.assert_eq(indoc! {"
3288 123 234 34ˇ5
3289
3290 789 890 901
3291 "});
3292
3293 // With punctuation
3294 cx.set_shared_state(indoc! {r"
3295 123 234 345
3296 4;5.ˇ6 567 678
3297 789 890 901
3298 "})
3299 .await;
3300 cx.simulate_shared_keystrokes("g shift-e").await;
3301 cx.shared_state().await.assert_eq(indoc! {"
3302 123 234 34ˇ5
3303 4;5.6 567 678
3304 789 890 901
3305 "});
3306 }
3307
3308 #[gpui::test]
3309 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3310 let mut cx = NeovimBackedTestContext::new(cx).await;
3311
3312 cx.set_shared_state(indoc! {"
3313 fn aˇ() {
3314 return
3315 }
3316 "})
3317 .await;
3318 cx.simulate_shared_keystrokes("v $ %").await;
3319 cx.shared_state().await.assert_eq(indoc! {"
3320 fn a«() {
3321 return
3322 }ˇ»
3323 "});
3324 }
3325
3326 #[gpui::test]
3327 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3328 let mut cx = VimTestContext::new(cx, true).await;
3329
3330 cx.set_state(
3331 indoc! {"
3332 struct Foo {
3333 ˇ
3334 }
3335 "},
3336 Mode::Normal,
3337 );
3338
3339 cx.update_editor(|editor, cx| {
3340 let range = editor.selections.newest_anchor().range();
3341 let inlay_text = " field: int,\n field2: string\n field3: float";
3342 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3343 editor.splice_inlays(vec![], vec![inlay], cx);
3344 });
3345
3346 cx.simulate_keystrokes("j");
3347 cx.assert_state(
3348 indoc! {"
3349 struct Foo {
3350
3351 ˇ}
3352 "},
3353 Mode::Normal,
3354 );
3355 }
3356}