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