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