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::{char_kind, 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 scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1135 for _ in 0..times {
1136 let mut crossed_newline = false;
1137 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1138 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1139 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1140 let at_newline = right == '\n';
1141
1142 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1143 || at_newline && crossed_newline
1144 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1145
1146 crossed_newline |= at_newline;
1147 found
1148 });
1149 if point == new_point {
1150 break;
1151 }
1152 point = new_point;
1153 }
1154 point
1155}
1156
1157pub(crate) fn next_word_end(
1158 map: &DisplaySnapshot,
1159 mut point: DisplayPoint,
1160 ignore_punctuation: bool,
1161 times: usize,
1162 allow_cross_newline: bool,
1163) -> DisplayPoint {
1164 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1165 for _ in 0..times {
1166 let new_point = next_char(map, point, allow_cross_newline);
1167 let mut need_next_char = false;
1168 let new_point = movement::find_boundary_exclusive(
1169 map,
1170 new_point,
1171 FindRange::MultiLine,
1172 |left, right| {
1173 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1174 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1175 let at_newline = right == '\n';
1176
1177 if !allow_cross_newline && at_newline {
1178 need_next_char = true;
1179 return true;
1180 }
1181
1182 left_kind != right_kind && left_kind != CharKind::Whitespace
1183 },
1184 );
1185 let new_point = if need_next_char {
1186 next_char(map, new_point, true)
1187 } else {
1188 new_point
1189 };
1190 let new_point = map.clip_point(new_point, Bias::Left);
1191 if point == new_point {
1192 break;
1193 }
1194 point = new_point;
1195 }
1196 point
1197}
1198
1199fn previous_word_start(
1200 map: &DisplaySnapshot,
1201 mut point: DisplayPoint,
1202 ignore_punctuation: bool,
1203 times: usize,
1204) -> DisplayPoint {
1205 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1206 for _ in 0..times {
1207 // This works even though find_preceding_boundary is called for every character in the line containing
1208 // cursor because the newline is checked only once.
1209 let new_point = movement::find_preceding_boundary_display_point(
1210 map,
1211 point,
1212 FindRange::MultiLine,
1213 |left, right| {
1214 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1215 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1216
1217 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1218 },
1219 );
1220 if point == new_point {
1221 break;
1222 }
1223 point = new_point;
1224 }
1225 point
1226}
1227
1228fn previous_word_end(
1229 map: &DisplaySnapshot,
1230 point: DisplayPoint,
1231 ignore_punctuation: bool,
1232 times: usize,
1233) -> DisplayPoint {
1234 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1235 let mut point = point.to_point(map);
1236
1237 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1238 point.column += 1;
1239 }
1240 for _ in 0..times {
1241 let new_point = movement::find_preceding_boundary_point(
1242 &map.buffer_snapshot,
1243 point,
1244 FindRange::MultiLine,
1245 |left, right| {
1246 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1247 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1248 match (left_kind, right_kind) {
1249 (CharKind::Punctuation, CharKind::Whitespace)
1250 | (CharKind::Punctuation, CharKind::Word)
1251 | (CharKind::Word, CharKind::Whitespace)
1252 | (CharKind::Word, CharKind::Punctuation) => true,
1253 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1254 _ => false,
1255 }
1256 },
1257 );
1258 if new_point == point {
1259 break;
1260 }
1261 point = new_point;
1262 }
1263 movement::saturating_left(map, point.to_display_point(map))
1264}
1265
1266fn next_subword_start(
1267 map: &DisplaySnapshot,
1268 mut point: DisplayPoint,
1269 ignore_punctuation: bool,
1270 times: usize,
1271) -> DisplayPoint {
1272 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1273 for _ in 0..times {
1274 let mut crossed_newline = false;
1275 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1276 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1277 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1278 let at_newline = right == '\n';
1279
1280 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1281 let is_subword_start =
1282 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1283
1284 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1285 || at_newline && crossed_newline
1286 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1287
1288 crossed_newline |= at_newline;
1289 found
1290 });
1291 if point == new_point {
1292 break;
1293 }
1294 point = new_point;
1295 }
1296 point
1297}
1298
1299pub(crate) fn next_subword_end(
1300 map: &DisplaySnapshot,
1301 mut point: DisplayPoint,
1302 ignore_punctuation: bool,
1303 times: usize,
1304 allow_cross_newline: bool,
1305) -> DisplayPoint {
1306 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1307 for _ in 0..times {
1308 let new_point = next_char(map, point, allow_cross_newline);
1309
1310 let mut crossed_newline = false;
1311 let mut need_backtrack = false;
1312 let new_point =
1313 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1314 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1315 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1316 let at_newline = right == '\n';
1317
1318 if !allow_cross_newline && at_newline {
1319 return true;
1320 }
1321
1322 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1323 let is_subword_end =
1324 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1325
1326 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1327
1328 if found && (is_word_end || is_subword_end) {
1329 need_backtrack = true;
1330 }
1331
1332 crossed_newline |= at_newline;
1333 found
1334 });
1335 let mut new_point = map.clip_point(new_point, Bias::Left);
1336 if need_backtrack {
1337 *new_point.column_mut() -= 1;
1338 }
1339 if point == new_point {
1340 break;
1341 }
1342 point = new_point;
1343 }
1344 point
1345}
1346
1347fn previous_subword_start(
1348 map: &DisplaySnapshot,
1349 mut point: DisplayPoint,
1350 ignore_punctuation: bool,
1351 times: usize,
1352) -> DisplayPoint {
1353 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1354 for _ in 0..times {
1355 let mut crossed_newline = false;
1356 // This works even though find_preceding_boundary is called for every character in the line containing
1357 // cursor because the newline is checked only once.
1358 let new_point = movement::find_preceding_boundary_display_point(
1359 map,
1360 point,
1361 FindRange::MultiLine,
1362 |left, right| {
1363 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1364 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1365 let at_newline = right == '\n';
1366
1367 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1368 let is_subword_start =
1369 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1370
1371 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1372 || at_newline && crossed_newline
1373 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1374
1375 crossed_newline |= at_newline;
1376
1377 found
1378 },
1379 );
1380 if point == new_point {
1381 break;
1382 }
1383 point = new_point;
1384 }
1385 point
1386}
1387
1388fn previous_subword_end(
1389 map: &DisplaySnapshot,
1390 point: DisplayPoint,
1391 ignore_punctuation: bool,
1392 times: usize,
1393) -> DisplayPoint {
1394 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
1395 let mut point = point.to_point(map);
1396
1397 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1398 point.column += 1;
1399 }
1400 for _ in 0..times {
1401 let new_point = movement::find_preceding_boundary_point(
1402 &map.buffer_snapshot,
1403 point,
1404 FindRange::MultiLine,
1405 |left, right| {
1406 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
1407 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
1408
1409 let is_subword_end =
1410 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1411
1412 if is_subword_end {
1413 return true;
1414 }
1415
1416 match (left_kind, right_kind) {
1417 (CharKind::Word, CharKind::Whitespace)
1418 | (CharKind::Word, CharKind::Punctuation) => true,
1419 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1420 _ => false,
1421 }
1422 },
1423 );
1424 if new_point == point {
1425 break;
1426 }
1427 point = new_point;
1428 }
1429 movement::saturating_left(map, point.to_display_point(map))
1430}
1431
1432pub(crate) fn first_non_whitespace(
1433 map: &DisplaySnapshot,
1434 display_lines: bool,
1435 from: DisplayPoint,
1436) -> DisplayPoint {
1437 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1438 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1439 for (ch, offset) in map.buffer_chars_at(start_offset) {
1440 if ch == '\n' {
1441 return from;
1442 }
1443
1444 start_offset = offset;
1445
1446 if char_kind(&scope, ch) != CharKind::Whitespace {
1447 break;
1448 }
1449 }
1450
1451 start_offset.to_display_point(map)
1452}
1453
1454pub(crate) fn last_non_whitespace(
1455 map: &DisplaySnapshot,
1456 from: DisplayPoint,
1457 count: usize,
1458) -> DisplayPoint {
1459 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1460 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
1461
1462 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1463 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1464 if char_kind(&scope, ch) != CharKind::Whitespace {
1465 return end_of_line.to_display_point(map);
1466 }
1467 }
1468
1469 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1470 if ch == '\n' {
1471 break;
1472 }
1473 end_of_line = offset;
1474 if char_kind(&scope, ch) != CharKind::Whitespace || ch == '\n' {
1475 break;
1476 }
1477 }
1478
1479 end_of_line.to_display_point(map)
1480}
1481
1482pub(crate) fn start_of_line(
1483 map: &DisplaySnapshot,
1484 display_lines: bool,
1485 point: DisplayPoint,
1486) -> DisplayPoint {
1487 if display_lines {
1488 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1489 } else {
1490 map.prev_line_boundary(point.to_point(map)).1
1491 }
1492}
1493
1494pub(crate) fn end_of_line(
1495 map: &DisplaySnapshot,
1496 display_lines: bool,
1497 mut point: DisplayPoint,
1498 times: usize,
1499) -> DisplayPoint {
1500 if times > 1 {
1501 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1502 }
1503 if display_lines {
1504 map.clip_point(
1505 DisplayPoint::new(point.row(), map.line_len(point.row())),
1506 Bias::Left,
1507 )
1508 } else {
1509 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1510 }
1511}
1512
1513fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
1514 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
1515 *new_point.column_mut() = point.column();
1516 map.clip_point(new_point, Bias::Left)
1517}
1518
1519fn end_of_document(
1520 map: &DisplaySnapshot,
1521 point: DisplayPoint,
1522 line: Option<usize>,
1523) -> DisplayPoint {
1524 let new_row = if let Some(line) = line {
1525 (line - 1) as u32
1526 } else {
1527 map.max_buffer_row().0
1528 };
1529
1530 let new_point = Point::new(new_row, point.column());
1531 map.clip_point(new_point.to_display_point(map), Bias::Left)
1532}
1533
1534fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1535 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1536 let display_point = map.clip_at_line_end(display_point);
1537 let point = display_point.to_point(map);
1538 let offset = point.to_offset(&map.buffer_snapshot);
1539
1540 // Ensure the range is contained by the current line.
1541 let mut line_end = map.next_line_boundary(point).0;
1542 if line_end == point {
1543 line_end = map.max_point().to_point(map);
1544 }
1545
1546 let line_range = map.prev_line_boundary(point).0..line_end;
1547 let visible_line_range =
1548 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1549 let ranges = map
1550 .buffer_snapshot
1551 .bracket_ranges(visible_line_range.clone());
1552 if let Some(ranges) = ranges {
1553 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1554 ..line_range.end.to_offset(&map.buffer_snapshot);
1555 let mut closest_pair_destination = None;
1556 let mut closest_distance = usize::MAX;
1557
1558 for (open_range, close_range) in ranges {
1559 if open_range.start >= offset && line_range.contains(&open_range.start) {
1560 let distance = open_range.start - offset;
1561 if distance < closest_distance {
1562 closest_pair_destination = Some(close_range.start);
1563 closest_distance = distance;
1564 continue;
1565 }
1566 }
1567
1568 if close_range.start >= offset && line_range.contains(&close_range.start) {
1569 let distance = close_range.start - offset;
1570 if distance < closest_distance {
1571 closest_pair_destination = Some(open_range.start);
1572 closest_distance = distance;
1573 continue;
1574 }
1575 }
1576
1577 continue;
1578 }
1579
1580 closest_pair_destination
1581 .map(|destination| destination.to_display_point(map))
1582 .unwrap_or(display_point)
1583 } else {
1584 display_point
1585 }
1586}
1587
1588fn find_forward(
1589 map: &DisplaySnapshot,
1590 from: DisplayPoint,
1591 before: bool,
1592 target: char,
1593 times: usize,
1594 mode: FindRange,
1595 smartcase: bool,
1596) -> Option<DisplayPoint> {
1597 let mut to = from;
1598 let mut found = false;
1599
1600 for _ in 0..times {
1601 found = false;
1602 let new_to = find_boundary(map, to, mode, |_, right| {
1603 found = is_character_match(target, right, smartcase);
1604 found
1605 });
1606 if to == new_to {
1607 break;
1608 }
1609 to = new_to;
1610 }
1611
1612 if found {
1613 if before && to.column() > 0 {
1614 *to.column_mut() -= 1;
1615 Some(map.clip_point(to, Bias::Left))
1616 } else {
1617 Some(to)
1618 }
1619 } else {
1620 None
1621 }
1622}
1623
1624fn find_backward(
1625 map: &DisplaySnapshot,
1626 from: DisplayPoint,
1627 after: bool,
1628 target: char,
1629 times: usize,
1630 mode: FindRange,
1631 smartcase: bool,
1632) -> DisplayPoint {
1633 let mut to = from;
1634
1635 for _ in 0..times {
1636 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1637 is_character_match(target, right, smartcase)
1638 });
1639 if to == new_to {
1640 break;
1641 }
1642 to = new_to;
1643 }
1644
1645 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1646 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1647 if after {
1648 *to.column_mut() += 1;
1649 map.clip_point(to, Bias::Right)
1650 } else {
1651 to
1652 }
1653 } else {
1654 from
1655 }
1656}
1657
1658fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1659 if smartcase {
1660 if target.is_uppercase() {
1661 target == other
1662 } else {
1663 target == other.to_ascii_lowercase()
1664 }
1665 } else {
1666 target == other
1667 }
1668}
1669
1670fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1671 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1672 first_non_whitespace(map, false, correct_line)
1673}
1674
1675fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1676 let correct_line = start_of_relative_buffer_row(map, point, (times as isize) * -1);
1677 first_non_whitespace(map, false, correct_line)
1678}
1679
1680fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1681 let correct_line = start_of_relative_buffer_row(map, point, 0);
1682 right(map, correct_line, times.saturating_sub(1))
1683}
1684
1685pub(crate) fn next_line_end(
1686 map: &DisplaySnapshot,
1687 mut point: DisplayPoint,
1688 times: usize,
1689) -> DisplayPoint {
1690 if times > 1 {
1691 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1692 }
1693 end_of_line(map, false, point, 1)
1694}
1695
1696fn window_top(
1697 map: &DisplaySnapshot,
1698 point: DisplayPoint,
1699 text_layout_details: &TextLayoutDetails,
1700 mut times: usize,
1701) -> (DisplayPoint, SelectionGoal) {
1702 let first_visible_line = text_layout_details
1703 .scroll_anchor
1704 .anchor
1705 .to_display_point(map);
1706
1707 if first_visible_line.row() != DisplayRow(0)
1708 && text_layout_details.vertical_scroll_margin as usize > times
1709 {
1710 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1711 }
1712
1713 if let Some(visible_rows) = text_layout_details.visible_rows {
1714 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
1715 let new_row = (first_visible_line.row().0 + (times as u32))
1716 .min(bottom_row)
1717 .min(map.max_point().row().0);
1718 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1719
1720 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
1721 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1722 } else {
1723 let new_row =
1724 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
1725 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1726
1727 let new_point = DisplayPoint::new(new_row, new_col);
1728 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1729 }
1730}
1731
1732fn window_middle(
1733 map: &DisplaySnapshot,
1734 point: DisplayPoint,
1735 text_layout_details: &TextLayoutDetails,
1736) -> (DisplayPoint, SelectionGoal) {
1737 if let Some(visible_rows) = text_layout_details.visible_rows {
1738 let first_visible_line = text_layout_details
1739 .scroll_anchor
1740 .anchor
1741 .to_display_point(map);
1742
1743 let max_visible_rows =
1744 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
1745
1746 let new_row =
1747 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
1748 let new_row = DisplayRow(new_row);
1749 let new_col = point.column().min(map.line_len(new_row));
1750 let new_point = DisplayPoint::new(new_row, new_col);
1751 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1752 } else {
1753 (point, SelectionGoal::None)
1754 }
1755}
1756
1757fn window_bottom(
1758 map: &DisplaySnapshot,
1759 point: DisplayPoint,
1760 text_layout_details: &TextLayoutDetails,
1761 mut times: usize,
1762) -> (DisplayPoint, SelectionGoal) {
1763 if let Some(visible_rows) = text_layout_details.visible_rows {
1764 let first_visible_line = text_layout_details
1765 .scroll_anchor
1766 .anchor
1767 .to_display_point(map);
1768 let bottom_row = first_visible_line.row().0
1769 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1770 if bottom_row < map.max_point().row().0
1771 && text_layout_details.vertical_scroll_margin as usize > times
1772 {
1773 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1774 }
1775 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
1776 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
1777 {
1778 first_visible_line.row()
1779 } else {
1780 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
1781 };
1782 let new_col = point.column().min(map.line_len(new_row));
1783 let new_point = DisplayPoint::new(new_row, new_col);
1784 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1785 } else {
1786 (point, SelectionGoal::None)
1787 }
1788}
1789
1790pub fn coerce_punctuation(kind: CharKind, treat_punctuation_as_word: bool) -> CharKind {
1791 if treat_punctuation_as_word && kind == CharKind::Punctuation {
1792 CharKind::Word
1793 } else {
1794 kind
1795 }
1796}
1797
1798#[cfg(test)]
1799mod test {
1800
1801 use crate::test::NeovimBackedTestContext;
1802 use indoc::indoc;
1803
1804 #[gpui::test]
1805 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1806 let mut cx = NeovimBackedTestContext::new(cx).await;
1807
1808 let initial_state = indoc! {r"ˇabc
1809 def
1810
1811 paragraph
1812 the second
1813
1814
1815
1816 third and
1817 final"};
1818
1819 // goes down once
1820 cx.set_shared_state(initial_state).await;
1821 cx.simulate_shared_keystrokes("}").await;
1822 cx.shared_state().await.assert_eq(indoc! {r"abc
1823 def
1824 ˇ
1825 paragraph
1826 the second
1827
1828
1829
1830 third and
1831 final"});
1832
1833 // goes up once
1834 cx.simulate_shared_keystrokes("{").await;
1835 cx.shared_state().await.assert_eq(initial_state);
1836
1837 // goes down twice
1838 cx.simulate_shared_keystrokes("2 }").await;
1839 cx.shared_state().await.assert_eq(indoc! {r"abc
1840 def
1841
1842 paragraph
1843 the second
1844 ˇ
1845
1846
1847 third and
1848 final"});
1849
1850 // goes down over multiple blanks
1851 cx.simulate_shared_keystrokes("}").await;
1852 cx.shared_state().await.assert_eq(indoc! {r"abc
1853 def
1854
1855 paragraph
1856 the second
1857
1858
1859
1860 third and
1861 finaˇl"});
1862
1863 // goes up twice
1864 cx.simulate_shared_keystrokes("2 {").await;
1865 cx.shared_state().await.assert_eq(indoc! {r"abc
1866 def
1867 ˇ
1868 paragraph
1869 the second
1870
1871
1872
1873 third and
1874 final"});
1875 }
1876
1877 #[gpui::test]
1878 async fn test_matching(cx: &mut gpui::TestAppContext) {
1879 let mut cx = NeovimBackedTestContext::new(cx).await;
1880
1881 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1882 do(something(with<Types>.and_arrays[0, 2]))
1883 }"})
1884 .await;
1885 cx.simulate_shared_keystrokes("%").await;
1886 cx.shared_state()
1887 .await
1888 .assert_eq(indoc! {r"func (a stringˇ) {
1889 do(something(with<Types>.and_arrays[0, 2]))
1890 }"});
1891
1892 // test it works on the last character of the line
1893 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1894 do(something(with<Types>.and_arrays[0, 2]))
1895 }"})
1896 .await;
1897 cx.simulate_shared_keystrokes("%").await;
1898 cx.shared_state()
1899 .await
1900 .assert_eq(indoc! {r"func (a string) {
1901 do(something(with<Types>.and_arrays[0, 2]))
1902 ˇ}"});
1903
1904 // test it works on immediate nesting
1905 cx.set_shared_state("ˇ{()}").await;
1906 cx.simulate_shared_keystrokes("%").await;
1907 cx.shared_state().await.assert_eq("{()ˇ}");
1908 cx.simulate_shared_keystrokes("%").await;
1909 cx.shared_state().await.assert_eq("ˇ{()}");
1910
1911 // test it works on immediate nesting inside braces
1912 cx.set_shared_state("{\n ˇ{()}\n}").await;
1913 cx.simulate_shared_keystrokes("%").await;
1914 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
1915
1916 // test it jumps to the next paren on a line
1917 cx.set_shared_state("func ˇboop() {\n}").await;
1918 cx.simulate_shared_keystrokes("%").await;
1919 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
1920 }
1921
1922 #[gpui::test]
1923 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1924 let mut cx = NeovimBackedTestContext::new(cx).await;
1925
1926 // f and F
1927 cx.set_shared_state("ˇone two three four").await;
1928 cx.simulate_shared_keystrokes("f o").await;
1929 cx.shared_state().await.assert_eq("one twˇo three four");
1930 cx.simulate_shared_keystrokes(",").await;
1931 cx.shared_state().await.assert_eq("ˇone two three four");
1932 cx.simulate_shared_keystrokes("2 ;").await;
1933 cx.shared_state().await.assert_eq("one two three fˇour");
1934 cx.simulate_shared_keystrokes("shift-f e").await;
1935 cx.shared_state().await.assert_eq("one two threˇe four");
1936 cx.simulate_shared_keystrokes("2 ;").await;
1937 cx.shared_state().await.assert_eq("onˇe two three four");
1938 cx.simulate_shared_keystrokes(",").await;
1939 cx.shared_state().await.assert_eq("one two thrˇee four");
1940
1941 // t and T
1942 cx.set_shared_state("ˇone two three four").await;
1943 cx.simulate_shared_keystrokes("t o").await;
1944 cx.shared_state().await.assert_eq("one tˇwo three four");
1945 cx.simulate_shared_keystrokes(",").await;
1946 cx.shared_state().await.assert_eq("oˇne two three four");
1947 cx.simulate_shared_keystrokes("2 ;").await;
1948 cx.shared_state().await.assert_eq("one two three ˇfour");
1949 cx.simulate_shared_keystrokes("shift-t e").await;
1950 cx.shared_state().await.assert_eq("one two threeˇ four");
1951 cx.simulate_shared_keystrokes("3 ;").await;
1952 cx.shared_state().await.assert_eq("oneˇ two three four");
1953 cx.simulate_shared_keystrokes(",").await;
1954 cx.shared_state().await.assert_eq("one two thˇree four");
1955 }
1956
1957 #[gpui::test]
1958 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
1959 let mut cx = NeovimBackedTestContext::new(cx).await;
1960 let initial_state = indoc! {r"something(ˇfoo)"};
1961 cx.set_shared_state(initial_state).await;
1962 cx.simulate_shared_keystrokes("}").await;
1963 cx.shared_state().await.assert_eq("something(fooˇ)");
1964 }
1965
1966 #[gpui::test]
1967 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1968 let mut cx = NeovimBackedTestContext::new(cx).await;
1969 cx.set_shared_state("ˇone\n two\nthree").await;
1970 cx.simulate_shared_keystrokes("enter").await;
1971 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
1972 }
1973
1974 #[gpui::test]
1975 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
1976 let mut cx = NeovimBackedTestContext::new(cx).await;
1977 cx.set_shared_state("ˇ one\n two \nthree").await;
1978 cx.simulate_shared_keystrokes("g _").await;
1979 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
1980
1981 cx.set_shared_state("ˇ one \n two \nthree").await;
1982 cx.simulate_shared_keystrokes("g _").await;
1983 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
1984 cx.simulate_shared_keystrokes("2 g _").await;
1985 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
1986 }
1987
1988 #[gpui::test]
1989 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1990 let mut cx = NeovimBackedTestContext::new(cx).await;
1991 let initial_state = indoc! {r"abc
1992 def
1993 paragraph
1994 the second
1995 third ˇand
1996 final"};
1997
1998 cx.set_shared_state(initial_state).await;
1999 cx.simulate_shared_keystrokes("shift-h").await;
2000 cx.shared_state().await.assert_eq(indoc! {r"abˇc
2001 def
2002 paragraph
2003 the second
2004 third and
2005 final"});
2006
2007 // clip point
2008 cx.set_shared_state(indoc! {r"
2009 1 2 3
2010 4 5 6
2011 7 8 ˇ9
2012 "})
2013 .await;
2014 cx.simulate_shared_keystrokes("shift-h").await;
2015 cx.shared_state().await.assert_eq(indoc! {"
2016 1 2 ˇ3
2017 4 5 6
2018 7 8 9
2019 "});
2020
2021 cx.set_shared_state(indoc! {r"
2022 1 2 3
2023 4 5 6
2024 ˇ7 8 9
2025 "})
2026 .await;
2027 cx.simulate_shared_keystrokes("shift-h").await;
2028 cx.shared_state().await.assert_eq(indoc! {"
2029 ˇ1 2 3
2030 4 5 6
2031 7 8 9
2032 "});
2033
2034 cx.set_shared_state(indoc! {r"
2035 1 2 3
2036 4 5 ˇ6
2037 7 8 9"})
2038 .await;
2039 cx.simulate_shared_keystrokes("9 shift-h").await;
2040 cx.shared_state().await.assert_eq(indoc! {"
2041 1 2 3
2042 4 5 6
2043 7 8 ˇ9"});
2044 }
2045
2046 #[gpui::test]
2047 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2048 let mut cx = NeovimBackedTestContext::new(cx).await;
2049 let initial_state = indoc! {r"abˇc
2050 def
2051 paragraph
2052 the second
2053 third and
2054 final"};
2055
2056 cx.set_shared_state(initial_state).await;
2057 cx.simulate_shared_keystrokes("shift-m").await;
2058 cx.shared_state().await.assert_eq(indoc! {r"abc
2059 def
2060 paˇragraph
2061 the second
2062 third and
2063 final"});
2064
2065 cx.set_shared_state(indoc! {r"
2066 1 2 3
2067 4 5 6
2068 7 8 ˇ9
2069 "})
2070 .await;
2071 cx.simulate_shared_keystrokes("shift-m").await;
2072 cx.shared_state().await.assert_eq(indoc! {"
2073 1 2 3
2074 4 5 ˇ6
2075 7 8 9
2076 "});
2077 cx.set_shared_state(indoc! {r"
2078 1 2 3
2079 4 5 6
2080 ˇ7 8 9
2081 "})
2082 .await;
2083 cx.simulate_shared_keystrokes("shift-m").await;
2084 cx.shared_state().await.assert_eq(indoc! {"
2085 1 2 3
2086 ˇ4 5 6
2087 7 8 9
2088 "});
2089 cx.set_shared_state(indoc! {r"
2090 ˇ1 2 3
2091 4 5 6
2092 7 8 9
2093 "})
2094 .await;
2095 cx.simulate_shared_keystrokes("shift-m").await;
2096 cx.shared_state().await.assert_eq(indoc! {"
2097 1 2 3
2098 ˇ4 5 6
2099 7 8 9
2100 "});
2101 cx.set_shared_state(indoc! {r"
2102 1 2 3
2103 ˇ4 5 6
2104 7 8 9
2105 "})
2106 .await;
2107 cx.simulate_shared_keystrokes("shift-m").await;
2108 cx.shared_state().await.assert_eq(indoc! {"
2109 1 2 3
2110 ˇ4 5 6
2111 7 8 9
2112 "});
2113 cx.set_shared_state(indoc! {r"
2114 1 2 3
2115 4 5 ˇ6
2116 7 8 9
2117 "})
2118 .await;
2119 cx.simulate_shared_keystrokes("shift-m").await;
2120 cx.shared_state().await.assert_eq(indoc! {"
2121 1 2 3
2122 4 5 ˇ6
2123 7 8 9
2124 "});
2125 }
2126
2127 #[gpui::test]
2128 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2129 let mut cx = NeovimBackedTestContext::new(cx).await;
2130 let initial_state = indoc! {r"abc
2131 deˇf
2132 paragraph
2133 the second
2134 third and
2135 final"};
2136
2137 cx.set_shared_state(initial_state).await;
2138 cx.simulate_shared_keystrokes("shift-l").await;
2139 cx.shared_state().await.assert_eq(indoc! {r"abc
2140 def
2141 paragraph
2142 the second
2143 third and
2144 fiˇnal"});
2145
2146 cx.set_shared_state(indoc! {r"
2147 1 2 3
2148 4 5 ˇ6
2149 7 8 9
2150 "})
2151 .await;
2152 cx.simulate_shared_keystrokes("shift-l").await;
2153 cx.shared_state().await.assert_eq(indoc! {"
2154 1 2 3
2155 4 5 6
2156 7 8 9
2157 ˇ"});
2158
2159 cx.set_shared_state(indoc! {r"
2160 1 2 3
2161 ˇ4 5 6
2162 7 8 9
2163 "})
2164 .await;
2165 cx.simulate_shared_keystrokes("shift-l").await;
2166 cx.shared_state().await.assert_eq(indoc! {"
2167 1 2 3
2168 4 5 6
2169 7 8 9
2170 ˇ"});
2171
2172 cx.set_shared_state(indoc! {r"
2173 1 2 ˇ3
2174 4 5 6
2175 7 8 9
2176 "})
2177 .await;
2178 cx.simulate_shared_keystrokes("shift-l").await;
2179 cx.shared_state().await.assert_eq(indoc! {"
2180 1 2 3
2181 4 5 6
2182 7 8 9
2183 ˇ"});
2184
2185 cx.set_shared_state(indoc! {r"
2186 ˇ1 2 3
2187 4 5 6
2188 7 8 9
2189 "})
2190 .await;
2191 cx.simulate_shared_keystrokes("shift-l").await;
2192 cx.shared_state().await.assert_eq(indoc! {"
2193 1 2 3
2194 4 5 6
2195 7 8 9
2196 ˇ"});
2197
2198 cx.set_shared_state(indoc! {r"
2199 1 2 3
2200 4 5 ˇ6
2201 7 8 9
2202 "})
2203 .await;
2204 cx.simulate_shared_keystrokes("9 shift-l").await;
2205 cx.shared_state().await.assert_eq(indoc! {"
2206 1 2 ˇ3
2207 4 5 6
2208 7 8 9
2209 "});
2210 }
2211
2212 #[gpui::test]
2213 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2214 let mut cx = NeovimBackedTestContext::new(cx).await;
2215 cx.set_shared_state(indoc! {r"
2216 456 5ˇ67 678
2217 "})
2218 .await;
2219 cx.simulate_shared_keystrokes("g e").await;
2220 cx.shared_state().await.assert_eq(indoc! {"
2221 45ˇ6 567 678
2222 "});
2223
2224 // Test times
2225 cx.set_shared_state(indoc! {r"
2226 123 234 345
2227 456 5ˇ67 678
2228 "})
2229 .await;
2230 cx.simulate_shared_keystrokes("4 g e").await;
2231 cx.shared_state().await.assert_eq(indoc! {"
2232 12ˇ3 234 345
2233 456 567 678
2234 "});
2235
2236 // With punctuation
2237 cx.set_shared_state(indoc! {r"
2238 123 234 345
2239 4;5.6 5ˇ67 678
2240 789 890 901
2241 "})
2242 .await;
2243 cx.simulate_shared_keystrokes("g e").await;
2244 cx.shared_state().await.assert_eq(indoc! {"
2245 123 234 345
2246 4;5.ˇ6 567 678
2247 789 890 901
2248 "});
2249
2250 // With punctuation and count
2251 cx.set_shared_state(indoc! {r"
2252 123 234 345
2253 4;5.6 5ˇ67 678
2254 789 890 901
2255 "})
2256 .await;
2257 cx.simulate_shared_keystrokes("5 g e").await;
2258 cx.shared_state().await.assert_eq(indoc! {"
2259 123 234 345
2260 ˇ4;5.6 567 678
2261 789 890 901
2262 "});
2263
2264 // newlines
2265 cx.set_shared_state(indoc! {r"
2266 123 234 345
2267
2268 78ˇ9 890 901
2269 "})
2270 .await;
2271 cx.simulate_shared_keystrokes("g e").await;
2272 cx.shared_state().await.assert_eq(indoc! {"
2273 123 234 345
2274 ˇ
2275 789 890 901
2276 "});
2277 cx.simulate_shared_keystrokes("g e").await;
2278 cx.shared_state().await.assert_eq(indoc! {"
2279 123 234 34ˇ5
2280
2281 789 890 901
2282 "});
2283
2284 // With punctuation
2285 cx.set_shared_state(indoc! {r"
2286 123 234 345
2287 4;5.ˇ6 567 678
2288 789 890 901
2289 "})
2290 .await;
2291 cx.simulate_shared_keystrokes("g shift-e").await;
2292 cx.shared_state().await.assert_eq(indoc! {"
2293 123 234 34ˇ5
2294 4;5.6 567 678
2295 789 890 901
2296 "});
2297 }
2298
2299 #[gpui::test]
2300 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2301 let mut cx = NeovimBackedTestContext::new(cx).await;
2302
2303 cx.set_shared_state(indoc! {"
2304 fn aˇ() {
2305 return
2306 }
2307 "})
2308 .await;
2309 cx.simulate_shared_keystrokes("v $ %").await;
2310 cx.shared_state().await.assert_eq(indoc! {"
2311 fn a«() {
2312 return
2313 }ˇ»
2314 "});
2315 }
2316}