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_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
1701 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
1702 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
1703
1704 if head > outer.start && head < inner.start {
1705 let mut offset = inner.end.to_offset(map, Bias::Left);
1706 for c in map.buffer_snapshot.chars_at(offset) {
1707 if c == '/' || c == '\n' || c == '>' {
1708 return Some(offset.to_display_point(map));
1709 }
1710 offset += c.len_utf8();
1711 }
1712 } else {
1713 let mut offset = outer.start.to_offset(map, Bias::Left);
1714 for c in map.buffer_snapshot.chars_at(offset) {
1715 offset += c.len_utf8();
1716 if c == '<' || c == '\n' {
1717 return Some(offset.to_display_point(map));
1718 }
1719 }
1720 }
1721
1722 return None;
1723}
1724
1725fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
1726 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
1727 let display_point = map.clip_at_line_end(display_point);
1728 let point = display_point.to_point(map);
1729 let offset = point.to_offset(&map.buffer_snapshot);
1730
1731 // Ensure the range is contained by the current line.
1732 let mut line_end = map.next_line_boundary(point).0;
1733 if line_end == point {
1734 line_end = map.max_point().to_point(map);
1735 }
1736
1737 let line_range = map.prev_line_boundary(point).0..line_end;
1738 let visible_line_range =
1739 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
1740 let ranges = map
1741 .buffer_snapshot
1742 .bracket_ranges(visible_line_range.clone());
1743 if let Some(ranges) = ranges {
1744 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
1745 ..line_range.end.to_offset(&map.buffer_snapshot);
1746 let mut closest_pair_destination = None;
1747 let mut closest_distance = usize::MAX;
1748
1749 for (open_range, close_range) in ranges {
1750 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
1751 if offset > open_range.start && offset < close_range.start {
1752 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
1753 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
1754 return display_point;
1755 }
1756 if let Some(tag) = matching_tag(map, display_point) {
1757 return tag;
1758 }
1759 } else if close_range.contains(&offset) {
1760 return open_range.start.to_display_point(map);
1761 } else if open_range.contains(&offset) {
1762 return (close_range.end - 1).to_display_point(map);
1763 }
1764 }
1765
1766 if open_range.start >= offset && line_range.contains(&open_range.start) {
1767 let distance = open_range.start - offset;
1768 if distance < closest_distance {
1769 closest_pair_destination = Some(close_range.end - 1);
1770 closest_distance = distance;
1771 continue;
1772 }
1773 }
1774
1775 if close_range.start >= offset && line_range.contains(&close_range.start) {
1776 let distance = close_range.start - offset;
1777 if distance < closest_distance {
1778 closest_pair_destination = Some(open_range.start);
1779 closest_distance = distance;
1780 continue;
1781 }
1782 }
1783
1784 continue;
1785 }
1786
1787 closest_pair_destination
1788 .map(|destination| destination.to_display_point(map))
1789 .unwrap_or(display_point)
1790 } else {
1791 display_point
1792 }
1793}
1794
1795fn find_forward(
1796 map: &DisplaySnapshot,
1797 from: DisplayPoint,
1798 before: bool,
1799 target: char,
1800 times: usize,
1801 mode: FindRange,
1802 smartcase: bool,
1803) -> Option<DisplayPoint> {
1804 let mut to = from;
1805 let mut found = false;
1806
1807 for _ in 0..times {
1808 found = false;
1809 let new_to = find_boundary(map, to, mode, |_, right| {
1810 found = is_character_match(target, right, smartcase);
1811 found
1812 });
1813 if to == new_to {
1814 break;
1815 }
1816 to = new_to;
1817 }
1818
1819 if found {
1820 if before && to.column() > 0 {
1821 *to.column_mut() -= 1;
1822 Some(map.clip_point(to, Bias::Left))
1823 } else {
1824 Some(to)
1825 }
1826 } else {
1827 None
1828 }
1829}
1830
1831fn find_backward(
1832 map: &DisplaySnapshot,
1833 from: DisplayPoint,
1834 after: bool,
1835 target: char,
1836 times: usize,
1837 mode: FindRange,
1838 smartcase: bool,
1839) -> DisplayPoint {
1840 let mut to = from;
1841
1842 for _ in 0..times {
1843 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
1844 is_character_match(target, right, smartcase)
1845 });
1846 if to == new_to {
1847 break;
1848 }
1849 to = new_to;
1850 }
1851
1852 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
1853 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
1854 if after {
1855 *to.column_mut() += 1;
1856 map.clip_point(to, Bias::Right)
1857 } else {
1858 to
1859 }
1860 } else {
1861 from
1862 }
1863}
1864
1865fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
1866 if smartcase {
1867 if target.is_uppercase() {
1868 target == other
1869 } else {
1870 target == other.to_ascii_lowercase()
1871 }
1872 } else {
1873 target == other
1874 }
1875}
1876
1877fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1878 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
1879 first_non_whitespace(map, false, correct_line)
1880}
1881
1882fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1883 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
1884 first_non_whitespace(map, false, correct_line)
1885}
1886
1887fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
1888 let correct_line = start_of_relative_buffer_row(map, point, 0);
1889 right(map, correct_line, times.saturating_sub(1))
1890}
1891
1892pub(crate) fn next_line_end(
1893 map: &DisplaySnapshot,
1894 mut point: DisplayPoint,
1895 times: usize,
1896) -> DisplayPoint {
1897 if times > 1 {
1898 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1899 }
1900 end_of_line(map, false, point, 1)
1901}
1902
1903fn window_top(
1904 map: &DisplaySnapshot,
1905 point: DisplayPoint,
1906 text_layout_details: &TextLayoutDetails,
1907 mut times: usize,
1908) -> (DisplayPoint, SelectionGoal) {
1909 let first_visible_line = text_layout_details
1910 .scroll_anchor
1911 .anchor
1912 .to_display_point(map);
1913
1914 if first_visible_line.row() != DisplayRow(0)
1915 && text_layout_details.vertical_scroll_margin as usize > times
1916 {
1917 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1918 }
1919
1920 if let Some(visible_rows) = text_layout_details.visible_rows {
1921 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
1922 let new_row = (first_visible_line.row().0 + (times as u32))
1923 .min(bottom_row)
1924 .min(map.max_point().row().0);
1925 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1926
1927 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
1928 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1929 } else {
1930 let new_row =
1931 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
1932 let new_col = point.column().min(map.line_len(first_visible_line.row()));
1933
1934 let new_point = DisplayPoint::new(new_row, new_col);
1935 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1936 }
1937}
1938
1939fn window_middle(
1940 map: &DisplaySnapshot,
1941 point: DisplayPoint,
1942 text_layout_details: &TextLayoutDetails,
1943) -> (DisplayPoint, SelectionGoal) {
1944 if let Some(visible_rows) = text_layout_details.visible_rows {
1945 let first_visible_line = text_layout_details
1946 .scroll_anchor
1947 .anchor
1948 .to_display_point(map);
1949
1950 let max_visible_rows =
1951 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
1952
1953 let new_row =
1954 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
1955 let new_row = DisplayRow(new_row);
1956 let new_col = point.column().min(map.line_len(new_row));
1957 let new_point = DisplayPoint::new(new_row, new_col);
1958 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1959 } else {
1960 (point, SelectionGoal::None)
1961 }
1962}
1963
1964fn window_bottom(
1965 map: &DisplaySnapshot,
1966 point: DisplayPoint,
1967 text_layout_details: &TextLayoutDetails,
1968 mut times: usize,
1969) -> (DisplayPoint, SelectionGoal) {
1970 if let Some(visible_rows) = text_layout_details.visible_rows {
1971 let first_visible_line = text_layout_details
1972 .scroll_anchor
1973 .anchor
1974 .to_display_point(map);
1975 let bottom_row = first_visible_line.row().0
1976 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
1977 if bottom_row < map.max_point().row().0
1978 && text_layout_details.vertical_scroll_margin as usize > times
1979 {
1980 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
1981 }
1982 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
1983 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
1984 {
1985 first_visible_line.row()
1986 } else {
1987 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
1988 };
1989 let new_col = point.column().min(map.line_len(new_row));
1990 let new_point = DisplayPoint::new(new_row, new_col);
1991 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1992 } else {
1993 (point, SelectionGoal::None)
1994 }
1995}
1996
1997#[cfg(test)]
1998mod test {
1999
2000 use crate::test::NeovimBackedTestContext;
2001 use indoc::indoc;
2002
2003 #[gpui::test]
2004 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2005 let mut cx = NeovimBackedTestContext::new(cx).await;
2006
2007 let initial_state = indoc! {r"ˇabc
2008 def
2009
2010 paragraph
2011 the second
2012
2013
2014
2015 third and
2016 final"};
2017
2018 // goes down once
2019 cx.set_shared_state(initial_state).await;
2020 cx.simulate_shared_keystrokes("}").await;
2021 cx.shared_state().await.assert_eq(indoc! {r"abc
2022 def
2023 ˇ
2024 paragraph
2025 the second
2026
2027
2028
2029 third and
2030 final"});
2031
2032 // goes up once
2033 cx.simulate_shared_keystrokes("{").await;
2034 cx.shared_state().await.assert_eq(initial_state);
2035
2036 // goes down twice
2037 cx.simulate_shared_keystrokes("2 }").await;
2038 cx.shared_state().await.assert_eq(indoc! {r"abc
2039 def
2040
2041 paragraph
2042 the second
2043 ˇ
2044
2045
2046 third and
2047 final"});
2048
2049 // goes down over multiple blanks
2050 cx.simulate_shared_keystrokes("}").await;
2051 cx.shared_state().await.assert_eq(indoc! {r"abc
2052 def
2053
2054 paragraph
2055 the second
2056
2057
2058
2059 third and
2060 finaˇl"});
2061
2062 // goes up twice
2063 cx.simulate_shared_keystrokes("2 {").await;
2064 cx.shared_state().await.assert_eq(indoc! {r"abc
2065 def
2066 ˇ
2067 paragraph
2068 the second
2069
2070
2071
2072 third and
2073 final"});
2074 }
2075
2076 #[gpui::test]
2077 async fn test_matching(cx: &mut gpui::TestAppContext) {
2078 let mut cx = NeovimBackedTestContext::new(cx).await;
2079
2080 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2081 do(something(with<Types>.and_arrays[0, 2]))
2082 }"})
2083 .await;
2084 cx.simulate_shared_keystrokes("%").await;
2085 cx.shared_state()
2086 .await
2087 .assert_eq(indoc! {r"func (a stringˇ) {
2088 do(something(with<Types>.and_arrays[0, 2]))
2089 }"});
2090
2091 // test it works on the last character of the line
2092 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2093 do(something(with<Types>.and_arrays[0, 2]))
2094 }"})
2095 .await;
2096 cx.simulate_shared_keystrokes("%").await;
2097 cx.shared_state()
2098 .await
2099 .assert_eq(indoc! {r"func (a string) {
2100 do(something(with<Types>.and_arrays[0, 2]))
2101 ˇ}"});
2102
2103 // test it works on immediate nesting
2104 cx.set_shared_state("ˇ{()}").await;
2105 cx.simulate_shared_keystrokes("%").await;
2106 cx.shared_state().await.assert_eq("{()ˇ}");
2107 cx.simulate_shared_keystrokes("%").await;
2108 cx.shared_state().await.assert_eq("ˇ{()}");
2109
2110 // test it works on immediate nesting inside braces
2111 cx.set_shared_state("{\n ˇ{()}\n}").await;
2112 cx.simulate_shared_keystrokes("%").await;
2113 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
2114
2115 // test it jumps to the next paren on a line
2116 cx.set_shared_state("func ˇboop() {\n}").await;
2117 cx.simulate_shared_keystrokes("%").await;
2118 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2119 }
2120
2121 #[gpui::test]
2122 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2123 let mut cx = NeovimBackedTestContext::new_html(cx).await;
2124
2125 cx.neovim.exec("set filetype=html").await;
2126
2127 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2128 cx.simulate_shared_keystrokes("%").await;
2129 cx.shared_state()
2130 .await
2131 .assert_eq(indoc! {r"<body><ˇ/body>"});
2132 cx.simulate_shared_keystrokes("%").await;
2133
2134 // test jumping backwards
2135 cx.shared_state()
2136 .await
2137 .assert_eq(indoc! {r"<ˇbody></body>"});
2138
2139 // test self-closing tags
2140 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2141 cx.simulate_shared_keystrokes("%").await;
2142 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2143
2144 // test tag with attributes
2145 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2146 </div>
2147 "})
2148 .await;
2149 cx.simulate_shared_keystrokes("%").await;
2150 cx.shared_state()
2151 .await
2152 .assert_eq(indoc! {r"<div class='test' id='main'>
2153 <ˇ/div>
2154 "});
2155
2156 // test multi-line self-closing tag
2157 cx.set_shared_state(indoc! {r#"<a>
2158 <br
2159 test = "test"
2160 /ˇ>
2161 </a>"#})
2162 .await;
2163 cx.simulate_shared_keystrokes("%").await;
2164 cx.shared_state().await.assert_eq(indoc! {r#"<a>
2165 ˇ<br
2166 test = "test"
2167 />
2168 </a>"#});
2169 }
2170
2171 #[gpui::test]
2172 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
2173 let mut cx = NeovimBackedTestContext::new(cx).await;
2174
2175 // f and F
2176 cx.set_shared_state("ˇone two three four").await;
2177 cx.simulate_shared_keystrokes("f o").await;
2178 cx.shared_state().await.assert_eq("one twˇo three four");
2179 cx.simulate_shared_keystrokes(",").await;
2180 cx.shared_state().await.assert_eq("ˇone two three four");
2181 cx.simulate_shared_keystrokes("2 ;").await;
2182 cx.shared_state().await.assert_eq("one two three fˇour");
2183 cx.simulate_shared_keystrokes("shift-f e").await;
2184 cx.shared_state().await.assert_eq("one two threˇe four");
2185 cx.simulate_shared_keystrokes("2 ;").await;
2186 cx.shared_state().await.assert_eq("onˇe two three four");
2187 cx.simulate_shared_keystrokes(",").await;
2188 cx.shared_state().await.assert_eq("one two thrˇee four");
2189
2190 // t and T
2191 cx.set_shared_state("ˇone two three four").await;
2192 cx.simulate_shared_keystrokes("t o").await;
2193 cx.shared_state().await.assert_eq("one tˇwo three four");
2194 cx.simulate_shared_keystrokes(",").await;
2195 cx.shared_state().await.assert_eq("oˇne two three four");
2196 cx.simulate_shared_keystrokes("2 ;").await;
2197 cx.shared_state().await.assert_eq("one two three ˇfour");
2198 cx.simulate_shared_keystrokes("shift-t e").await;
2199 cx.shared_state().await.assert_eq("one two threeˇ four");
2200 cx.simulate_shared_keystrokes("3 ;").await;
2201 cx.shared_state().await.assert_eq("oneˇ two three four");
2202 cx.simulate_shared_keystrokes(",").await;
2203 cx.shared_state().await.assert_eq("one two thˇree four");
2204 }
2205
2206 #[gpui::test]
2207 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
2208 let mut cx = NeovimBackedTestContext::new(cx).await;
2209 let initial_state = indoc! {r"something(ˇfoo)"};
2210 cx.set_shared_state(initial_state).await;
2211 cx.simulate_shared_keystrokes("}").await;
2212 cx.shared_state().await.assert_eq("something(fooˇ)");
2213 }
2214
2215 #[gpui::test]
2216 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
2217 let mut cx = NeovimBackedTestContext::new(cx).await;
2218 cx.set_shared_state("ˇone\n two\nthree").await;
2219 cx.simulate_shared_keystrokes("enter").await;
2220 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
2221 }
2222
2223 #[gpui::test]
2224 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
2225 let mut cx = NeovimBackedTestContext::new(cx).await;
2226 cx.set_shared_state("ˇ one\n two \nthree").await;
2227 cx.simulate_shared_keystrokes("g _").await;
2228 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
2229
2230 cx.set_shared_state("ˇ one \n two \nthree").await;
2231 cx.simulate_shared_keystrokes("g _").await;
2232 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
2233 cx.simulate_shared_keystrokes("2 g _").await;
2234 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
2235 }
2236
2237 #[gpui::test]
2238 async fn test_window_top(cx: &mut gpui::TestAppContext) {
2239 let mut cx = NeovimBackedTestContext::new(cx).await;
2240 let initial_state = indoc! {r"abc
2241 def
2242 paragraph
2243 the second
2244 third ˇand
2245 final"};
2246
2247 cx.set_shared_state(initial_state).await;
2248 cx.simulate_shared_keystrokes("shift-h").await;
2249 cx.shared_state().await.assert_eq(indoc! {r"abˇc
2250 def
2251 paragraph
2252 the second
2253 third and
2254 final"});
2255
2256 // clip point
2257 cx.set_shared_state(indoc! {r"
2258 1 2 3
2259 4 5 6
2260 7 8 ˇ9
2261 "})
2262 .await;
2263 cx.simulate_shared_keystrokes("shift-h").await;
2264 cx.shared_state().await.assert_eq(indoc! {"
2265 1 2 ˇ3
2266 4 5 6
2267 7 8 9
2268 "});
2269
2270 cx.set_shared_state(indoc! {r"
2271 1 2 3
2272 4 5 6
2273 ˇ7 8 9
2274 "})
2275 .await;
2276 cx.simulate_shared_keystrokes("shift-h").await;
2277 cx.shared_state().await.assert_eq(indoc! {"
2278 ˇ1 2 3
2279 4 5 6
2280 7 8 9
2281 "});
2282
2283 cx.set_shared_state(indoc! {r"
2284 1 2 3
2285 4 5 ˇ6
2286 7 8 9"})
2287 .await;
2288 cx.simulate_shared_keystrokes("9 shift-h").await;
2289 cx.shared_state().await.assert_eq(indoc! {"
2290 1 2 3
2291 4 5 6
2292 7 8 ˇ9"});
2293 }
2294
2295 #[gpui::test]
2296 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
2297 let mut cx = NeovimBackedTestContext::new(cx).await;
2298 let initial_state = indoc! {r"abˇc
2299 def
2300 paragraph
2301 the second
2302 third and
2303 final"};
2304
2305 cx.set_shared_state(initial_state).await;
2306 cx.simulate_shared_keystrokes("shift-m").await;
2307 cx.shared_state().await.assert_eq(indoc! {r"abc
2308 def
2309 paˇragraph
2310 the second
2311 third and
2312 final"});
2313
2314 cx.set_shared_state(indoc! {r"
2315 1 2 3
2316 4 5 6
2317 7 8 ˇ9
2318 "})
2319 .await;
2320 cx.simulate_shared_keystrokes("shift-m").await;
2321 cx.shared_state().await.assert_eq(indoc! {"
2322 1 2 3
2323 4 5 ˇ6
2324 7 8 9
2325 "});
2326 cx.set_shared_state(indoc! {r"
2327 1 2 3
2328 4 5 6
2329 ˇ7 8 9
2330 "})
2331 .await;
2332 cx.simulate_shared_keystrokes("shift-m").await;
2333 cx.shared_state().await.assert_eq(indoc! {"
2334 1 2 3
2335 ˇ4 5 6
2336 7 8 9
2337 "});
2338 cx.set_shared_state(indoc! {r"
2339 ˇ1 2 3
2340 4 5 6
2341 7 8 9
2342 "})
2343 .await;
2344 cx.simulate_shared_keystrokes("shift-m").await;
2345 cx.shared_state().await.assert_eq(indoc! {"
2346 1 2 3
2347 ˇ4 5 6
2348 7 8 9
2349 "});
2350 cx.set_shared_state(indoc! {r"
2351 1 2 3
2352 ˇ4 5 6
2353 7 8 9
2354 "})
2355 .await;
2356 cx.simulate_shared_keystrokes("shift-m").await;
2357 cx.shared_state().await.assert_eq(indoc! {"
2358 1 2 3
2359 ˇ4 5 6
2360 7 8 9
2361 "});
2362 cx.set_shared_state(indoc! {r"
2363 1 2 3
2364 4 5 ˇ6
2365 7 8 9
2366 "})
2367 .await;
2368 cx.simulate_shared_keystrokes("shift-m").await;
2369 cx.shared_state().await.assert_eq(indoc! {"
2370 1 2 3
2371 4 5 ˇ6
2372 7 8 9
2373 "});
2374 }
2375
2376 #[gpui::test]
2377 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
2378 let mut cx = NeovimBackedTestContext::new(cx).await;
2379 let initial_state = indoc! {r"abc
2380 deˇf
2381 paragraph
2382 the second
2383 third and
2384 final"};
2385
2386 cx.set_shared_state(initial_state).await;
2387 cx.simulate_shared_keystrokes("shift-l").await;
2388 cx.shared_state().await.assert_eq(indoc! {r"abc
2389 def
2390 paragraph
2391 the second
2392 third and
2393 fiˇnal"});
2394
2395 cx.set_shared_state(indoc! {r"
2396 1 2 3
2397 4 5 ˇ6
2398 7 8 9
2399 "})
2400 .await;
2401 cx.simulate_shared_keystrokes("shift-l").await;
2402 cx.shared_state().await.assert_eq(indoc! {"
2403 1 2 3
2404 4 5 6
2405 7 8 9
2406 ˇ"});
2407
2408 cx.set_shared_state(indoc! {r"
2409 1 2 3
2410 ˇ4 5 6
2411 7 8 9
2412 "})
2413 .await;
2414 cx.simulate_shared_keystrokes("shift-l").await;
2415 cx.shared_state().await.assert_eq(indoc! {"
2416 1 2 3
2417 4 5 6
2418 7 8 9
2419 ˇ"});
2420
2421 cx.set_shared_state(indoc! {r"
2422 1 2 ˇ3
2423 4 5 6
2424 7 8 9
2425 "})
2426 .await;
2427 cx.simulate_shared_keystrokes("shift-l").await;
2428 cx.shared_state().await.assert_eq(indoc! {"
2429 1 2 3
2430 4 5 6
2431 7 8 9
2432 ˇ"});
2433
2434 cx.set_shared_state(indoc! {r"
2435 ˇ1 2 3
2436 4 5 6
2437 7 8 9
2438 "})
2439 .await;
2440 cx.simulate_shared_keystrokes("shift-l").await;
2441 cx.shared_state().await.assert_eq(indoc! {"
2442 1 2 3
2443 4 5 6
2444 7 8 9
2445 ˇ"});
2446
2447 cx.set_shared_state(indoc! {r"
2448 1 2 3
2449 4 5 ˇ6
2450 7 8 9
2451 "})
2452 .await;
2453 cx.simulate_shared_keystrokes("9 shift-l").await;
2454 cx.shared_state().await.assert_eq(indoc! {"
2455 1 2 ˇ3
2456 4 5 6
2457 7 8 9
2458 "});
2459 }
2460
2461 #[gpui::test]
2462 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
2463 let mut cx = NeovimBackedTestContext::new(cx).await;
2464 cx.set_shared_state(indoc! {r"
2465 456 5ˇ67 678
2466 "})
2467 .await;
2468 cx.simulate_shared_keystrokes("g e").await;
2469 cx.shared_state().await.assert_eq(indoc! {"
2470 45ˇ6 567 678
2471 "});
2472
2473 // Test times
2474 cx.set_shared_state(indoc! {r"
2475 123 234 345
2476 456 5ˇ67 678
2477 "})
2478 .await;
2479 cx.simulate_shared_keystrokes("4 g e").await;
2480 cx.shared_state().await.assert_eq(indoc! {"
2481 12ˇ3 234 345
2482 456 567 678
2483 "});
2484
2485 // With punctuation
2486 cx.set_shared_state(indoc! {r"
2487 123 234 345
2488 4;5.6 5ˇ67 678
2489 789 890 901
2490 "})
2491 .await;
2492 cx.simulate_shared_keystrokes("g e").await;
2493 cx.shared_state().await.assert_eq(indoc! {"
2494 123 234 345
2495 4;5.ˇ6 567 678
2496 789 890 901
2497 "});
2498
2499 // With punctuation and count
2500 cx.set_shared_state(indoc! {r"
2501 123 234 345
2502 4;5.6 5ˇ67 678
2503 789 890 901
2504 "})
2505 .await;
2506 cx.simulate_shared_keystrokes("5 g e").await;
2507 cx.shared_state().await.assert_eq(indoc! {"
2508 123 234 345
2509 ˇ4;5.6 567 678
2510 789 890 901
2511 "});
2512
2513 // newlines
2514 cx.set_shared_state(indoc! {r"
2515 123 234 345
2516
2517 78ˇ9 890 901
2518 "})
2519 .await;
2520 cx.simulate_shared_keystrokes("g e").await;
2521 cx.shared_state().await.assert_eq(indoc! {"
2522 123 234 345
2523 ˇ
2524 789 890 901
2525 "});
2526 cx.simulate_shared_keystrokes("g e").await;
2527 cx.shared_state().await.assert_eq(indoc! {"
2528 123 234 34ˇ5
2529
2530 789 890 901
2531 "});
2532
2533 // With punctuation
2534 cx.set_shared_state(indoc! {r"
2535 123 234 345
2536 4;5.ˇ6 567 678
2537 789 890 901
2538 "})
2539 .await;
2540 cx.simulate_shared_keystrokes("g shift-e").await;
2541 cx.shared_state().await.assert_eq(indoc! {"
2542 123 234 34ˇ5
2543 4;5.6 567 678
2544 789 890 901
2545 "});
2546 }
2547
2548 #[gpui::test]
2549 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
2550 let mut cx = NeovimBackedTestContext::new(cx).await;
2551
2552 cx.set_shared_state(indoc! {"
2553 fn aˇ() {
2554 return
2555 }
2556 "})
2557 .await;
2558 cx.simulate_shared_keystrokes("v $ %").await;
2559 cx.shared_state().await.assert_eq(indoc! {"
2560 fn a«() {
2561 return
2562 }ˇ»
2563 "});
2564 }
2565}