1use editor::{
2 display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
3 movement::{
4 self, find_boundary, find_preceding_boundary_display_point, FindRange, TextLayoutDetails,
5 },
6 scroll::Autoscroll,
7 Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset,
8};
9use gpui::{actions, impl_actions, px, ViewContext};
10use language::{CharKind, Point, Selection, SelectionGoal};
11use multi_buffer::MultiBufferRow;
12use serde::Deserialize;
13use std::ops::Range;
14
15use crate::{
16 normal::mark,
17 state::{Mode, Operator},
18 surrounds::SurroundsType,
19 Vim,
20};
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub enum Motion {
24 Left,
25 Backspace,
26 Down {
27 display_lines: bool,
28 },
29 Up {
30 display_lines: bool,
31 },
32 Right,
33 Space,
34 NextWordStart {
35 ignore_punctuation: bool,
36 },
37 NextWordEnd {
38 ignore_punctuation: bool,
39 },
40 PreviousWordStart {
41 ignore_punctuation: bool,
42 },
43 PreviousWordEnd {
44 ignore_punctuation: bool,
45 },
46 NextSubwordStart {
47 ignore_punctuation: bool,
48 },
49 NextSubwordEnd {
50 ignore_punctuation: bool,
51 },
52 PreviousSubwordStart {
53 ignore_punctuation: bool,
54 },
55 PreviousSubwordEnd {
56 ignore_punctuation: bool,
57 },
58 FirstNonWhitespace {
59 display_lines: bool,
60 },
61 CurrentLine,
62 StartOfLine {
63 display_lines: bool,
64 },
65 EndOfLine {
66 display_lines: bool,
67 },
68 StartOfParagraph,
69 EndOfParagraph,
70 StartOfDocument,
71 EndOfDocument,
72 Matching,
73 FindForward {
74 before: bool,
75 char: char,
76 mode: FindRange,
77 smartcase: bool,
78 },
79 FindBackward {
80 after: bool,
81 char: char,
82 mode: FindRange,
83 smartcase: bool,
84 },
85 RepeatFind {
86 last_find: Box<Motion>,
87 },
88 RepeatFindReversed {
89 last_find: Box<Motion>,
90 },
91 NextLineStart,
92 PreviousLineStart,
93 StartOfLineDownward,
94 EndOfLineDownward,
95 GoToColumn,
96 WindowTop,
97 WindowMiddle,
98 WindowBottom,
99
100 // we don't have a good way to run a search synchronously, so
101 // we handle search motions by running the search async and then
102 // calling back into motion with this
103 ZedSearchResult {
104 prior_selections: Vec<Range<Anchor>>,
105 new_selections: Vec<Range<Anchor>>,
106 },
107 Jump {
108 anchor: Anchor,
109 line: bool,
110 },
111}
112
113#[derive(Clone, Deserialize, PartialEq)]
114#[serde(rename_all = "camelCase")]
115struct NextWordStart {
116 #[serde(default)]
117 ignore_punctuation: bool,
118}
119
120#[derive(Clone, Deserialize, PartialEq)]
121#[serde(rename_all = "camelCase")]
122struct NextWordEnd {
123 #[serde(default)]
124 ignore_punctuation: bool,
125}
126
127#[derive(Clone, Deserialize, PartialEq)]
128#[serde(rename_all = "camelCase")]
129struct PreviousWordStart {
130 #[serde(default)]
131 ignore_punctuation: bool,
132}
133
134#[derive(Clone, Deserialize, PartialEq)]
135#[serde(rename_all = "camelCase")]
136struct PreviousWordEnd {
137 #[serde(default)]
138 ignore_punctuation: bool,
139}
140
141#[derive(Clone, Deserialize, PartialEq)]
142#[serde(rename_all = "camelCase")]
143pub(crate) struct NextSubwordStart {
144 #[serde(default)]
145 pub(crate) ignore_punctuation: bool,
146}
147
148#[derive(Clone, Deserialize, PartialEq)]
149#[serde(rename_all = "camelCase")]
150pub(crate) struct NextSubwordEnd {
151 #[serde(default)]
152 pub(crate) ignore_punctuation: bool,
153}
154
155#[derive(Clone, Deserialize, PartialEq)]
156#[serde(rename_all = "camelCase")]
157pub(crate) struct PreviousSubwordStart {
158 #[serde(default)]
159 pub(crate) ignore_punctuation: bool,
160}
161
162#[derive(Clone, Deserialize, PartialEq)]
163#[serde(rename_all = "camelCase")]
164pub(crate) struct PreviousSubwordEnd {
165 #[serde(default)]
166 pub(crate) ignore_punctuation: bool,
167}
168
169#[derive(Clone, Deserialize, PartialEq)]
170#[serde(rename_all = "camelCase")]
171pub(crate) struct Up {
172 #[serde(default)]
173 pub(crate) display_lines: bool,
174}
175
176#[derive(Clone, Deserialize, PartialEq)]
177#[serde(rename_all = "camelCase")]
178pub(crate) struct Down {
179 #[serde(default)]
180 pub(crate) display_lines: bool,
181}
182
183#[derive(Clone, Deserialize, PartialEq)]
184#[serde(rename_all = "camelCase")]
185struct FirstNonWhitespace {
186 #[serde(default)]
187 display_lines: bool,
188}
189
190#[derive(Clone, Deserialize, PartialEq)]
191#[serde(rename_all = "camelCase")]
192struct EndOfLine {
193 #[serde(default)]
194 display_lines: bool,
195}
196
197#[derive(Clone, Deserialize, PartialEq)]
198#[serde(rename_all = "camelCase")]
199pub struct StartOfLine {
200 #[serde(default)]
201 pub(crate) display_lines: bool,
202}
203
204impl_actions!(
205 vim,
206 [
207 StartOfLine,
208 EndOfLine,
209 FirstNonWhitespace,
210 Down,
211 Up,
212 NextWordStart,
213 NextWordEnd,
214 PreviousWordStart,
215 PreviousWordEnd,
216 NextSubwordStart,
217 NextSubwordEnd,
218 PreviousSubwordStart,
219 PreviousSubwordEnd,
220 ]
221);
222
223actions!(
224 vim,
225 [
226 Left,
227 Backspace,
228 Right,
229 Space,
230 CurrentLine,
231 StartOfParagraph,
232 EndOfParagraph,
233 StartOfDocument,
234 EndOfDocument,
235 Matching,
236 NextLineStart,
237 PreviousLineStart,
238 StartOfLineDownward,
239 EndOfLineDownward,
240 GoToColumn,
241 RepeatFind,
242 RepeatFindReversed,
243 WindowTop,
244 WindowMiddle,
245 WindowBottom,
246 ]
247);
248
249pub fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
250 Vim::action(editor, cx, |vim, _: &Left, cx| vim.motion(Motion::Left, cx));
251 Vim::action(editor, cx, |vim, _: &Backspace, cx| {
252 vim.motion(Motion::Backspace, cx)
253 });
254 Vim::action(editor, cx, |vim, action: &Down, cx| {
255 vim.motion(
256 Motion::Down {
257 display_lines: action.display_lines,
258 },
259 cx,
260 )
261 });
262 Vim::action(editor, cx, |vim, action: &Up, cx| {
263 vim.motion(
264 Motion::Up {
265 display_lines: action.display_lines,
266 },
267 cx,
268 )
269 });
270 Vim::action(editor, cx, |vim, _: &Right, cx| {
271 vim.motion(Motion::Right, cx)
272 });
273 Vim::action(editor, cx, |vim, _: &Space, cx| {
274 vim.motion(Motion::Space, cx)
275 });
276 Vim::action(editor, cx, |vim, action: &FirstNonWhitespace, cx| {
277 vim.motion(
278 Motion::FirstNonWhitespace {
279 display_lines: action.display_lines,
280 },
281 cx,
282 )
283 });
284 Vim::action(editor, cx, |vim, action: &StartOfLine, cx| {
285 vim.motion(
286 Motion::StartOfLine {
287 display_lines: action.display_lines,
288 },
289 cx,
290 )
291 });
292 Vim::action(editor, cx, |vim, action: &EndOfLine, cx| {
293 vim.motion(
294 Motion::EndOfLine {
295 display_lines: action.display_lines,
296 },
297 cx,
298 )
299 });
300 Vim::action(editor, cx, |vim, _: &CurrentLine, cx| {
301 vim.motion(Motion::CurrentLine, cx)
302 });
303 Vim::action(editor, cx, |vim, _: &StartOfParagraph, cx| {
304 vim.motion(Motion::StartOfParagraph, cx)
305 });
306 Vim::action(editor, cx, |vim, _: &EndOfParagraph, cx| {
307 vim.motion(Motion::EndOfParagraph, cx)
308 });
309 Vim::action(editor, cx, |vim, _: &StartOfDocument, cx| {
310 vim.motion(Motion::StartOfDocument, cx)
311 });
312 Vim::action(editor, cx, |vim, _: &EndOfDocument, cx| {
313 vim.motion(Motion::EndOfDocument, cx)
314 });
315 Vim::action(editor, cx, |vim, _: &Matching, cx| {
316 vim.motion(Motion::Matching, cx)
317 });
318
319 Vim::action(
320 editor,
321 cx,
322 |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, cx| {
323 vim.motion(Motion::NextWordStart { ignore_punctuation }, cx)
324 },
325 );
326 Vim::action(
327 editor,
328 cx,
329 |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx| {
330 vim.motion(Motion::NextWordEnd { ignore_punctuation }, cx)
331 },
332 );
333 Vim::action(
334 editor,
335 cx,
336 |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, cx| {
337 vim.motion(Motion::PreviousWordStart { ignore_punctuation }, cx)
338 },
339 );
340 Vim::action(
341 editor,
342 cx,
343 |vim, &PreviousWordEnd { ignore_punctuation }, cx| {
344 vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, cx)
345 },
346 );
347 Vim::action(
348 editor,
349 cx,
350 |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, cx| {
351 vim.motion(Motion::NextSubwordStart { ignore_punctuation }, cx)
352 },
353 );
354 Vim::action(
355 editor,
356 cx,
357 |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, cx| {
358 vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, cx)
359 },
360 );
361 Vim::action(
362 editor,
363 cx,
364 |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, cx| {
365 vim.motion(Motion::PreviousSubwordStart { ignore_punctuation }, cx)
366 },
367 );
368 Vim::action(
369 editor,
370 cx,
371 |vim, &PreviousSubwordEnd { ignore_punctuation }, cx| {
372 vim.motion(Motion::PreviousSubwordEnd { ignore_punctuation }, cx)
373 },
374 );
375 Vim::action(editor, cx, |vim, &NextLineStart, cx| {
376 vim.motion(Motion::NextLineStart, cx)
377 });
378 Vim::action(editor, cx, |vim, &PreviousLineStart, cx| {
379 vim.motion(Motion::PreviousLineStart, cx)
380 });
381 Vim::action(editor, cx, |vim, &StartOfLineDownward, cx| {
382 vim.motion(Motion::StartOfLineDownward, cx)
383 });
384 Vim::action(editor, cx, |vim, &EndOfLineDownward, cx| {
385 vim.motion(Motion::EndOfLineDownward, cx)
386 });
387 Vim::action(editor, cx, |vim, &GoToColumn, cx| {
388 vim.motion(Motion::GoToColumn, cx)
389 });
390
391 Vim::action(editor, cx, |vim, _: &RepeatFind, cx| {
392 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
393 vim.motion(Motion::RepeatFind { last_find }, cx);
394 }
395 });
396
397 Vim::action(editor, cx, |vim, _: &RepeatFindReversed, cx| {
398 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
399 vim.motion(Motion::RepeatFindReversed { last_find }, cx);
400 }
401 });
402 Vim::action(editor, cx, |vim, &WindowTop, cx| {
403 vim.motion(Motion::WindowTop, cx)
404 });
405 Vim::action(editor, cx, |vim, &WindowMiddle, cx| {
406 vim.motion(Motion::WindowMiddle, cx)
407 });
408 Vim::action(editor, cx, |vim, &WindowBottom, cx| {
409 vim.motion(Motion::WindowBottom, cx)
410 });
411}
412
413impl Vim {
414 pub(crate) fn search_motion(&mut self, m: Motion, cx: &mut ViewContext<Self>) {
415 if let Motion::ZedSearchResult {
416 prior_selections, ..
417 } = &m
418 {
419 match self.mode {
420 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
421 if !prior_selections.is_empty() {
422 self.update_editor(cx, |_, editor, cx| {
423 editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
424 s.select_ranges(prior_selections.iter().cloned())
425 })
426 });
427 }
428 }
429 Mode::Normal | Mode::Replace | Mode::Insert => {
430 if self.active_operator().is_none() {
431 return;
432 }
433 }
434 }
435 }
436
437 self.motion(m, cx)
438 }
439
440 pub(crate) fn motion(&mut self, motion: Motion, cx: &mut ViewContext<Self>) {
441 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
442 self.active_operator()
443 {
444 self.pop_operator(cx);
445 }
446
447 let count = self.take_count(cx);
448 let active_operator = self.active_operator();
449 let mut waiting_operator: Option<Operator> = None;
450 match self.mode {
451 Mode::Normal | Mode::Replace | Mode::Insert => {
452 if active_operator == Some(Operator::AddSurrounds { target: None }) {
453 waiting_operator = Some(Operator::AddSurrounds {
454 target: Some(SurroundsType::Motion(motion)),
455 });
456 } else {
457 self.normal_motion(motion.clone(), active_operator.clone(), count, cx)
458 }
459 }
460 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
461 self.visual_motion(motion.clone(), count, cx)
462 }
463 }
464 self.clear_operator(cx);
465 if let Some(operator) = waiting_operator {
466 self.push_operator(operator, cx);
467 self.pre_count = count
468 }
469 }
470}
471
472// Motion handling is specified here:
473// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
474impl Motion {
475 pub fn linewise(&self) -> bool {
476 use Motion::*;
477 match self {
478 Down { .. }
479 | Up { .. }
480 | StartOfDocument
481 | EndOfDocument
482 | CurrentLine
483 | NextLineStart
484 | PreviousLineStart
485 | StartOfLineDownward
486 | StartOfParagraph
487 | WindowTop
488 | WindowMiddle
489 | WindowBottom
490 | Jump { line: true, .. }
491 | EndOfParagraph => true,
492 EndOfLine { .. }
493 | Matching
494 | FindForward { .. }
495 | Left
496 | Backspace
497 | Right
498 | Space
499 | StartOfLine { .. }
500 | EndOfLineDownward
501 | GoToColumn
502 | NextWordStart { .. }
503 | NextWordEnd { .. }
504 | PreviousWordStart { .. }
505 | PreviousWordEnd { .. }
506 | NextSubwordStart { .. }
507 | NextSubwordEnd { .. }
508 | PreviousSubwordStart { .. }
509 | PreviousSubwordEnd { .. }
510 | FirstNonWhitespace { .. }
511 | FindBackward { .. }
512 | RepeatFind { .. }
513 | RepeatFindReversed { .. }
514 | Jump { line: false, .. }
515 | ZedSearchResult { .. } => false,
516 }
517 }
518
519 pub fn infallible(&self) -> bool {
520 use Motion::*;
521 match self {
522 StartOfDocument | EndOfDocument | CurrentLine => true,
523 Down { .. }
524 | Up { .. }
525 | EndOfLine { .. }
526 | Matching
527 | FindForward { .. }
528 | RepeatFind { .. }
529 | Left
530 | Backspace
531 | Right
532 | Space
533 | StartOfLine { .. }
534 | StartOfParagraph
535 | EndOfParagraph
536 | StartOfLineDownward
537 | EndOfLineDownward
538 | GoToColumn
539 | NextWordStart { .. }
540 | NextWordEnd { .. }
541 | PreviousWordStart { .. }
542 | PreviousWordEnd { .. }
543 | NextSubwordStart { .. }
544 | NextSubwordEnd { .. }
545 | PreviousSubwordStart { .. }
546 | PreviousSubwordEnd { .. }
547 | FirstNonWhitespace { .. }
548 | FindBackward { .. }
549 | RepeatFindReversed { .. }
550 | WindowTop
551 | WindowMiddle
552 | WindowBottom
553 | NextLineStart
554 | PreviousLineStart
555 | ZedSearchResult { .. }
556 | Jump { .. } => false,
557 }
558 }
559
560 pub fn inclusive(&self) -> bool {
561 use Motion::*;
562 match self {
563 Down { .. }
564 | Up { .. }
565 | StartOfDocument
566 | EndOfDocument
567 | CurrentLine
568 | EndOfLine { .. }
569 | EndOfLineDownward
570 | Matching
571 | FindForward { .. }
572 | WindowTop
573 | WindowMiddle
574 | WindowBottom
575 | NextWordEnd { .. }
576 | PreviousWordEnd { .. }
577 | NextSubwordEnd { .. }
578 | PreviousSubwordEnd { .. }
579 | NextLineStart
580 | PreviousLineStart => true,
581 Left
582 | Backspace
583 | Right
584 | Space
585 | StartOfLine { .. }
586 | StartOfLineDownward
587 | StartOfParagraph
588 | EndOfParagraph
589 | GoToColumn
590 | NextWordStart { .. }
591 | PreviousWordStart { .. }
592 | NextSubwordStart { .. }
593 | PreviousSubwordStart { .. }
594 | FirstNonWhitespace { .. }
595 | FindBackward { .. }
596 | Jump { .. }
597 | ZedSearchResult { .. } => false,
598 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
599 motion.inclusive()
600 }
601 }
602 }
603
604 pub fn move_point(
605 &self,
606 map: &DisplaySnapshot,
607 point: DisplayPoint,
608 goal: SelectionGoal,
609 maybe_times: Option<usize>,
610 text_layout_details: &TextLayoutDetails,
611 ) -> Option<(DisplayPoint, SelectionGoal)> {
612 let times = maybe_times.unwrap_or(1);
613 use Motion::*;
614 let infallible = self.infallible();
615 let (new_point, goal) = match self {
616 Left => (left(map, point, times), SelectionGoal::None),
617 Backspace => (backspace(map, point, times), SelectionGoal::None),
618 Down {
619 display_lines: false,
620 } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
621 Down {
622 display_lines: true,
623 } => down_display(map, point, goal, times, &text_layout_details),
624 Up {
625 display_lines: false,
626 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
627 Up {
628 display_lines: true,
629 } => up_display(map, point, goal, times, &text_layout_details),
630 Right => (right(map, point, times), SelectionGoal::None),
631 Space => (space(map, point, times), SelectionGoal::None),
632 NextWordStart { ignore_punctuation } => (
633 next_word_start(map, point, *ignore_punctuation, times),
634 SelectionGoal::None,
635 ),
636 NextWordEnd { ignore_punctuation } => (
637 next_word_end(map, point, *ignore_punctuation, times, true),
638 SelectionGoal::None,
639 ),
640 PreviousWordStart { ignore_punctuation } => (
641 previous_word_start(map, point, *ignore_punctuation, times),
642 SelectionGoal::None,
643 ),
644 PreviousWordEnd { ignore_punctuation } => (
645 previous_word_end(map, point, *ignore_punctuation, times),
646 SelectionGoal::None,
647 ),
648 NextSubwordStart { ignore_punctuation } => (
649 next_subword_start(map, point, *ignore_punctuation, times),
650 SelectionGoal::None,
651 ),
652 NextSubwordEnd { ignore_punctuation } => (
653 next_subword_end(map, point, *ignore_punctuation, times, true),
654 SelectionGoal::None,
655 ),
656 PreviousSubwordStart { ignore_punctuation } => (
657 previous_subword_start(map, point, *ignore_punctuation, times),
658 SelectionGoal::None,
659 ),
660 PreviousSubwordEnd { ignore_punctuation } => (
661 previous_subword_end(map, point, *ignore_punctuation, times),
662 SelectionGoal::None,
663 ),
664 FirstNonWhitespace { display_lines } => (
665 first_non_whitespace(map, *display_lines, point),
666 SelectionGoal::None,
667 ),
668 StartOfLine { display_lines } => (
669 start_of_line(map, *display_lines, point),
670 SelectionGoal::None,
671 ),
672 EndOfLine { display_lines } => (
673 end_of_line(map, *display_lines, point, times),
674 SelectionGoal::None,
675 ),
676 StartOfParagraph => (
677 movement::start_of_paragraph(map, point, times),
678 SelectionGoal::None,
679 ),
680 EndOfParagraph => (
681 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
682 SelectionGoal::None,
683 ),
684 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
685 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
686 EndOfDocument => (
687 end_of_document(map, point, maybe_times),
688 SelectionGoal::None,
689 ),
690 Matching => (matching(map, point), SelectionGoal::None),
691 // t f
692 FindForward {
693 before,
694 char,
695 mode,
696 smartcase,
697 } => {
698 return find_forward(map, point, *before, *char, times, *mode, *smartcase)
699 .map(|new_point| (new_point, SelectionGoal::None))
700 }
701 // T F
702 FindBackward {
703 after,
704 char,
705 mode,
706 smartcase,
707 } => (
708 find_backward(map, point, *after, *char, times, *mode, *smartcase),
709 SelectionGoal::None,
710 ),
711 // ; -- repeat the last find done with t, f, T, F
712 RepeatFind { last_find } => match **last_find {
713 Motion::FindForward {
714 before,
715 char,
716 mode,
717 smartcase,
718 } => {
719 let mut new_point =
720 find_forward(map, point, before, char, times, mode, smartcase);
721 if new_point == Some(point) {
722 new_point =
723 find_forward(map, point, before, char, times + 1, mode, smartcase);
724 }
725
726 return new_point.map(|new_point| (new_point, SelectionGoal::None));
727 }
728
729 Motion::FindBackward {
730 after,
731 char,
732 mode,
733 smartcase,
734 } => {
735 let mut new_point =
736 find_backward(map, point, after, char, times, mode, smartcase);
737 if new_point == point {
738 new_point =
739 find_backward(map, point, after, char, times + 1, mode, smartcase);
740 }
741
742 (new_point, SelectionGoal::None)
743 }
744 _ => return None,
745 },
746 // , -- repeat the last find done with t, f, T, F, in opposite direction
747 RepeatFindReversed { last_find } => match **last_find {
748 Motion::FindForward {
749 before,
750 char,
751 mode,
752 smartcase,
753 } => {
754 let mut new_point =
755 find_backward(map, point, before, char, times, mode, smartcase);
756 if new_point == point {
757 new_point =
758 find_backward(map, point, before, char, times + 1, mode, smartcase);
759 }
760
761 (new_point, SelectionGoal::None)
762 }
763
764 Motion::FindBackward {
765 after,
766 char,
767 mode,
768 smartcase,
769 } => {
770 let mut new_point =
771 find_forward(map, point, after, char, times, mode, smartcase);
772 if new_point == Some(point) {
773 new_point =
774 find_forward(map, point, after, char, times + 1, mode, smartcase);
775 }
776
777 return new_point.map(|new_point| (new_point, SelectionGoal::None));
778 }
779 _ => return None,
780 },
781 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
782 PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
783 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
784 EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
785 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
786 WindowTop => window_top(map, point, &text_layout_details, times - 1),
787 WindowMiddle => window_middle(map, point, &text_layout_details),
788 WindowBottom => window_bottom(map, point, &text_layout_details, times - 1),
789 Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
790 ZedSearchResult { new_selections, .. } => {
791 // There will be only one selection, as
792 // Search::SelectNextMatch selects a single match.
793 if let Some(new_selection) = new_selections.first() {
794 (
795 new_selection.start.to_display_point(map),
796 SelectionGoal::None,
797 )
798 } else {
799 return None;
800 }
801 }
802 };
803
804 (new_point != point || infallible).then_some((new_point, goal))
805 }
806
807 // Get the range value after self is applied to the specified selection.
808 pub fn range(
809 &self,
810 map: &DisplaySnapshot,
811 selection: Selection<DisplayPoint>,
812 times: Option<usize>,
813 expand_to_surrounding_newline: bool,
814 text_layout_details: &TextLayoutDetails,
815 ) -> Option<Range<DisplayPoint>> {
816 if let Motion::ZedSearchResult {
817 prior_selections,
818 new_selections,
819 } = self
820 {
821 if let Some((prior_selection, new_selection)) =
822 prior_selections.first().zip(new_selections.first())
823 {
824 let start = prior_selection
825 .start
826 .to_display_point(map)
827 .min(new_selection.start.to_display_point(map));
828 let end = new_selection
829 .end
830 .to_display_point(map)
831 .max(prior_selection.end.to_display_point(map));
832
833 if start < end {
834 return Some(start..end);
835 } else {
836 return Some(end..start);
837 }
838 } else {
839 return None;
840 }
841 }
842
843 if let Some((new_head, goal)) = self.move_point(
844 map,
845 selection.head(),
846 selection.goal,
847 times,
848 &text_layout_details,
849 ) {
850 let mut selection = selection.clone();
851 selection.set_head(new_head, goal);
852
853 if self.linewise() {
854 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
855
856 if expand_to_surrounding_newline {
857 if selection.end.row() < map.max_point().row() {
858 *selection.end.row_mut() += 1;
859 *selection.end.column_mut() = 0;
860 selection.end = map.clip_point(selection.end, Bias::Right);
861 // Don't reset the end here
862 return Some(selection.start..selection.end);
863 } else if selection.start.row().0 > 0 {
864 *selection.start.row_mut() -= 1;
865 *selection.start.column_mut() = map.line_len(selection.start.row());
866 selection.start = map.clip_point(selection.start, Bias::Left);
867 }
868 }
869
870 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
871 } else {
872 // Another special case: When using the "w" motion in combination with an
873 // operator and the last word moved over is at the end of a line, the end of
874 // that word becomes the end of the operated text, not the first word in the
875 // next line.
876 if let Motion::NextWordStart {
877 ignore_punctuation: _,
878 } = self
879 {
880 let start_row = MultiBufferRow(selection.start.to_point(&map).row);
881 if selection.end.to_point(&map).row > start_row.0 {
882 selection.end =
883 Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
884 .to_display_point(&map)
885 }
886 }
887
888 // If the motion is exclusive and the end of the motion is in column 1, the
889 // end of the motion is moved to the end of the previous line and the motion
890 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
891 // but "d}" will not include that line.
892 let mut inclusive = self.inclusive();
893 let start_point = selection.start.to_point(&map);
894 let mut end_point = selection.end.to_point(&map);
895
896 // DisplayPoint
897
898 if !inclusive
899 && self != &Motion::Backspace
900 && end_point.row > start_point.row
901 && end_point.column == 0
902 {
903 inclusive = true;
904 end_point.row -= 1;
905 end_point.column = 0;
906 selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
907 }
908
909 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
910 selection.end = movement::saturating_right(map, selection.end)
911 }
912 }
913 Some(selection.start..selection.end)
914 } else {
915 None
916 }
917 }
918
919 // Expands a selection using self for an operator
920 pub fn expand_selection(
921 &self,
922 map: &DisplaySnapshot,
923 selection: &mut Selection<DisplayPoint>,
924 times: Option<usize>,
925 expand_to_surrounding_newline: bool,
926 text_layout_details: &TextLayoutDetails,
927 ) -> bool {
928 if let Some(range) = self.range(
929 map,
930 selection.clone(),
931 times,
932 expand_to_surrounding_newline,
933 text_layout_details,
934 ) {
935 selection.start = range.start;
936 selection.end = range.end;
937 true
938 } else {
939 false
940 }
941 }
942}
943
944fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
945 for _ in 0..times {
946 point = movement::saturating_left(map, point);
947 if point.column() == 0 {
948 break;
949 }
950 }
951 point
952}
953
954pub(crate) fn backspace(
955 map: &DisplaySnapshot,
956 mut point: DisplayPoint,
957 times: usize,
958) -> DisplayPoint {
959 for _ in 0..times {
960 point = movement::left(map, point);
961 if point.is_zero() {
962 break;
963 }
964 }
965 point
966}
967
968fn space(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
969 for _ in 0..times {
970 point = wrapping_right(map, point);
971 if point == map.max_point() {
972 break;
973 }
974 }
975 point
976}
977
978fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
979 let max_column = map.line_len(point.row()).saturating_sub(1);
980 if point.column() < max_column {
981 *point.column_mut() += 1;
982 } else if point.row() < map.max_point().row() {
983 *point.row_mut() += 1;
984 *point.column_mut() = 0;
985 }
986 point
987}
988
989pub(crate) fn start_of_relative_buffer_row(
990 map: &DisplaySnapshot,
991 point: DisplayPoint,
992 times: isize,
993) -> DisplayPoint {
994 let start = map.display_point_to_fold_point(point, Bias::Left);
995 let target = start.row() as isize + times;
996 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
997
998 map.clip_point(
999 map.fold_point_to_display_point(
1000 map.fold_snapshot
1001 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1002 ),
1003 Bias::Right,
1004 )
1005}
1006
1007fn up_down_buffer_rows(
1008 map: &DisplaySnapshot,
1009 point: DisplayPoint,
1010 mut goal: SelectionGoal,
1011 times: isize,
1012 text_layout_details: &TextLayoutDetails,
1013) -> (DisplayPoint, SelectionGoal) {
1014 let start = map.display_point_to_fold_point(point, Bias::Left);
1015 let begin_folded_line = map.fold_point_to_display_point(
1016 map.fold_snapshot
1017 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1018 );
1019 let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1020
1021 let (goal_wrap, goal_x) = match goal {
1022 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1023 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1024 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1025 _ => {
1026 let x = map.x_for_display_point(point, text_layout_details);
1027 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1028 (select_nth_wrapped_row, x.0)
1029 }
1030 };
1031
1032 let target = start.row() as isize + times;
1033 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1034
1035 let mut begin_folded_line = map.fold_point_to_display_point(
1036 map.fold_snapshot
1037 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
1038 );
1039
1040 let mut i = 0;
1041 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1042 let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1043 if map
1044 .display_point_to_fold_point(next_folded_line, Bias::Right)
1045 .row()
1046 == new_row
1047 {
1048 i += 1;
1049 begin_folded_line = next_folded_line;
1050 } else {
1051 break;
1052 }
1053 }
1054
1055 let new_col = if i == goal_wrap {
1056 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1057 } else {
1058 map.line_len(begin_folded_line.row())
1059 };
1060
1061 (
1062 map.clip_point(
1063 DisplayPoint::new(begin_folded_line.row(), new_col),
1064 Bias::Left,
1065 ),
1066 goal,
1067 )
1068}
1069
1070fn down_display(
1071 map: &DisplaySnapshot,
1072 mut point: DisplayPoint,
1073 mut goal: SelectionGoal,
1074 times: usize,
1075 text_layout_details: &TextLayoutDetails,
1076) -> (DisplayPoint, SelectionGoal) {
1077 for _ in 0..times {
1078 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1079 }
1080
1081 (point, goal)
1082}
1083
1084fn up_display(
1085 map: &DisplaySnapshot,
1086 mut point: DisplayPoint,
1087 mut goal: SelectionGoal,
1088 times: usize,
1089 text_layout_details: &TextLayoutDetails,
1090) -> (DisplayPoint, SelectionGoal) {
1091 for _ in 0..times {
1092 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
1093 }
1094
1095 (point, goal)
1096}
1097
1098pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1099 for _ in 0..times {
1100 let new_point = movement::saturating_right(map, point);
1101 if point == new_point {
1102 break;
1103 }
1104 point = new_point;
1105 }
1106 point
1107}
1108
1109pub(crate) fn next_char(
1110 map: &DisplaySnapshot,
1111 point: DisplayPoint,
1112 allow_cross_newline: bool,
1113) -> DisplayPoint {
1114 let mut new_point = point;
1115 let mut max_column = map.line_len(new_point.row());
1116 if !allow_cross_newline {
1117 max_column -= 1;
1118 }
1119 if new_point.column() < max_column {
1120 *new_point.column_mut() += 1;
1121 } else if new_point < map.max_point() && allow_cross_newline {
1122 *new_point.row_mut() += 1;
1123 *new_point.column_mut() = 0;
1124 }
1125 map.clip_ignoring_line_ends(new_point, Bias::Right)
1126}
1127
1128pub(crate) fn next_word_start(
1129 map: &DisplaySnapshot,
1130 mut point: DisplayPoint,
1131 ignore_punctuation: bool,
1132 times: usize,
1133) -> DisplayPoint {
1134 let classifier = map
1135 .buffer_snapshot
1136 .char_classifier_at(point.to_point(map))
1137 .ignore_punctuation(ignore_punctuation);
1138 for _ in 0..times {
1139 let mut crossed_newline = false;
1140 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1141 let left_kind = classifier.kind(left);
1142 let right_kind = classifier.kind(right);
1143 let at_newline = right == '\n';
1144
1145 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1146 || at_newline && crossed_newline
1147 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1148
1149 crossed_newline |= at_newline;
1150 found
1151 });
1152 if point == new_point {
1153 break;
1154 }
1155 point = new_point;
1156 }
1157 point
1158}
1159
1160pub(crate) fn next_word_end(
1161 map: &DisplaySnapshot,
1162 mut point: DisplayPoint,
1163 ignore_punctuation: bool,
1164 times: usize,
1165 allow_cross_newline: bool,
1166) -> DisplayPoint {
1167 let classifier = map
1168 .buffer_snapshot
1169 .char_classifier_at(point.to_point(map))
1170 .ignore_punctuation(ignore_punctuation);
1171 for _ in 0..times {
1172 let new_point = next_char(map, point, allow_cross_newline);
1173 let mut need_next_char = false;
1174 let new_point = movement::find_boundary_exclusive(
1175 map,
1176 new_point,
1177 FindRange::MultiLine,
1178 |left, right| {
1179 let left_kind = classifier.kind(left);
1180 let right_kind = classifier.kind(right);
1181 let at_newline = right == '\n';
1182
1183 if !allow_cross_newline && at_newline {
1184 need_next_char = true;
1185 return true;
1186 }
1187
1188 left_kind != right_kind && left_kind != CharKind::Whitespace
1189 },
1190 );
1191 let new_point = if need_next_char {
1192 next_char(map, new_point, true)
1193 } else {
1194 new_point
1195 };
1196 let new_point = map.clip_point(new_point, Bias::Left);
1197 if point == new_point {
1198 break;
1199 }
1200 point = new_point;
1201 }
1202 point
1203}
1204
1205fn previous_word_start(
1206 map: &DisplaySnapshot,
1207 mut point: DisplayPoint,
1208 ignore_punctuation: bool,
1209 times: usize,
1210) -> DisplayPoint {
1211 let classifier = map
1212 .buffer_snapshot
1213 .char_classifier_at(point.to_point(map))
1214 .ignore_punctuation(ignore_punctuation);
1215 for _ in 0..times {
1216 // This works even though find_preceding_boundary is called for every character in the line containing
1217 // cursor because the newline is checked only once.
1218 let new_point = movement::find_preceding_boundary_display_point(
1219 map,
1220 point,
1221 FindRange::MultiLine,
1222 |left, right| {
1223 let left_kind = classifier.kind(left);
1224 let right_kind = classifier.kind(right);
1225
1226 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1227 },
1228 );
1229 if point == new_point {
1230 break;
1231 }
1232 point = new_point;
1233 }
1234 point
1235}
1236
1237fn previous_word_end(
1238 map: &DisplaySnapshot,
1239 point: DisplayPoint,
1240 ignore_punctuation: bool,
1241 times: usize,
1242) -> DisplayPoint {
1243 let classifier = map
1244 .buffer_snapshot
1245 .char_classifier_at(point.to_point(map))
1246 .ignore_punctuation(ignore_punctuation);
1247 let mut point = point.to_point(map);
1248
1249 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1250 point.column += 1;
1251 }
1252 for _ in 0..times {
1253 let new_point = movement::find_preceding_boundary_point(
1254 &map.buffer_snapshot,
1255 point,
1256 FindRange::MultiLine,
1257 |left, right| {
1258 let left_kind = classifier.kind(left);
1259 let right_kind = classifier.kind(right);
1260 match (left_kind, right_kind) {
1261 (CharKind::Punctuation, CharKind::Whitespace)
1262 | (CharKind::Punctuation, CharKind::Word)
1263 | (CharKind::Word, CharKind::Whitespace)
1264 | (CharKind::Word, CharKind::Punctuation) => true,
1265 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1266 _ => false,
1267 }
1268 },
1269 );
1270 if new_point == point {
1271 break;
1272 }
1273 point = new_point;
1274 }
1275 movement::saturating_left(map, point.to_display_point(map))
1276}
1277
1278fn next_subword_start(
1279 map: &DisplaySnapshot,
1280 mut point: DisplayPoint,
1281 ignore_punctuation: bool,
1282 times: usize,
1283) -> DisplayPoint {
1284 let classifier = map
1285 .buffer_snapshot
1286 .char_classifier_at(point.to_point(map))
1287 .ignore_punctuation(ignore_punctuation);
1288 for _ in 0..times {
1289 let mut crossed_newline = false;
1290 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1291 let left_kind = classifier.kind(left);
1292 let right_kind = classifier.kind(right);
1293 let at_newline = right == '\n';
1294
1295 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1296 let is_subword_start =
1297 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1298
1299 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1300 || at_newline && crossed_newline
1301 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1302
1303 crossed_newline |= at_newline;
1304 found
1305 });
1306 if point == new_point {
1307 break;
1308 }
1309 point = new_point;
1310 }
1311 point
1312}
1313
1314pub(crate) fn next_subword_end(
1315 map: &DisplaySnapshot,
1316 mut point: DisplayPoint,
1317 ignore_punctuation: bool,
1318 times: usize,
1319 allow_cross_newline: bool,
1320) -> DisplayPoint {
1321 let classifier = map
1322 .buffer_snapshot
1323 .char_classifier_at(point.to_point(map))
1324 .ignore_punctuation(ignore_punctuation);
1325 for _ in 0..times {
1326 let new_point = next_char(map, point, allow_cross_newline);
1327
1328 let mut crossed_newline = false;
1329 let mut need_backtrack = false;
1330 let new_point =
1331 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1332 let left_kind = classifier.kind(left);
1333 let right_kind = classifier.kind(right);
1334 let at_newline = right == '\n';
1335
1336 if !allow_cross_newline && at_newline {
1337 return true;
1338 }
1339
1340 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1341 let is_subword_end =
1342 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1343
1344 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1345
1346 if found && (is_word_end || is_subword_end) {
1347 need_backtrack = true;
1348 }
1349
1350 crossed_newline |= at_newline;
1351 found
1352 });
1353 let mut new_point = map.clip_point(new_point, Bias::Left);
1354 if need_backtrack {
1355 *new_point.column_mut() -= 1;
1356 }
1357 if point == new_point {
1358 break;
1359 }
1360 point = new_point;
1361 }
1362 point
1363}
1364
1365fn previous_subword_start(
1366 map: &DisplaySnapshot,
1367 mut point: DisplayPoint,
1368 ignore_punctuation: bool,
1369 times: usize,
1370) -> DisplayPoint {
1371 let classifier = map
1372 .buffer_snapshot
1373 .char_classifier_at(point.to_point(map))
1374 .ignore_punctuation(ignore_punctuation);
1375 for _ in 0..times {
1376 let mut crossed_newline = false;
1377 // This works even though find_preceding_boundary is called for every character in the line containing
1378 // cursor because the newline is checked only once.
1379 let new_point = movement::find_preceding_boundary_display_point(
1380 map,
1381 point,
1382 FindRange::MultiLine,
1383 |left, right| {
1384 let left_kind = classifier.kind(left);
1385 let right_kind = classifier.kind(right);
1386 let at_newline = right == '\n';
1387
1388 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1389 let is_subword_start =
1390 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1391
1392 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1393 || at_newline && crossed_newline
1394 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1395
1396 crossed_newline |= at_newline;
1397
1398 found
1399 },
1400 );
1401 if point == new_point {
1402 break;
1403 }
1404 point = new_point;
1405 }
1406 point
1407}
1408
1409fn previous_subword_end(
1410 map: &DisplaySnapshot,
1411 point: DisplayPoint,
1412 ignore_punctuation: bool,
1413 times: usize,
1414) -> DisplayPoint {
1415 let classifier = map
1416 .buffer_snapshot
1417 .char_classifier_at(point.to_point(map))
1418 .ignore_punctuation(ignore_punctuation);
1419 let mut point = point.to_point(map);
1420
1421 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1422 point.column += 1;
1423 }
1424 for _ in 0..times {
1425 let new_point = movement::find_preceding_boundary_point(
1426 &map.buffer_snapshot,
1427 point,
1428 FindRange::MultiLine,
1429 |left, right| {
1430 let left_kind = classifier.kind(left);
1431 let right_kind = classifier.kind(right);
1432
1433 let is_subword_end =
1434 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1435
1436 if is_subword_end {
1437 return true;
1438 }
1439
1440 match (left_kind, right_kind) {
1441 (CharKind::Word, CharKind::Whitespace)
1442 | (CharKind::Word, CharKind::Punctuation) => true,
1443 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1444 _ => false,
1445 }
1446 },
1447 );
1448 if new_point == point {
1449 break;
1450 }
1451 point = new_point;
1452 }
1453 movement::saturating_left(map, point.to_display_point(map))
1454}
1455
1456pub(crate) fn first_non_whitespace(
1457 map: &DisplaySnapshot,
1458 display_lines: bool,
1459 from: DisplayPoint,
1460) -> DisplayPoint {
1461 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1462 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1463 for (ch, offset) in map.buffer_chars_at(start_offset) {
1464 if ch == '\n' {
1465 return from;
1466 }
1467
1468 start_offset = offset;
1469
1470 if classifier.kind(ch) != CharKind::Whitespace {
1471 break;
1472 }
1473 }
1474
1475 start_offset.to_display_point(map)
1476}
1477
1478pub(crate) fn last_non_whitespace(
1479 map: &DisplaySnapshot,
1480 from: DisplayPoint,
1481 count: usize,
1482) -> DisplayPoint {
1483 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1484 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1485
1486 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1487 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1488 if classifier.kind(ch) != CharKind::Whitespace {
1489 return end_of_line.to_display_point(map);
1490 }
1491 }
1492
1493 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1494 if ch == '\n' {
1495 break;
1496 }
1497 end_of_line = offset;
1498 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1499 break;
1500 }
1501 }
1502
1503 end_of_line.to_display_point(map)
1504}
1505
1506pub(crate) fn start_of_line(
1507 map: &DisplaySnapshot,
1508 display_lines: bool,
1509 point: DisplayPoint,
1510) -> DisplayPoint {
1511 if display_lines {
1512 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1513 } else {
1514 map.prev_line_boundary(point.to_point(map)).1
1515 }
1516}
1517
1518pub(crate) fn end_of_line(
1519 map: &DisplaySnapshot,
1520 display_lines: bool,
1521 mut point: DisplayPoint,
1522 times: usize,
1523) -> DisplayPoint {
1524 if times > 1 {
1525 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1526 }
1527 if display_lines {
1528 map.clip_point(
1529 DisplayPoint::new(point.row(), map.line_len(point.row())),
1530 Bias::Left,
1531 )
1532 } else {
1533 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1534 }
1535}
1536
1537fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1538 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1539 *new_point.column_mut() = point.column();
1540 map.clip_point(new_point, Bias::Left)
1541}
1542
1543fn end_of_document(
1544 map: &DisplaySnapshot,
1545 point: DisplayPoint,
1546 line: Option<usize>,
1547) -> DisplayPoint {
1548 let new_row = if let Some(line) = line {
1549 (line - 1) as u32
1550 } else {
1551 map.max_buffer_row().0
1552 };
1553
1554 let new_point = Point::new(new_row, point.column());
1555 map.clip_point(new_point.to_display_point(map), Bias::Left)
1556}
1557
1558fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1559 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1560 let display_point = map.clip_at_line_end(display_point);
1561 let point = display_point.to_point(map);
1562 let offset = point.to_offset(&map.buffer_snapshot);
1563
1564 // Ensure the range is contained by the current line.
1565 let mut line_end = map.next_line_boundary(point).0;
1566 if line_end == point {
1567 line_end = map.max_point().to_point(map);
1568 }
1569
1570 let line_range = map.prev_line_boundary(point).0..line_end;
1571 let visible_line_range =
1572 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1573 let ranges = map
1574 .buffer_snapshot
1575 .bracket_ranges(visible_line_range.clone());
1576 if let Some(ranges) = ranges {
1577 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1578 ..line_range.end.to_offset(&map.buffer_snapshot);
1579 let mut closest_pair_destination = None;
1580 let mut closest_distance = usize::MAX;
1581
1582 for (open_range, close_range) in ranges {
1583 if open_range.start >= offset && line_range.contains(&open_range.start) {
1584 let distance = open_range.start - offset;
1585 if distance < closest_distance {
1586 closest_pair_destination = Some(close_range.start);
1587 closest_distance = distance;
1588 continue;
1589 }
1590 }
1591
1592 if close_range.start >= offset && line_range.contains(&close_range.start) {
1593 let distance = close_range.start - offset;
1594 if distance < closest_distance {
1595 closest_pair_destination = Some(open_range.start);
1596 closest_distance = distance;
1597 continue;
1598 }
1599 }
1600
1601 continue;
1602 }
1603
1604 closest_pair_destination
1605 .map(|destination| destination.to_display_point(map))
1606 .unwrap_or(display_point)
1607 } else {
1608 display_point
1609 }
1610}
1611
1612fn find_forward(
1613 map: &DisplaySnapshot,
1614 from: DisplayPoint,
1615 before: bool,
1616 target: char,
1617 times: usize,
1618 mode: FindRange,
1619 smartcase: bool,
1620) -> Option<DisplayPoint> {
1621 let mut to = from;
1622 let mut found = false;
1623
1624 for _ in 0..times {
1625 found = false;
1626 let new_to = find_boundary(map, to, mode, |_, right| {
1627 found = is_character_match(target, right, smartcase);
1628 found
1629 });
1630 if to == new_to {
1631 break;
1632 }
1633 to = new_to;
1634 }
1635
1636 if found {
1637 if before && to.column() > 0 {
1638 *to.column_mut() -= 1;
1639 Some(map.clip_point(to, Bias::Left))
1640 } else {
1641 Some(to)
1642 }
1643 } else {
1644 None
1645 }
1646}
1647
1648fn find_backward(
1649 map: &DisplaySnapshot,
1650 from: DisplayPoint,
1651 after: bool,
1652 target: char,
1653 times: usize,
1654 mode: FindRange,
1655 smartcase: bool,
1656) -> DisplayPoint {
1657 let mut to = from;
1658
1659 for _ in 0..times {
1660 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1661 is_character_match(target, right, smartcase)
1662 });
1663 if to == new_to {
1664 break;
1665 }
1666 to = new_to;
1667 }
1668
1669 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1670 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1671 if after {
1672 *to.column_mut() += 1;
1673 map.clip_point(to, Bias::Right)
1674 } else {
1675 to
1676 }
1677 } else {
1678 from
1679 }
1680}
1681
1682fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1683 if smartcase {
1684 if target.is_uppercase() {
1685 target == other
1686 } else {
1687 target == other.to_ascii_lowercase()
1688 }
1689 } else {
1690 target == other
1691 }
1692}
1693
1694fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1695 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1696 first_non_whitespace(map, false, correct_line)
1697}
1698
1699fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1700 let correct_line = start_of_relative_buffer_row(map, point, (times as isize) * -1);
1701 first_non_whitespace(map, false, correct_line)
1702}
1703
1704fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1705 let correct_line = start_of_relative_buffer_row(map, point, 0);
1706 right(map, correct_line, times.saturating_sub(1))
1707}
1708
1709pub(crate) fn next_line_end(
1710 map: &DisplaySnapshot,
1711 mut point: DisplayPoint,
1712 times: usize,
1713) -> DisplayPoint {
1714 if times > 1 {
1715 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1716 }
1717 end_of_line(map, false, point, 1)
1718}
1719
1720fn window_top(
1721 map: &DisplaySnapshot,
1722 point: DisplayPoint,
1723 text_layout_details: &TextLayoutDetails,
1724 mut times: usize,
1725) -> (DisplayPoint, SelectionGoal) {
1726 let first_visible_line = text_layout_details
1727 .scroll_anchor
1728 .anchor
1729 .to_display_point(map);
1730
1731 if first_visible_line.row() != DisplayRow(0)
1732 && text_layout_details.vertical_scroll_margin as usize > times
1733 {
1734 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1735 }
1736
1737 if let Some(visible_rows) = text_layout_details.visible_rows {
1738 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
1739 let new_row = (first_visible_line.row().0 + (times as u32))
1740 .min(bottom_row)
1741 .min(map.max_point().row().0);
1742 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1743
1744 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
1745 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1746 } else {
1747 let new_row =
1748 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
1749 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1750
1751 let new_point = DisplayPoint::new(new_row, new_col);
1752 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1753 }
1754}
1755
1756fn window_middle(
1757 map: &DisplaySnapshot,
1758 point: DisplayPoint,
1759 text_layout_details: &TextLayoutDetails,
1760) -> (DisplayPoint, SelectionGoal) {
1761 if let Some(visible_rows) = text_layout_details.visible_rows {
1762 let first_visible_line = text_layout_details
1763 .scroll_anchor
1764 .anchor
1765 .to_display_point(map);
1766
1767 let max_visible_rows =
1768 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
1769
1770 let new_row =
1771 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
1772 let new_row = DisplayRow(new_row);
1773 let new_col = point.column().min(map.line_len(new_row));
1774 let new_point = DisplayPoint::new(new_row, new_col);
1775 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1776 } else {
1777 (point, SelectionGoal::None)
1778 }
1779}
1780
1781fn window_bottom(
1782 map: &DisplaySnapshot,
1783 point: DisplayPoint,
1784 text_layout_details: &TextLayoutDetails,
1785 mut times: usize,
1786) -> (DisplayPoint, SelectionGoal) {
1787 if let Some(visible_rows) = text_layout_details.visible_rows {
1788 let first_visible_line = text_layout_details
1789 .scroll_anchor
1790 .anchor
1791 .to_display_point(map);
1792 let bottom_row = first_visible_line.row().0
1793 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1794 if bottom_row < map.max_point().row().0
1795 && text_layout_details.vertical_scroll_margin as usize > times
1796 {
1797 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1798 }
1799 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
1800 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
1801 {
1802 first_visible_line.row()
1803 } else {
1804 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
1805 };
1806 let new_col = point.column().min(map.line_len(new_row));
1807 let new_point = DisplayPoint::new(new_row, new_col);
1808 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1809 } else {
1810 (point, SelectionGoal::None)
1811 }
1812}
1813
1814#[cfg(test)]
1815mod test {
1816
1817 use crate::test::NeovimBackedTestContext;
1818 use indoc::indoc;
1819
1820 #[gpui::test]
1821 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1822 let mut cx = NeovimBackedTestContext::new(cx).await;
1823
1824 let initial_state = indoc! {r"ˇabc
1825 def
1826
1827 paragraph
1828 the second
1829
1830
1831
1832 third and
1833 final"};
1834
1835 // goes down once
1836 cx.set_shared_state(initial_state).await;
1837 cx.simulate_shared_keystrokes("}").await;
1838 cx.shared_state().await.assert_eq(indoc! {r"abc
1839 def
1840 ˇ
1841 paragraph
1842 the second
1843
1844
1845
1846 third and
1847 final"});
1848
1849 // goes up once
1850 cx.simulate_shared_keystrokes("{").await;
1851 cx.shared_state().await.assert_eq(initial_state);
1852
1853 // goes down twice
1854 cx.simulate_shared_keystrokes("2 }").await;
1855 cx.shared_state().await.assert_eq(indoc! {r"abc
1856 def
1857
1858 paragraph
1859 the second
1860 ˇ
1861
1862
1863 third and
1864 final"});
1865
1866 // goes down over multiple blanks
1867 cx.simulate_shared_keystrokes("}").await;
1868 cx.shared_state().await.assert_eq(indoc! {r"abc
1869 def
1870
1871 paragraph
1872 the second
1873
1874
1875
1876 third and
1877 finaˇl"});
1878
1879 // goes up twice
1880 cx.simulate_shared_keystrokes("2 {").await;
1881 cx.shared_state().await.assert_eq(indoc! {r"abc
1882 def
1883 ˇ
1884 paragraph
1885 the second
1886
1887
1888
1889 third and
1890 final"});
1891 }
1892
1893 #[gpui::test]
1894 async fn test_matching(cx: &mut gpui::TestAppContext) {
1895 let mut cx = NeovimBackedTestContext::new(cx).await;
1896
1897 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1898 do(something(with<Types>.and_arrays[0, 2]))
1899 }"})
1900 .await;
1901 cx.simulate_shared_keystrokes("%").await;
1902 cx.shared_state()
1903 .await
1904 .assert_eq(indoc! {r"func (a stringˇ) {
1905 do(something(with<Types>.and_arrays[0, 2]))
1906 }"});
1907
1908 // test it works on the last character of the line
1909 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1910 do(something(with<Types>.and_arrays[0, 2]))
1911 }"})
1912 .await;
1913 cx.simulate_shared_keystrokes("%").await;
1914 cx.shared_state()
1915 .await
1916 .assert_eq(indoc! {r"func (a string) {
1917 do(something(with<Types>.and_arrays[0, 2]))
1918 ˇ}"});
1919
1920 // test it works on immediate nesting
1921 cx.set_shared_state("ˇ{()}").await;
1922 cx.simulate_shared_keystrokes("%").await;
1923 cx.shared_state().await.assert_eq("{()ˇ}");
1924 cx.simulate_shared_keystrokes("%").await;
1925 cx.shared_state().await.assert_eq("ˇ{()}");
1926
1927 // test it works on immediate nesting inside braces
1928 cx.set_shared_state("{\n ˇ{()}\n}").await;
1929 cx.simulate_shared_keystrokes("%").await;
1930 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
1931
1932 // test it jumps to the next paren on a line
1933 cx.set_shared_state("func ˇboop() {\n}").await;
1934 cx.simulate_shared_keystrokes("%").await;
1935 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
1936 }
1937
1938 #[gpui::test]
1939 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1940 let mut cx = NeovimBackedTestContext::new(cx).await;
1941
1942 // f and F
1943 cx.set_shared_state("ˇone two three four").await;
1944 cx.simulate_shared_keystrokes("f o").await;
1945 cx.shared_state().await.assert_eq("one twˇo three four");
1946 cx.simulate_shared_keystrokes(",").await;
1947 cx.shared_state().await.assert_eq("ˇone two three four");
1948 cx.simulate_shared_keystrokes("2 ;").await;
1949 cx.shared_state().await.assert_eq("one two three fˇour");
1950 cx.simulate_shared_keystrokes("shift-f e").await;
1951 cx.shared_state().await.assert_eq("one two threˇe four");
1952 cx.simulate_shared_keystrokes("2 ;").await;
1953 cx.shared_state().await.assert_eq("onˇe two three four");
1954 cx.simulate_shared_keystrokes(",").await;
1955 cx.shared_state().await.assert_eq("one two thrˇee four");
1956
1957 // t and T
1958 cx.set_shared_state("ˇone two three four").await;
1959 cx.simulate_shared_keystrokes("t o").await;
1960 cx.shared_state().await.assert_eq("one tˇwo three four");
1961 cx.simulate_shared_keystrokes(",").await;
1962 cx.shared_state().await.assert_eq("oˇne two three four");
1963 cx.simulate_shared_keystrokes("2 ;").await;
1964 cx.shared_state().await.assert_eq("one two three ˇfour");
1965 cx.simulate_shared_keystrokes("shift-t e").await;
1966 cx.shared_state().await.assert_eq("one two threeˇ four");
1967 cx.simulate_shared_keystrokes("3 ;").await;
1968 cx.shared_state().await.assert_eq("oneˇ two three four");
1969 cx.simulate_shared_keystrokes(",").await;
1970 cx.shared_state().await.assert_eq("one two thˇree four");
1971 }
1972
1973 #[gpui::test]
1974 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1975 let mut cx = NeovimBackedTestContext::new(cx).await;
1976 let initial_state = indoc! {r"something(ˇfoo)"};
1977 cx.set_shared_state(initial_state).await;
1978 cx.simulate_shared_keystrokes("}").await;
1979 cx.shared_state().await.assert_eq("something(fooˇ)");
1980 }
1981
1982 #[gpui::test]
1983 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1984 let mut cx = NeovimBackedTestContext::new(cx).await;
1985 cx.set_shared_state("ˇone\n two\nthree").await;
1986 cx.simulate_shared_keystrokes("enter").await;
1987 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
1988 }
1989
1990 #[gpui::test]
1991 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1992 let mut cx = NeovimBackedTestContext::new(cx).await;
1993 cx.set_shared_state("ˇ one\n two \nthree").await;
1994 cx.simulate_shared_keystrokes("g _").await;
1995 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
1996
1997 cx.set_shared_state("ˇ one \n two \nthree").await;
1998 cx.simulate_shared_keystrokes("g _").await;
1999 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
2000 cx.simulate_shared_keystrokes("2 g _").await;
2001 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2002 }
2003
2004 #[gpui::test]
2005 async fn test_window_top(cx: &mut gpui::TestAppContext) {
2006 let mut cx = NeovimBackedTestContext::new(cx).await;
2007 let initial_state = indoc! {r"abc
2008 def
2009 paragraph
2010 the second
2011 third ˇand
2012 final"};
2013
2014 cx.set_shared_state(initial_state).await;
2015 cx.simulate_shared_keystrokes("shift-h").await;
2016 cx.shared_state().await.assert_eq(indoc! {r"abˇc
2017 def
2018 paragraph
2019 the second
2020 third and
2021 final"});
2022
2023 // clip point
2024 cx.set_shared_state(indoc! {r"
2025 1 2 3
2026 4 5 6
2027 7 8 ˇ9
2028 "})
2029 .await;
2030 cx.simulate_shared_keystrokes("shift-h").await;
2031 cx.shared_state().await.assert_eq(indoc! {"
2032 1 2 ˇ3
2033 4 5 6
2034 7 8 9
2035 "});
2036
2037 cx.set_shared_state(indoc! {r"
2038 1 2 3
2039 4 5 6
2040 ˇ7 8 9
2041 "})
2042 .await;
2043 cx.simulate_shared_keystrokes("shift-h").await;
2044 cx.shared_state().await.assert_eq(indoc! {"
2045 ˇ1 2 3
2046 4 5 6
2047 7 8 9
2048 "});
2049
2050 cx.set_shared_state(indoc! {r"
2051 1 2 3
2052 4 5 ˇ6
2053 7 8 9"})
2054 .await;
2055 cx.simulate_shared_keystrokes("9 shift-h").await;
2056 cx.shared_state().await.assert_eq(indoc! {"
2057 1 2 3
2058 4 5 6
2059 7 8 ˇ9"});
2060 }
2061
2062 #[gpui::test]
2063 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2064 let mut cx = NeovimBackedTestContext::new(cx).await;
2065 let initial_state = indoc! {r"abˇc
2066 def
2067 paragraph
2068 the second
2069 third and
2070 final"};
2071
2072 cx.set_shared_state(initial_state).await;
2073 cx.simulate_shared_keystrokes("shift-m").await;
2074 cx.shared_state().await.assert_eq(indoc! {r"abc
2075 def
2076 paˇragraph
2077 the second
2078 third and
2079 final"});
2080
2081 cx.set_shared_state(indoc! {r"
2082 1 2 3
2083 4 5 6
2084 7 8 ˇ9
2085 "})
2086 .await;
2087 cx.simulate_shared_keystrokes("shift-m").await;
2088 cx.shared_state().await.assert_eq(indoc! {"
2089 1 2 3
2090 4 5 ˇ6
2091 7 8 9
2092 "});
2093 cx.set_shared_state(indoc! {r"
2094 1 2 3
2095 4 5 6
2096 ˇ7 8 9
2097 "})
2098 .await;
2099 cx.simulate_shared_keystrokes("shift-m").await;
2100 cx.shared_state().await.assert_eq(indoc! {"
2101 1 2 3
2102 ˇ4 5 6
2103 7 8 9
2104 "});
2105 cx.set_shared_state(indoc! {r"
2106 ˇ1 2 3
2107 4 5 6
2108 7 8 9
2109 "})
2110 .await;
2111 cx.simulate_shared_keystrokes("shift-m").await;
2112 cx.shared_state().await.assert_eq(indoc! {"
2113 1 2 3
2114 ˇ4 5 6
2115 7 8 9
2116 "});
2117 cx.set_shared_state(indoc! {r"
2118 1 2 3
2119 ˇ4 5 6
2120 7 8 9
2121 "})
2122 .await;
2123 cx.simulate_shared_keystrokes("shift-m").await;
2124 cx.shared_state().await.assert_eq(indoc! {"
2125 1 2 3
2126 ˇ4 5 6
2127 7 8 9
2128 "});
2129 cx.set_shared_state(indoc! {r"
2130 1 2 3
2131 4 5 ˇ6
2132 7 8 9
2133 "})
2134 .await;
2135 cx.simulate_shared_keystrokes("shift-m").await;
2136 cx.shared_state().await.assert_eq(indoc! {"
2137 1 2 3
2138 4 5 ˇ6
2139 7 8 9
2140 "});
2141 }
2142
2143 #[gpui::test]
2144 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2145 let mut cx = NeovimBackedTestContext::new(cx).await;
2146 let initial_state = indoc! {r"abc
2147 deˇf
2148 paragraph
2149 the second
2150 third and
2151 final"};
2152
2153 cx.set_shared_state(initial_state).await;
2154 cx.simulate_shared_keystrokes("shift-l").await;
2155 cx.shared_state().await.assert_eq(indoc! {r"abc
2156 def
2157 paragraph
2158 the second
2159 third and
2160 fiˇnal"});
2161
2162 cx.set_shared_state(indoc! {r"
2163 1 2 3
2164 4 5 ˇ6
2165 7 8 9
2166 "})
2167 .await;
2168 cx.simulate_shared_keystrokes("shift-l").await;
2169 cx.shared_state().await.assert_eq(indoc! {"
2170 1 2 3
2171 4 5 6
2172 7 8 9
2173 ˇ"});
2174
2175 cx.set_shared_state(indoc! {r"
2176 1 2 3
2177 ˇ4 5 6
2178 7 8 9
2179 "})
2180 .await;
2181 cx.simulate_shared_keystrokes("shift-l").await;
2182 cx.shared_state().await.assert_eq(indoc! {"
2183 1 2 3
2184 4 5 6
2185 7 8 9
2186 ˇ"});
2187
2188 cx.set_shared_state(indoc! {r"
2189 1 2 ˇ3
2190 4 5 6
2191 7 8 9
2192 "})
2193 .await;
2194 cx.simulate_shared_keystrokes("shift-l").await;
2195 cx.shared_state().await.assert_eq(indoc! {"
2196 1 2 3
2197 4 5 6
2198 7 8 9
2199 ˇ"});
2200
2201 cx.set_shared_state(indoc! {r"
2202 ˇ1 2 3
2203 4 5 6
2204 7 8 9
2205 "})
2206 .await;
2207 cx.simulate_shared_keystrokes("shift-l").await;
2208 cx.shared_state().await.assert_eq(indoc! {"
2209 1 2 3
2210 4 5 6
2211 7 8 9
2212 ˇ"});
2213
2214 cx.set_shared_state(indoc! {r"
2215 1 2 3
2216 4 5 ˇ6
2217 7 8 9
2218 "})
2219 .await;
2220 cx.simulate_shared_keystrokes("9 shift-l").await;
2221 cx.shared_state().await.assert_eq(indoc! {"
2222 1 2 ˇ3
2223 4 5 6
2224 7 8 9
2225 "});
2226 }
2227
2228 #[gpui::test]
2229 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2230 let mut cx = NeovimBackedTestContext::new(cx).await;
2231 cx.set_shared_state(indoc! {r"
2232 456 5ˇ67 678
2233 "})
2234 .await;
2235 cx.simulate_shared_keystrokes("g e").await;
2236 cx.shared_state().await.assert_eq(indoc! {"
2237 45ˇ6 567 678
2238 "});
2239
2240 // Test times
2241 cx.set_shared_state(indoc! {r"
2242 123 234 345
2243 456 5ˇ67 678
2244 "})
2245 .await;
2246 cx.simulate_shared_keystrokes("4 g e").await;
2247 cx.shared_state().await.assert_eq(indoc! {"
2248 12ˇ3 234 345
2249 456 567 678
2250 "});
2251
2252 // With punctuation
2253 cx.set_shared_state(indoc! {r"
2254 123 234 345
2255 4;5.6 5ˇ67 678
2256 789 890 901
2257 "})
2258 .await;
2259 cx.simulate_shared_keystrokes("g e").await;
2260 cx.shared_state().await.assert_eq(indoc! {"
2261 123 234 345
2262 4;5.ˇ6 567 678
2263 789 890 901
2264 "});
2265
2266 // With punctuation and count
2267 cx.set_shared_state(indoc! {r"
2268 123 234 345
2269 4;5.6 5ˇ67 678
2270 789 890 901
2271 "})
2272 .await;
2273 cx.simulate_shared_keystrokes("5 g e").await;
2274 cx.shared_state().await.assert_eq(indoc! {"
2275 123 234 345
2276 ˇ4;5.6 567 678
2277 789 890 901
2278 "});
2279
2280 // newlines
2281 cx.set_shared_state(indoc! {r"
2282 123 234 345
2283
2284 78ˇ9 890 901
2285 "})
2286 .await;
2287 cx.simulate_shared_keystrokes("g e").await;
2288 cx.shared_state().await.assert_eq(indoc! {"
2289 123 234 345
2290 ˇ
2291 789 890 901
2292 "});
2293 cx.simulate_shared_keystrokes("g e").await;
2294 cx.shared_state().await.assert_eq(indoc! {"
2295 123 234 34ˇ5
2296
2297 789 890 901
2298 "});
2299
2300 // With punctuation
2301 cx.set_shared_state(indoc! {r"
2302 123 234 345
2303 4;5.ˇ6 567 678
2304 789 890 901
2305 "})
2306 .await;
2307 cx.simulate_shared_keystrokes("g shift-e").await;
2308 cx.shared_state().await.assert_eq(indoc! {"
2309 123 234 34ˇ5
2310 4;5.6 567 678
2311 789 890 901
2312 "});
2313 }
2314
2315 #[gpui::test]
2316 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2317 let mut cx = NeovimBackedTestContext::new(cx).await;
2318
2319 cx.set_shared_state(indoc! {"
2320 fn aˇ() {
2321 return
2322 }
2323 "})
2324 .await;
2325 cx.simulate_shared_keystrokes("v $ %").await;
2326 cx.shared_state().await.assert_eq(indoc! {"
2327 fn a«() {
2328 return
2329 }ˇ»
2330 "});
2331 }
2332}