1use editor::{
2 Anchor, Bias, DisplayPoint, Editor, RowExt, ToOffset, ToPoint,
3 display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
4 movement::{
5 self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
6 },
7 scroll::Autoscroll,
8};
9use gpui::{Context, Window, action_with_deprecated_aliases, actions, impl_actions, px};
10use language::{CharKind, Point, Selection, SelectionGoal};
11use multi_buffer::MultiBufferRow;
12use schemars::JsonSchema;
13use serde::Deserialize;
14use std::ops::Range;
15use workspace::searchable::Direction;
16
17use crate::{
18 Vim,
19 normal::mark,
20 state::{Mode, Operator},
21 surrounds::SurroundsType,
22};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub(crate) enum MotionKind {
26 Linewise,
27 Exclusive,
28 Inclusive,
29}
30
31impl MotionKind {
32 pub(crate) fn for_mode(mode: Mode) -> Self {
33 match mode {
34 Mode::VisualLine => MotionKind::Linewise,
35 _ => MotionKind::Exclusive,
36 }
37 }
38
39 pub(crate) fn linewise(&self) -> bool {
40 matches!(self, MotionKind::Linewise)
41 }
42}
43
44#[derive(Clone, Debug, PartialEq, Eq)]
45pub enum Motion {
46 Left,
47 WrappingLeft,
48 Down {
49 display_lines: bool,
50 },
51 Up {
52 display_lines: bool,
53 },
54 Right,
55 WrappingRight,
56 NextWordStart {
57 ignore_punctuation: bool,
58 },
59 NextWordEnd {
60 ignore_punctuation: bool,
61 },
62 PreviousWordStart {
63 ignore_punctuation: bool,
64 },
65 PreviousWordEnd {
66 ignore_punctuation: bool,
67 },
68 NextSubwordStart {
69 ignore_punctuation: bool,
70 },
71 NextSubwordEnd {
72 ignore_punctuation: bool,
73 },
74 PreviousSubwordStart {
75 ignore_punctuation: bool,
76 },
77 PreviousSubwordEnd {
78 ignore_punctuation: bool,
79 },
80 FirstNonWhitespace {
81 display_lines: bool,
82 },
83 CurrentLine,
84 StartOfLine {
85 display_lines: bool,
86 },
87 EndOfLine {
88 display_lines: bool,
89 },
90 SentenceBackward,
91 SentenceForward,
92 StartOfParagraph,
93 EndOfParagraph,
94 StartOfDocument,
95 EndOfDocument,
96 Matching,
97 GoToPercentage,
98 UnmatchedForward {
99 char: char,
100 },
101 UnmatchedBackward {
102 char: char,
103 },
104 FindForward {
105 before: bool,
106 char: char,
107 mode: FindRange,
108 smartcase: bool,
109 },
110 FindBackward {
111 after: bool,
112 char: char,
113 mode: FindRange,
114 smartcase: bool,
115 },
116 Sneak {
117 first_char: char,
118 second_char: char,
119 smartcase: bool,
120 },
121 SneakBackward {
122 first_char: char,
123 second_char: char,
124 smartcase: bool,
125 },
126 RepeatFind {
127 last_find: Box<Motion>,
128 },
129 RepeatFindReversed {
130 last_find: Box<Motion>,
131 },
132 NextLineStart,
133 PreviousLineStart,
134 StartOfLineDownward,
135 EndOfLineDownward,
136 GoToColumn,
137 WindowTop,
138 WindowMiddle,
139 WindowBottom,
140 NextSectionStart,
141 NextSectionEnd,
142 PreviousSectionStart,
143 PreviousSectionEnd,
144 NextMethodStart,
145 NextMethodEnd,
146 PreviousMethodStart,
147 PreviousMethodEnd,
148 NextComment,
149 PreviousComment,
150
151 // we don't have a good way to run a search synchronously, so
152 // we handle search motions by running the search async and then
153 // calling back into motion with this
154 ZedSearchResult {
155 prior_selections: Vec<Range<Anchor>>,
156 new_selections: Vec<Range<Anchor>>,
157 },
158 Jump {
159 anchor: Anchor,
160 line: bool,
161 },
162}
163
164#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
165#[serde(deny_unknown_fields)]
166struct NextWordStart {
167 #[serde(default)]
168 ignore_punctuation: bool,
169}
170
171#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
172#[serde(deny_unknown_fields)]
173struct NextWordEnd {
174 #[serde(default)]
175 ignore_punctuation: bool,
176}
177
178#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
179#[serde(deny_unknown_fields)]
180struct PreviousWordStart {
181 #[serde(default)]
182 ignore_punctuation: bool,
183}
184
185#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
186#[serde(deny_unknown_fields)]
187struct PreviousWordEnd {
188 #[serde(default)]
189 ignore_punctuation: bool,
190}
191
192#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
193#[serde(deny_unknown_fields)]
194pub(crate) struct NextSubwordStart {
195 #[serde(default)]
196 pub(crate) ignore_punctuation: bool,
197}
198
199#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
200#[serde(deny_unknown_fields)]
201pub(crate) struct NextSubwordEnd {
202 #[serde(default)]
203 pub(crate) ignore_punctuation: bool,
204}
205
206#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
207#[serde(deny_unknown_fields)]
208pub(crate) struct PreviousSubwordStart {
209 #[serde(default)]
210 pub(crate) ignore_punctuation: bool,
211}
212
213#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
214#[serde(deny_unknown_fields)]
215pub(crate) struct PreviousSubwordEnd {
216 #[serde(default)]
217 pub(crate) ignore_punctuation: bool,
218}
219
220#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
221#[serde(deny_unknown_fields)]
222pub(crate) struct Up {
223 #[serde(default)]
224 pub(crate) display_lines: bool,
225}
226
227#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
228#[serde(deny_unknown_fields)]
229pub(crate) struct Down {
230 #[serde(default)]
231 pub(crate) display_lines: bool,
232}
233
234#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
235#[serde(deny_unknown_fields)]
236struct FirstNonWhitespace {
237 #[serde(default)]
238 display_lines: bool,
239}
240
241#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
242#[serde(deny_unknown_fields)]
243struct EndOfLine {
244 #[serde(default)]
245 display_lines: bool,
246}
247
248#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
249#[serde(deny_unknown_fields)]
250pub struct StartOfLine {
251 #[serde(default)]
252 pub(crate) display_lines: bool,
253}
254
255#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
256#[serde(deny_unknown_fields)]
257struct UnmatchedForward {
258 #[serde(default)]
259 char: char,
260}
261
262#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
263#[serde(deny_unknown_fields)]
264struct UnmatchedBackward {
265 #[serde(default)]
266 char: char,
267}
268
269impl_actions!(
270 vim,
271 [
272 StartOfLine,
273 EndOfLine,
274 FirstNonWhitespace,
275 Down,
276 Up,
277 NextWordStart,
278 NextWordEnd,
279 PreviousWordStart,
280 PreviousWordEnd,
281 NextSubwordStart,
282 NextSubwordEnd,
283 PreviousSubwordStart,
284 PreviousSubwordEnd,
285 UnmatchedForward,
286 UnmatchedBackward
287 ]
288);
289
290actions!(
291 vim,
292 [
293 Left,
294 Backspace,
295 Right,
296 Space,
297 CurrentLine,
298 SentenceForward,
299 SentenceBackward,
300 StartOfParagraph,
301 EndOfParagraph,
302 StartOfDocument,
303 EndOfDocument,
304 Matching,
305 GoToPercentage,
306 NextLineStart,
307 PreviousLineStart,
308 StartOfLineDownward,
309 EndOfLineDownward,
310 GoToColumn,
311 RepeatFind,
312 RepeatFindReversed,
313 WindowTop,
314 WindowMiddle,
315 WindowBottom,
316 NextSectionStart,
317 NextSectionEnd,
318 PreviousSectionStart,
319 PreviousSectionEnd,
320 NextMethodStart,
321 NextMethodEnd,
322 PreviousMethodStart,
323 PreviousMethodEnd,
324 NextComment,
325 PreviousComment,
326 ]
327);
328
329action_with_deprecated_aliases!(vim, WrappingLeft, ["vim::Backspace"]);
330action_with_deprecated_aliases!(vim, WrappingRight, ["vim::Space"]);
331
332pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
333 Vim::action(editor, cx, |vim, _: &Left, window, cx| {
334 vim.motion(Motion::Left, window, cx)
335 });
336 Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
337 vim.motion(Motion::WrappingLeft, window, cx)
338 });
339 // Deprecated.
340 Vim::action(editor, cx, |vim, _: &Backspace, window, cx| {
341 vim.motion(Motion::WrappingLeft, window, cx)
342 });
343 Vim::action(editor, cx, |vim, action: &Down, window, cx| {
344 vim.motion(
345 Motion::Down {
346 display_lines: action.display_lines,
347 },
348 window,
349 cx,
350 )
351 });
352 Vim::action(editor, cx, |vim, action: &Up, window, cx| {
353 vim.motion(
354 Motion::Up {
355 display_lines: action.display_lines,
356 },
357 window,
358 cx,
359 )
360 });
361 Vim::action(editor, cx, |vim, _: &Right, window, cx| {
362 vim.motion(Motion::Right, window, cx)
363 });
364 Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
365 vim.motion(Motion::WrappingRight, window, cx)
366 });
367 // Deprecated.
368 Vim::action(editor, cx, |vim, _: &Space, window, cx| {
369 vim.motion(Motion::WrappingRight, window, cx)
370 });
371 Vim::action(
372 editor,
373 cx,
374 |vim, action: &FirstNonWhitespace, window, cx| {
375 vim.motion(
376 Motion::FirstNonWhitespace {
377 display_lines: action.display_lines,
378 },
379 window,
380 cx,
381 )
382 },
383 );
384 Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
385 vim.motion(
386 Motion::StartOfLine {
387 display_lines: action.display_lines,
388 },
389 window,
390 cx,
391 )
392 });
393 Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| {
394 vim.motion(
395 Motion::EndOfLine {
396 display_lines: action.display_lines,
397 },
398 window,
399 cx,
400 )
401 });
402 Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| {
403 vim.motion(Motion::CurrentLine, window, cx)
404 });
405 Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| {
406 vim.motion(Motion::StartOfParagraph, window, cx)
407 });
408 Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| {
409 vim.motion(Motion::EndOfParagraph, window, cx)
410 });
411
412 Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| {
413 vim.motion(Motion::SentenceForward, window, cx)
414 });
415 Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| {
416 vim.motion(Motion::SentenceBackward, window, cx)
417 });
418 Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| {
419 vim.motion(Motion::StartOfDocument, window, cx)
420 });
421 Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| {
422 vim.motion(Motion::EndOfDocument, window, cx)
423 });
424 Vim::action(editor, cx, |vim, _: &Matching, window, cx| {
425 vim.motion(Motion::Matching, window, cx)
426 });
427 Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| {
428 vim.motion(Motion::GoToPercentage, window, cx)
429 });
430 Vim::action(
431 editor,
432 cx,
433 |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| {
434 vim.motion(Motion::UnmatchedForward { char }, window, cx)
435 },
436 );
437 Vim::action(
438 editor,
439 cx,
440 |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| {
441 vim.motion(Motion::UnmatchedBackward { char }, window, cx)
442 },
443 );
444 Vim::action(
445 editor,
446 cx,
447 |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| {
448 vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx)
449 },
450 );
451 Vim::action(
452 editor,
453 cx,
454 |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| {
455 vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx)
456 },
457 );
458 Vim::action(
459 editor,
460 cx,
461 |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| {
462 vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx)
463 },
464 );
465 Vim::action(
466 editor,
467 cx,
468 |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| {
469 vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx)
470 },
471 );
472 Vim::action(
473 editor,
474 cx,
475 |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| {
476 vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx)
477 },
478 );
479 Vim::action(
480 editor,
481 cx,
482 |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| {
483 vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx)
484 },
485 );
486 Vim::action(
487 editor,
488 cx,
489 |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| {
490 vim.motion(
491 Motion::PreviousSubwordStart { ignore_punctuation },
492 window,
493 cx,
494 )
495 },
496 );
497 Vim::action(
498 editor,
499 cx,
500 |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| {
501 vim.motion(
502 Motion::PreviousSubwordEnd { ignore_punctuation },
503 window,
504 cx,
505 )
506 },
507 );
508 Vim::action(editor, cx, |vim, &NextLineStart, window, cx| {
509 vim.motion(Motion::NextLineStart, window, cx)
510 });
511 Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| {
512 vim.motion(Motion::PreviousLineStart, window, cx)
513 });
514 Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| {
515 vim.motion(Motion::StartOfLineDownward, window, cx)
516 });
517 Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| {
518 vim.motion(Motion::EndOfLineDownward, window, cx)
519 });
520 Vim::action(editor, cx, |vim, &GoToColumn, window, cx| {
521 vim.motion(Motion::GoToColumn, window, cx)
522 });
523
524 Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| {
525 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
526 vim.motion(Motion::RepeatFind { last_find }, window, cx);
527 }
528 });
529
530 Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| {
531 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
532 vim.motion(Motion::RepeatFindReversed { last_find }, window, cx);
533 }
534 });
535 Vim::action(editor, cx, |vim, &WindowTop, window, cx| {
536 vim.motion(Motion::WindowTop, window, cx)
537 });
538 Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| {
539 vim.motion(Motion::WindowMiddle, window, cx)
540 });
541 Vim::action(editor, cx, |vim, &WindowBottom, window, cx| {
542 vim.motion(Motion::WindowBottom, window, cx)
543 });
544
545 Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| {
546 vim.motion(Motion::PreviousSectionStart, window, cx)
547 });
548 Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| {
549 vim.motion(Motion::NextSectionStart, window, cx)
550 });
551 Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| {
552 vim.motion(Motion::PreviousSectionEnd, window, cx)
553 });
554 Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| {
555 vim.motion(Motion::NextSectionEnd, window, cx)
556 });
557 Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| {
558 vim.motion(Motion::PreviousMethodStart, window, cx)
559 });
560 Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| {
561 vim.motion(Motion::NextMethodStart, window, cx)
562 });
563 Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| {
564 vim.motion(Motion::PreviousMethodEnd, window, cx)
565 });
566 Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| {
567 vim.motion(Motion::NextMethodEnd, window, cx)
568 });
569 Vim::action(editor, cx, |vim, &NextComment, window, cx| {
570 vim.motion(Motion::NextComment, window, cx)
571 });
572 Vim::action(editor, cx, |vim, &PreviousComment, window, cx| {
573 vim.motion(Motion::PreviousComment, window, cx)
574 });
575}
576
577impl Vim {
578 pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context<Self>) {
579 if let Motion::ZedSearchResult {
580 prior_selections, ..
581 } = &m
582 {
583 match self.mode {
584 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
585 if !prior_selections.is_empty() {
586 self.update_editor(window, cx, |_, editor, window, cx| {
587 editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
588 s.select_ranges(prior_selections.iter().cloned())
589 })
590 });
591 }
592 }
593 Mode::Normal | Mode::Replace | Mode::Insert => {
594 if self.active_operator().is_none() {
595 return;
596 }
597 }
598
599 Mode::HelixNormal => {}
600 }
601 }
602
603 self.motion(m, window, cx)
604 }
605
606 pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context<Self>) {
607 if let Some(Operator::FindForward { .. })
608 | Some(Operator::Sneak { .. })
609 | Some(Operator::SneakBackward { .. })
610 | Some(Operator::FindBackward { .. }) = self.active_operator()
611 {
612 self.pop_operator(window, cx);
613 }
614
615 let count = Vim::take_count(cx);
616 let active_operator = self.active_operator();
617 let mut waiting_operator: Option<Operator> = None;
618 match self.mode {
619 Mode::Normal | Mode::Replace | Mode::Insert => {
620 if active_operator == Some(Operator::AddSurrounds { target: None }) {
621 waiting_operator = Some(Operator::AddSurrounds {
622 target: Some(SurroundsType::Motion(motion)),
623 });
624 } else {
625 self.normal_motion(motion.clone(), active_operator.clone(), count, window, cx)
626 }
627 }
628 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
629 self.visual_motion(motion.clone(), count, window, cx)
630 }
631
632 Mode::HelixNormal => self.helix_normal_motion(motion.clone(), count, window, cx),
633 }
634 self.clear_operator(window, cx);
635 if let Some(operator) = waiting_operator {
636 self.push_operator(operator, window, cx);
637 Vim::globals(cx).pre_count = count
638 }
639 }
640}
641
642// Motion handling is specified here:
643// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
644impl Motion {
645 fn default_kind(&self) -> MotionKind {
646 use Motion::*;
647 match self {
648 Down { .. }
649 | Up { .. }
650 | StartOfDocument
651 | EndOfDocument
652 | CurrentLine
653 | NextLineStart
654 | PreviousLineStart
655 | StartOfLineDownward
656 | WindowTop
657 | WindowMiddle
658 | WindowBottom
659 | NextSectionStart
660 | NextSectionEnd
661 | PreviousSectionStart
662 | PreviousSectionEnd
663 | NextMethodStart
664 | NextMethodEnd
665 | PreviousMethodStart
666 | PreviousMethodEnd
667 | NextComment
668 | PreviousComment
669 | GoToPercentage
670 | Jump { line: true, .. } => MotionKind::Linewise,
671 EndOfLine { .. }
672 | EndOfLineDownward
673 | Matching
674 | FindForward { .. }
675 | NextWordEnd { .. }
676 | PreviousWordEnd { .. }
677 | NextSubwordEnd { .. }
678 | PreviousSubwordEnd { .. } => MotionKind::Inclusive,
679 Left
680 | WrappingLeft
681 | Right
682 | WrappingRight
683 | StartOfLine { .. }
684 | StartOfParagraph
685 | EndOfParagraph
686 | SentenceBackward
687 | SentenceForward
688 | GoToColumn
689 | UnmatchedForward { .. }
690 | UnmatchedBackward { .. }
691 | NextWordStart { .. }
692 | PreviousWordStart { .. }
693 | NextSubwordStart { .. }
694 | PreviousSubwordStart { .. }
695 | FirstNonWhitespace { .. }
696 | FindBackward { .. }
697 | Sneak { .. }
698 | SneakBackward { .. }
699 | Jump { .. }
700 | ZedSearchResult { .. } => MotionKind::Exclusive,
701 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
702 motion.default_kind()
703 }
704 }
705 }
706
707 fn skip_exclusive_special_case(&self) -> bool {
708 match self {
709 Motion::WrappingLeft | Motion::WrappingRight => true,
710 _ => false,
711 }
712 }
713
714 pub fn infallible(&self) -> bool {
715 use Motion::*;
716 match self {
717 StartOfDocument | EndOfDocument | CurrentLine => true,
718 Down { .. }
719 | Up { .. }
720 | EndOfLine { .. }
721 | Matching
722 | UnmatchedForward { .. }
723 | UnmatchedBackward { .. }
724 | FindForward { .. }
725 | RepeatFind { .. }
726 | Left
727 | WrappingLeft
728 | Right
729 | WrappingRight
730 | StartOfLine { .. }
731 | StartOfParagraph
732 | EndOfParagraph
733 | SentenceBackward
734 | SentenceForward
735 | StartOfLineDownward
736 | EndOfLineDownward
737 | GoToColumn
738 | GoToPercentage
739 | NextWordStart { .. }
740 | NextWordEnd { .. }
741 | PreviousWordStart { .. }
742 | PreviousWordEnd { .. }
743 | NextSubwordStart { .. }
744 | NextSubwordEnd { .. }
745 | PreviousSubwordStart { .. }
746 | PreviousSubwordEnd { .. }
747 | FirstNonWhitespace { .. }
748 | FindBackward { .. }
749 | Sneak { .. }
750 | SneakBackward { .. }
751 | RepeatFindReversed { .. }
752 | WindowTop
753 | WindowMiddle
754 | WindowBottom
755 | NextLineStart
756 | PreviousLineStart
757 | ZedSearchResult { .. }
758 | NextSectionStart
759 | NextSectionEnd
760 | PreviousSectionStart
761 | PreviousSectionEnd
762 | NextMethodStart
763 | NextMethodEnd
764 | PreviousMethodStart
765 | PreviousMethodEnd
766 | NextComment
767 | PreviousComment
768 | Jump { .. } => false,
769 }
770 }
771
772 pub fn move_point(
773 &self,
774 map: &DisplaySnapshot,
775 point: DisplayPoint,
776 goal: SelectionGoal,
777 maybe_times: Option<usize>,
778 text_layout_details: &TextLayoutDetails,
779 ) -> Option<(DisplayPoint, SelectionGoal)> {
780 let times = maybe_times.unwrap_or(1);
781 use Motion::*;
782 let infallible = self.infallible();
783 let (new_point, goal) = match self {
784 Left => (left(map, point, times), SelectionGoal::None),
785 WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None),
786 Down {
787 display_lines: false,
788 } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
789 Down {
790 display_lines: true,
791 } => down_display(map, point, goal, times, text_layout_details),
792 Up {
793 display_lines: false,
794 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details),
795 Up {
796 display_lines: true,
797 } => up_display(map, point, goal, times, text_layout_details),
798 Right => (right(map, point, times), SelectionGoal::None),
799 WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None),
800 NextWordStart { ignore_punctuation } => (
801 next_word_start(map, point, *ignore_punctuation, times),
802 SelectionGoal::None,
803 ),
804 NextWordEnd { ignore_punctuation } => (
805 next_word_end(map, point, *ignore_punctuation, times, true),
806 SelectionGoal::None,
807 ),
808 PreviousWordStart { ignore_punctuation } => (
809 previous_word_start(map, point, *ignore_punctuation, times),
810 SelectionGoal::None,
811 ),
812 PreviousWordEnd { ignore_punctuation } => (
813 previous_word_end(map, point, *ignore_punctuation, times),
814 SelectionGoal::None,
815 ),
816 NextSubwordStart { ignore_punctuation } => (
817 next_subword_start(map, point, *ignore_punctuation, times),
818 SelectionGoal::None,
819 ),
820 NextSubwordEnd { ignore_punctuation } => (
821 next_subword_end(map, point, *ignore_punctuation, times, true),
822 SelectionGoal::None,
823 ),
824 PreviousSubwordStart { ignore_punctuation } => (
825 previous_subword_start(map, point, *ignore_punctuation, times),
826 SelectionGoal::None,
827 ),
828 PreviousSubwordEnd { ignore_punctuation } => (
829 previous_subword_end(map, point, *ignore_punctuation, times),
830 SelectionGoal::None,
831 ),
832 FirstNonWhitespace { display_lines } => (
833 first_non_whitespace(map, *display_lines, point),
834 SelectionGoal::None,
835 ),
836 StartOfLine { display_lines } => (
837 start_of_line(map, *display_lines, point),
838 SelectionGoal::None,
839 ),
840 EndOfLine { display_lines } => (
841 end_of_line(map, *display_lines, point, times),
842 SelectionGoal::None,
843 ),
844 SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
845 SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
846 StartOfParagraph => (
847 movement::start_of_paragraph(map, point, times),
848 SelectionGoal::None,
849 ),
850 EndOfParagraph => (
851 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
852 SelectionGoal::None,
853 ),
854 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
855 StartOfDocument => (
856 start_of_document(map, point, maybe_times),
857 SelectionGoal::None,
858 ),
859 EndOfDocument => (
860 end_of_document(map, point, maybe_times),
861 SelectionGoal::None,
862 ),
863 Matching => (matching(map, point), SelectionGoal::None),
864 GoToPercentage => (go_to_percentage(map, point, times), SelectionGoal::None),
865 UnmatchedForward { char } => (
866 unmatched_forward(map, point, *char, times),
867 SelectionGoal::None,
868 ),
869 UnmatchedBackward { char } => (
870 unmatched_backward(map, point, *char, times),
871 SelectionGoal::None,
872 ),
873 // t f
874 FindForward {
875 before,
876 char,
877 mode,
878 smartcase,
879 } => {
880 return find_forward(map, point, *before, *char, times, *mode, *smartcase)
881 .map(|new_point| (new_point, SelectionGoal::None));
882 }
883 // T F
884 FindBackward {
885 after,
886 char,
887 mode,
888 smartcase,
889 } => (
890 find_backward(map, point, *after, *char, times, *mode, *smartcase),
891 SelectionGoal::None,
892 ),
893 Sneak {
894 first_char,
895 second_char,
896 smartcase,
897 } => {
898 return sneak(map, point, *first_char, *second_char, times, *smartcase)
899 .map(|new_point| (new_point, SelectionGoal::None));
900 }
901 SneakBackward {
902 first_char,
903 second_char,
904 smartcase,
905 } => {
906 return sneak_backward(map, point, *first_char, *second_char, times, *smartcase)
907 .map(|new_point| (new_point, SelectionGoal::None));
908 }
909 // ; -- repeat the last find done with t, f, T, F
910 RepeatFind { last_find } => match **last_find {
911 Motion::FindForward {
912 before,
913 char,
914 mode,
915 smartcase,
916 } => {
917 let mut new_point =
918 find_forward(map, point, before, char, times, mode, smartcase);
919 if new_point == Some(point) {
920 new_point =
921 find_forward(map, point, before, char, times + 1, mode, smartcase);
922 }
923
924 return new_point.map(|new_point| (new_point, SelectionGoal::None));
925 }
926
927 Motion::FindBackward {
928 after,
929 char,
930 mode,
931 smartcase,
932 } => {
933 let mut new_point =
934 find_backward(map, point, after, char, times, mode, smartcase);
935 if new_point == point {
936 new_point =
937 find_backward(map, point, after, char, times + 1, mode, smartcase);
938 }
939
940 (new_point, SelectionGoal::None)
941 }
942 Motion::Sneak {
943 first_char,
944 second_char,
945 smartcase,
946 } => {
947 let mut new_point =
948 sneak(map, point, first_char, second_char, times, smartcase);
949 if new_point == Some(point) {
950 new_point =
951 sneak(map, point, first_char, second_char, times + 1, smartcase);
952 }
953
954 return new_point.map(|new_point| (new_point, SelectionGoal::None));
955 }
956
957 Motion::SneakBackward {
958 first_char,
959 second_char,
960 smartcase,
961 } => {
962 let mut new_point =
963 sneak_backward(map, point, first_char, second_char, times, smartcase);
964 if new_point == Some(point) {
965 new_point = sneak_backward(
966 map,
967 point,
968 first_char,
969 second_char,
970 times + 1,
971 smartcase,
972 );
973 }
974
975 return new_point.map(|new_point| (new_point, SelectionGoal::None));
976 }
977 _ => return None,
978 },
979 // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction
980 RepeatFindReversed { last_find } => match **last_find {
981 Motion::FindForward {
982 before,
983 char,
984 mode,
985 smartcase,
986 } => {
987 let mut new_point =
988 find_backward(map, point, before, char, times, mode, smartcase);
989 if new_point == point {
990 new_point =
991 find_backward(map, point, before, char, times + 1, mode, smartcase);
992 }
993
994 (new_point, SelectionGoal::None)
995 }
996
997 Motion::FindBackward {
998 after,
999 char,
1000 mode,
1001 smartcase,
1002 } => {
1003 let mut new_point =
1004 find_forward(map, point, after, char, times, mode, smartcase);
1005 if new_point == Some(point) {
1006 new_point =
1007 find_forward(map, point, after, char, times + 1, mode, smartcase);
1008 }
1009
1010 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1011 }
1012
1013 Motion::Sneak {
1014 first_char,
1015 second_char,
1016 smartcase,
1017 } => {
1018 let mut new_point =
1019 sneak_backward(map, point, first_char, second_char, times, smartcase);
1020 if new_point == Some(point) {
1021 new_point = sneak_backward(
1022 map,
1023 point,
1024 first_char,
1025 second_char,
1026 times + 1,
1027 smartcase,
1028 );
1029 }
1030
1031 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1032 }
1033
1034 Motion::SneakBackward {
1035 first_char,
1036 second_char,
1037 smartcase,
1038 } => {
1039 let mut new_point =
1040 sneak(map, point, first_char, second_char, times, smartcase);
1041 if new_point == Some(point) {
1042 new_point =
1043 sneak(map, point, first_char, second_char, times + 1, smartcase);
1044 }
1045
1046 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1047 }
1048 _ => return None,
1049 },
1050 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
1051 PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
1052 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
1053 EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
1054 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
1055 WindowTop => window_top(map, point, text_layout_details, times - 1),
1056 WindowMiddle => window_middle(map, point, text_layout_details),
1057 WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
1058 Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
1059 ZedSearchResult { new_selections, .. } => {
1060 // There will be only one selection, as
1061 // Search::SelectNextMatch selects a single match.
1062 if let Some(new_selection) = new_selections.first() {
1063 (
1064 new_selection.start.to_display_point(map),
1065 SelectionGoal::None,
1066 )
1067 } else {
1068 return None;
1069 }
1070 }
1071 NextSectionStart => (
1072 section_motion(map, point, times, Direction::Next, true),
1073 SelectionGoal::None,
1074 ),
1075 NextSectionEnd => (
1076 section_motion(map, point, times, Direction::Next, false),
1077 SelectionGoal::None,
1078 ),
1079 PreviousSectionStart => (
1080 section_motion(map, point, times, Direction::Prev, true),
1081 SelectionGoal::None,
1082 ),
1083 PreviousSectionEnd => (
1084 section_motion(map, point, times, Direction::Prev, false),
1085 SelectionGoal::None,
1086 ),
1087
1088 NextMethodStart => (
1089 method_motion(map, point, times, Direction::Next, true),
1090 SelectionGoal::None,
1091 ),
1092 NextMethodEnd => (
1093 method_motion(map, point, times, Direction::Next, false),
1094 SelectionGoal::None,
1095 ),
1096 PreviousMethodStart => (
1097 method_motion(map, point, times, Direction::Prev, true),
1098 SelectionGoal::None,
1099 ),
1100 PreviousMethodEnd => (
1101 method_motion(map, point, times, Direction::Prev, false),
1102 SelectionGoal::None,
1103 ),
1104 NextComment => (
1105 comment_motion(map, point, times, Direction::Next),
1106 SelectionGoal::None,
1107 ),
1108 PreviousComment => (
1109 comment_motion(map, point, times, Direction::Prev),
1110 SelectionGoal::None,
1111 ),
1112 };
1113
1114 (new_point != point || infallible).then_some((new_point, goal))
1115 }
1116
1117 // Get the range value after self is applied to the specified selection.
1118 pub fn range(
1119 &self,
1120 map: &DisplaySnapshot,
1121 selection: Selection<DisplayPoint>,
1122 times: Option<usize>,
1123 text_layout_details: &TextLayoutDetails,
1124 ) -> Option<(Range<DisplayPoint>, MotionKind)> {
1125 if let Motion::ZedSearchResult {
1126 prior_selections,
1127 new_selections,
1128 } = self
1129 {
1130 if let Some((prior_selection, new_selection)) =
1131 prior_selections.first().zip(new_selections.first())
1132 {
1133 let start = prior_selection
1134 .start
1135 .to_display_point(map)
1136 .min(new_selection.start.to_display_point(map));
1137 let end = new_selection
1138 .end
1139 .to_display_point(map)
1140 .max(prior_selection.end.to_display_point(map));
1141
1142 if start < end {
1143 return Some((start..end, MotionKind::Exclusive));
1144 } else {
1145 return Some((end..start, MotionKind::Exclusive));
1146 }
1147 } else {
1148 return None;
1149 }
1150 }
1151
1152 let (new_head, goal) = self.move_point(
1153 map,
1154 selection.head(),
1155 selection.goal,
1156 times,
1157 text_layout_details,
1158 )?;
1159 let mut selection = selection.clone();
1160 selection.set_head(new_head, goal);
1161
1162 let mut kind = self.default_kind();
1163
1164 if let Motion::NextWordStart {
1165 ignore_punctuation: _,
1166 } = self
1167 {
1168 // Another special case: When using the "w" motion in combination with an
1169 // operator and the last word moved over is at the end of a line, the end of
1170 // that word becomes the end of the operated text, not the first word in the
1171 // next line.
1172 let start = selection.start.to_point(map);
1173 let end = selection.end.to_point(map);
1174 let start_row = MultiBufferRow(selection.start.to_point(map).row);
1175 if end.row > start.row {
1176 selection.end = Point::new(start_row.0, map.buffer_snapshot.line_len(start_row))
1177 .to_display_point(map);
1178
1179 // a bit of a hack, we need `cw` on a blank line to not delete the newline,
1180 // but dw on a blank line should. The `Linewise` returned from this method
1181 // causes the `d` operator to include the trailing newline.
1182 if selection.start == selection.end {
1183 return Some((selection.start..selection.end, MotionKind::Linewise));
1184 }
1185 }
1186 } else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() {
1187 let start_point = selection.start.to_point(map);
1188 let mut end_point = selection.end.to_point(map);
1189
1190 if end_point.row > start_point.row {
1191 let first_non_blank_of_start_row = map
1192 .line_indent_for_buffer_row(MultiBufferRow(start_point.row))
1193 .raw_len();
1194 // https://github.com/neovim/neovim/blob/ee143aaf65a0e662c42c636aa4a959682858b3e7/src/nvim/ops.c#L6178-L6203
1195 if end_point.column == 0 {
1196 // If the motion is exclusive and the end of the motion is in column 1, the
1197 // end of the motion is moved to the end of the previous line and the motion
1198 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
1199 // but "d}" will not include that line.
1200 //
1201 // If the motion is exclusive, the end of the motion is in column 1 and the
1202 // start of the motion was at or before the first non-blank in the line, the
1203 // motion becomes linewise. Example: If a paragraph begins with some blanks
1204 // and you do "d}" while standing on the first non-blank, all the lines of
1205 // the paragraph are deleted, including the blanks.
1206 if start_point.column <= first_non_blank_of_start_row {
1207 kind = MotionKind::Linewise;
1208 } else {
1209 kind = MotionKind::Inclusive;
1210 }
1211 end_point.row -= 1;
1212 end_point.column = 0;
1213 selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
1214 }
1215 }
1216 } else if kind == MotionKind::Inclusive {
1217 selection.end = movement::saturating_right(map, selection.end)
1218 }
1219
1220 if kind == MotionKind::Linewise {
1221 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
1222 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
1223 }
1224 Some((selection.start..selection.end, kind))
1225 }
1226
1227 // Expands a selection using self for an operator
1228 pub fn expand_selection(
1229 &self,
1230 map: &DisplaySnapshot,
1231 selection: &mut Selection<DisplayPoint>,
1232 times: Option<usize>,
1233 text_layout_details: &TextLayoutDetails,
1234 ) -> Option<MotionKind> {
1235 let (range, kind) = self.range(map, selection.clone(), times, text_layout_details)?;
1236 selection.start = range.start;
1237 selection.end = range.end;
1238 Some(kind)
1239 }
1240}
1241
1242fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1243 for _ in 0..times {
1244 point = movement::saturating_left(map, point);
1245 if point.column() == 0 {
1246 break;
1247 }
1248 }
1249 point
1250}
1251
1252pub(crate) fn wrapping_left(
1253 map: &DisplaySnapshot,
1254 mut point: DisplayPoint,
1255 times: usize,
1256) -> DisplayPoint {
1257 for _ in 0..times {
1258 point = movement::left(map, point);
1259 if point.is_zero() {
1260 break;
1261 }
1262 }
1263 point
1264}
1265
1266fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1267 for _ in 0..times {
1268 point = wrapping_right_single(map, point);
1269 if point == map.max_point() {
1270 break;
1271 }
1272 }
1273 point
1274}
1275
1276fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
1277 let mut next_point = point;
1278 *next_point.column_mut() += 1;
1279 next_point = map.clip_point(next_point, Bias::Right);
1280 if next_point == point {
1281 if next_point.row() == map.max_point().row() {
1282 next_point
1283 } else {
1284 DisplayPoint::new(next_point.row().next_row(), 0)
1285 }
1286 } else {
1287 next_point
1288 }
1289}
1290
1291pub(crate) fn start_of_relative_buffer_row(
1292 map: &DisplaySnapshot,
1293 point: DisplayPoint,
1294 times: isize,
1295) -> DisplayPoint {
1296 let start = map.display_point_to_fold_point(point, Bias::Left);
1297 let target = start.row() as isize + times;
1298 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1299
1300 map.clip_point(
1301 map.fold_point_to_display_point(
1302 map.fold_snapshot
1303 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1304 ),
1305 Bias::Right,
1306 )
1307}
1308
1309fn up_down_buffer_rows(
1310 map: &DisplaySnapshot,
1311 mut point: DisplayPoint,
1312 mut goal: SelectionGoal,
1313 mut times: isize,
1314 text_layout_details: &TextLayoutDetails,
1315) -> (DisplayPoint, SelectionGoal) {
1316 let bias = if times < 0 { Bias::Left } else { Bias::Right };
1317
1318 while map.is_folded_buffer_header(point.row()) {
1319 if times < 0 {
1320 (point, _) = movement::up(map, point, goal, true, text_layout_details);
1321 times += 1;
1322 } else if times > 0 {
1323 (point, _) = movement::down(map, point, goal, true, text_layout_details);
1324 times -= 1;
1325 } else {
1326 break;
1327 }
1328 }
1329
1330 let start = map.display_point_to_fold_point(point, Bias::Left);
1331 let begin_folded_line = map.fold_point_to_display_point(
1332 map.fold_snapshot
1333 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1334 );
1335 let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1336
1337 let (goal_wrap, goal_x) = match goal {
1338 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1339 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1340 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1341 _ => {
1342 let x = map.x_for_display_point(point, text_layout_details);
1343 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1344 (select_nth_wrapped_row, x.0)
1345 }
1346 };
1347
1348 let target = start.row() as isize + times;
1349 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1350
1351 let mut begin_folded_line = map.fold_point_to_display_point(
1352 map.fold_snapshot
1353 .clip_point(FoldPoint::new(new_row, 0), bias),
1354 );
1355
1356 let mut i = 0;
1357 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1358 let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1359 if map
1360 .display_point_to_fold_point(next_folded_line, bias)
1361 .row()
1362 == new_row
1363 {
1364 i += 1;
1365 begin_folded_line = next_folded_line;
1366 } else {
1367 break;
1368 }
1369 }
1370
1371 let new_col = if i == goal_wrap {
1372 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1373 } else {
1374 map.line_len(begin_folded_line.row())
1375 };
1376
1377 (
1378 map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
1379 goal,
1380 )
1381}
1382
1383fn down_display(
1384 map: &DisplaySnapshot,
1385 mut point: DisplayPoint,
1386 mut goal: SelectionGoal,
1387 times: usize,
1388 text_layout_details: &TextLayoutDetails,
1389) -> (DisplayPoint, SelectionGoal) {
1390 for _ in 0..times {
1391 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1392 }
1393
1394 (point, goal)
1395}
1396
1397fn up_display(
1398 map: &DisplaySnapshot,
1399 mut point: DisplayPoint,
1400 mut goal: SelectionGoal,
1401 times: usize,
1402 text_layout_details: &TextLayoutDetails,
1403) -> (DisplayPoint, SelectionGoal) {
1404 for _ in 0..times {
1405 (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1406 }
1407
1408 (point, goal)
1409}
1410
1411pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1412 for _ in 0..times {
1413 let new_point = movement::saturating_right(map, point);
1414 if point == new_point {
1415 break;
1416 }
1417 point = new_point;
1418 }
1419 point
1420}
1421
1422pub(crate) fn next_char(
1423 map: &DisplaySnapshot,
1424 point: DisplayPoint,
1425 allow_cross_newline: bool,
1426) -> DisplayPoint {
1427 let mut new_point = point;
1428 let mut max_column = map.line_len(new_point.row());
1429 if !allow_cross_newline {
1430 max_column -= 1;
1431 }
1432 if new_point.column() < max_column {
1433 *new_point.column_mut() += 1;
1434 } else if new_point < map.max_point() && allow_cross_newline {
1435 *new_point.row_mut() += 1;
1436 *new_point.column_mut() = 0;
1437 }
1438 map.clip_ignoring_line_ends(new_point, Bias::Right)
1439}
1440
1441pub(crate) fn next_word_start(
1442 map: &DisplaySnapshot,
1443 mut point: DisplayPoint,
1444 ignore_punctuation: bool,
1445 times: usize,
1446) -> DisplayPoint {
1447 let classifier = map
1448 .buffer_snapshot
1449 .char_classifier_at(point.to_point(map))
1450 .ignore_punctuation(ignore_punctuation);
1451 for _ in 0..times {
1452 let mut crossed_newline = false;
1453 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1454 let left_kind = classifier.kind(left);
1455 let right_kind = classifier.kind(right);
1456 let at_newline = right == '\n';
1457
1458 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1459 || at_newline && crossed_newline
1460 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1461
1462 crossed_newline |= at_newline;
1463 found
1464 });
1465 if point == new_point {
1466 break;
1467 }
1468 point = new_point;
1469 }
1470 point
1471}
1472
1473pub(crate) fn next_word_end(
1474 map: &DisplaySnapshot,
1475 mut point: DisplayPoint,
1476 ignore_punctuation: bool,
1477 times: usize,
1478 allow_cross_newline: bool,
1479) -> DisplayPoint {
1480 let classifier = map
1481 .buffer_snapshot
1482 .char_classifier_at(point.to_point(map))
1483 .ignore_punctuation(ignore_punctuation);
1484 for _ in 0..times {
1485 let new_point = next_char(map, point, allow_cross_newline);
1486 let mut need_next_char = false;
1487 let new_point = movement::find_boundary_exclusive(
1488 map,
1489 new_point,
1490 FindRange::MultiLine,
1491 |left, right| {
1492 let left_kind = classifier.kind(left);
1493 let right_kind = classifier.kind(right);
1494 let at_newline = right == '\n';
1495
1496 if !allow_cross_newline && at_newline {
1497 need_next_char = true;
1498 return true;
1499 }
1500
1501 left_kind != right_kind && left_kind != CharKind::Whitespace
1502 },
1503 );
1504 let new_point = if need_next_char {
1505 next_char(map, new_point, true)
1506 } else {
1507 new_point
1508 };
1509 let new_point = map.clip_point(new_point, Bias::Left);
1510 if point == new_point {
1511 break;
1512 }
1513 point = new_point;
1514 }
1515 point
1516}
1517
1518fn previous_word_start(
1519 map: &DisplaySnapshot,
1520 mut point: DisplayPoint,
1521 ignore_punctuation: bool,
1522 times: usize,
1523) -> DisplayPoint {
1524 let classifier = map
1525 .buffer_snapshot
1526 .char_classifier_at(point.to_point(map))
1527 .ignore_punctuation(ignore_punctuation);
1528 for _ in 0..times {
1529 // This works even though find_preceding_boundary is called for every character in the line containing
1530 // cursor because the newline is checked only once.
1531 let new_point = movement::find_preceding_boundary_display_point(
1532 map,
1533 point,
1534 FindRange::MultiLine,
1535 |left, right| {
1536 let left_kind = classifier.kind(left);
1537 let right_kind = classifier.kind(right);
1538
1539 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1540 },
1541 );
1542 if point == new_point {
1543 break;
1544 }
1545 point = new_point;
1546 }
1547 point
1548}
1549
1550fn previous_word_end(
1551 map: &DisplaySnapshot,
1552 point: DisplayPoint,
1553 ignore_punctuation: bool,
1554 times: usize,
1555) -> DisplayPoint {
1556 let classifier = map
1557 .buffer_snapshot
1558 .char_classifier_at(point.to_point(map))
1559 .ignore_punctuation(ignore_punctuation);
1560 let mut point = point.to_point(map);
1561
1562 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1563 point.column += 1;
1564 }
1565 for _ in 0..times {
1566 let new_point = movement::find_preceding_boundary_point(
1567 &map.buffer_snapshot,
1568 point,
1569 FindRange::MultiLine,
1570 |left, right| {
1571 let left_kind = classifier.kind(left);
1572 let right_kind = classifier.kind(right);
1573 match (left_kind, right_kind) {
1574 (CharKind::Punctuation, CharKind::Whitespace)
1575 | (CharKind::Punctuation, CharKind::Word)
1576 | (CharKind::Word, CharKind::Whitespace)
1577 | (CharKind::Word, CharKind::Punctuation) => true,
1578 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1579 _ => false,
1580 }
1581 },
1582 );
1583 if new_point == point {
1584 break;
1585 }
1586 point = new_point;
1587 }
1588 movement::saturating_left(map, point.to_display_point(map))
1589}
1590
1591fn next_subword_start(
1592 map: &DisplaySnapshot,
1593 mut point: DisplayPoint,
1594 ignore_punctuation: bool,
1595 times: usize,
1596) -> DisplayPoint {
1597 let classifier = map
1598 .buffer_snapshot
1599 .char_classifier_at(point.to_point(map))
1600 .ignore_punctuation(ignore_punctuation);
1601 for _ in 0..times {
1602 let mut crossed_newline = false;
1603 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1604 let left_kind = classifier.kind(left);
1605 let right_kind = classifier.kind(right);
1606 let at_newline = right == '\n';
1607
1608 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1609 let is_subword_start =
1610 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1611
1612 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1613 || at_newline && crossed_newline
1614 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1615
1616 crossed_newline |= at_newline;
1617 found
1618 });
1619 if point == new_point {
1620 break;
1621 }
1622 point = new_point;
1623 }
1624 point
1625}
1626
1627pub(crate) fn next_subword_end(
1628 map: &DisplaySnapshot,
1629 mut point: DisplayPoint,
1630 ignore_punctuation: bool,
1631 times: usize,
1632 allow_cross_newline: bool,
1633) -> DisplayPoint {
1634 let classifier = map
1635 .buffer_snapshot
1636 .char_classifier_at(point.to_point(map))
1637 .ignore_punctuation(ignore_punctuation);
1638 for _ in 0..times {
1639 let new_point = next_char(map, point, allow_cross_newline);
1640
1641 let mut crossed_newline = false;
1642 let mut need_backtrack = false;
1643 let new_point =
1644 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1645 let left_kind = classifier.kind(left);
1646 let right_kind = classifier.kind(right);
1647 let at_newline = right == '\n';
1648
1649 if !allow_cross_newline && at_newline {
1650 return true;
1651 }
1652
1653 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1654 let is_subword_end =
1655 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1656
1657 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1658
1659 if found && (is_word_end || is_subword_end) {
1660 need_backtrack = true;
1661 }
1662
1663 crossed_newline |= at_newline;
1664 found
1665 });
1666 let mut new_point = map.clip_point(new_point, Bias::Left);
1667 if need_backtrack {
1668 *new_point.column_mut() -= 1;
1669 }
1670 let new_point = map.clip_point(new_point, Bias::Left);
1671 if point == new_point {
1672 break;
1673 }
1674 point = new_point;
1675 }
1676 point
1677}
1678
1679fn previous_subword_start(
1680 map: &DisplaySnapshot,
1681 mut point: DisplayPoint,
1682 ignore_punctuation: bool,
1683 times: usize,
1684) -> DisplayPoint {
1685 let classifier = map
1686 .buffer_snapshot
1687 .char_classifier_at(point.to_point(map))
1688 .ignore_punctuation(ignore_punctuation);
1689 for _ in 0..times {
1690 let mut crossed_newline = false;
1691 // This works even though find_preceding_boundary is called for every character in the line containing
1692 // cursor because the newline is checked only once.
1693 let new_point = movement::find_preceding_boundary_display_point(
1694 map,
1695 point,
1696 FindRange::MultiLine,
1697 |left, right| {
1698 let left_kind = classifier.kind(left);
1699 let right_kind = classifier.kind(right);
1700 let at_newline = right == '\n';
1701
1702 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1703 let is_subword_start =
1704 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1705
1706 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1707 || at_newline && crossed_newline
1708 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1709
1710 crossed_newline |= at_newline;
1711
1712 found
1713 },
1714 );
1715 if point == new_point {
1716 break;
1717 }
1718 point = new_point;
1719 }
1720 point
1721}
1722
1723fn previous_subword_end(
1724 map: &DisplaySnapshot,
1725 point: DisplayPoint,
1726 ignore_punctuation: bool,
1727 times: usize,
1728) -> DisplayPoint {
1729 let classifier = map
1730 .buffer_snapshot
1731 .char_classifier_at(point.to_point(map))
1732 .ignore_punctuation(ignore_punctuation);
1733 let mut point = point.to_point(map);
1734
1735 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1736 point.column += 1;
1737 }
1738 for _ in 0..times {
1739 let new_point = movement::find_preceding_boundary_point(
1740 &map.buffer_snapshot,
1741 point,
1742 FindRange::MultiLine,
1743 |left, right| {
1744 let left_kind = classifier.kind(left);
1745 let right_kind = classifier.kind(right);
1746
1747 let is_subword_end =
1748 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1749
1750 if is_subword_end {
1751 return true;
1752 }
1753
1754 match (left_kind, right_kind) {
1755 (CharKind::Word, CharKind::Whitespace)
1756 | (CharKind::Word, CharKind::Punctuation) => true,
1757 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1758 _ => false,
1759 }
1760 },
1761 );
1762 if new_point == point {
1763 break;
1764 }
1765 point = new_point;
1766 }
1767 movement::saturating_left(map, point.to_display_point(map))
1768}
1769
1770pub(crate) fn first_non_whitespace(
1771 map: &DisplaySnapshot,
1772 display_lines: bool,
1773 from: DisplayPoint,
1774) -> DisplayPoint {
1775 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1776 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1777 for (ch, offset) in map.buffer_chars_at(start_offset) {
1778 if ch == '\n' {
1779 return from;
1780 }
1781
1782 start_offset = offset;
1783
1784 if classifier.kind(ch) != CharKind::Whitespace {
1785 break;
1786 }
1787 }
1788
1789 start_offset.to_display_point(map)
1790}
1791
1792pub(crate) fn last_non_whitespace(
1793 map: &DisplaySnapshot,
1794 from: DisplayPoint,
1795 count: usize,
1796) -> DisplayPoint {
1797 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1798 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1799
1800 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1801 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1802 if classifier.kind(ch) != CharKind::Whitespace {
1803 return end_of_line.to_display_point(map);
1804 }
1805 }
1806
1807 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1808 if ch == '\n' {
1809 break;
1810 }
1811 end_of_line = offset;
1812 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1813 break;
1814 }
1815 }
1816
1817 end_of_line.to_display_point(map)
1818}
1819
1820pub(crate) fn start_of_line(
1821 map: &DisplaySnapshot,
1822 display_lines: bool,
1823 point: DisplayPoint,
1824) -> DisplayPoint {
1825 if display_lines {
1826 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1827 } else {
1828 map.prev_line_boundary(point.to_point(map)).1
1829 }
1830}
1831
1832pub(crate) fn end_of_line(
1833 map: &DisplaySnapshot,
1834 display_lines: bool,
1835 mut point: DisplayPoint,
1836 times: usize,
1837) -> DisplayPoint {
1838 if times > 1 {
1839 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1840 }
1841 if display_lines {
1842 map.clip_point(
1843 DisplayPoint::new(point.row(), map.line_len(point.row())),
1844 Bias::Left,
1845 )
1846 } else {
1847 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1848 }
1849}
1850
1851pub(crate) fn sentence_backwards(
1852 map: &DisplaySnapshot,
1853 point: DisplayPoint,
1854 mut times: usize,
1855) -> DisplayPoint {
1856 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1857 let mut chars = map.reverse_buffer_chars_at(start).peekable();
1858
1859 let mut was_newline = map
1860 .buffer_chars_at(start)
1861 .next()
1862 .is_some_and(|(c, _)| c == '\n');
1863
1864 while let Some((ch, offset)) = chars.next() {
1865 let start_of_next_sentence = if was_newline && ch == '\n' {
1866 Some(offset + ch.len_utf8())
1867 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1868 Some(next_non_blank(map, offset + ch.len_utf8()))
1869 } else if ch == '.' || ch == '?' || ch == '!' {
1870 start_of_next_sentence(map, offset + ch.len_utf8())
1871 } else {
1872 None
1873 };
1874
1875 if let Some(start_of_next_sentence) = start_of_next_sentence {
1876 if start_of_next_sentence < start {
1877 times = times.saturating_sub(1);
1878 }
1879 if times == 0 || offset == 0 {
1880 return map.clip_point(
1881 start_of_next_sentence
1882 .to_offset(&map.buffer_snapshot)
1883 .to_display_point(map),
1884 Bias::Left,
1885 );
1886 }
1887 }
1888 if was_newline {
1889 start = offset;
1890 }
1891 was_newline = ch == '\n';
1892 }
1893
1894 DisplayPoint::zero()
1895}
1896
1897pub(crate) fn sentence_forwards(
1898 map: &DisplaySnapshot,
1899 point: DisplayPoint,
1900 mut times: usize,
1901) -> DisplayPoint {
1902 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1903 let mut chars = map.buffer_chars_at(start).peekable();
1904
1905 let mut was_newline = map
1906 .reverse_buffer_chars_at(start)
1907 .next()
1908 .is_some_and(|(c, _)| c == '\n')
1909 && chars.peek().is_some_and(|(c, _)| *c == '\n');
1910
1911 while let Some((ch, offset)) = chars.next() {
1912 if was_newline && ch == '\n' {
1913 continue;
1914 }
1915 let start_of_next_sentence = if was_newline {
1916 Some(next_non_blank(map, offset))
1917 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1918 Some(next_non_blank(map, offset + ch.len_utf8()))
1919 } else if ch == '.' || ch == '?' || ch == '!' {
1920 start_of_next_sentence(map, offset + ch.len_utf8())
1921 } else {
1922 None
1923 };
1924
1925 if let Some(start_of_next_sentence) = start_of_next_sentence {
1926 times = times.saturating_sub(1);
1927 if times == 0 {
1928 return map.clip_point(
1929 start_of_next_sentence
1930 .to_offset(&map.buffer_snapshot)
1931 .to_display_point(map),
1932 Bias::Right,
1933 );
1934 }
1935 }
1936
1937 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1938 }
1939
1940 map.max_point()
1941}
1942
1943fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1944 for (c, o) in map.buffer_chars_at(start) {
1945 if c == '\n' || !c.is_whitespace() {
1946 return o;
1947 }
1948 }
1949
1950 map.buffer_snapshot.len()
1951}
1952
1953// given the offset after a ., !, or ? find the start of the next sentence.
1954// if this is not a sentence boundary, returns None.
1955fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1956 let chars = map.buffer_chars_at(end_of_sentence);
1957 let mut seen_space = false;
1958
1959 for (char, offset) in chars {
1960 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1961 continue;
1962 }
1963
1964 if char == '\n' && seen_space {
1965 return Some(offset);
1966 } else if char.is_whitespace() {
1967 seen_space = true;
1968 } else if seen_space {
1969 return Some(offset);
1970 } else {
1971 return None;
1972 }
1973 }
1974
1975 Some(map.buffer_snapshot.len())
1976}
1977
1978fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
1979 let point = map.display_point_to_point(display_point, Bias::Left);
1980 let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
1981 return display_point;
1982 };
1983 let offset = excerpt.buffer().point_to_offset(
1984 excerpt
1985 .buffer()
1986 .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
1987 );
1988 let buffer_range = excerpt.buffer_range();
1989 if offset >= buffer_range.start && offset <= buffer_range.end {
1990 let point = map
1991 .buffer_snapshot
1992 .offset_to_point(excerpt.map_offset_from_buffer(offset));
1993 return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
1994 }
1995 let mut last_position = None;
1996 for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
1997 let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
1998 ..language::ToOffset::to_offset(&range.context.end, &buffer);
1999 if offset >= excerpt_range.start && offset <= excerpt_range.end {
2000 let text_anchor = buffer.anchor_after(offset);
2001 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2002 return anchor.to_display_point(map);
2003 } else if offset <= excerpt_range.start {
2004 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2005 return anchor.to_display_point(map);
2006 } else {
2007 last_position = Some(Anchor::in_buffer(
2008 excerpt,
2009 buffer.remote_id(),
2010 range.context.end,
2011 ));
2012 }
2013 }
2014
2015 let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2016 last_point.column = point.column;
2017
2018 map.clip_point(
2019 map.point_to_display_point(
2020 map.buffer_snapshot.clip_point(point, Bias::Left),
2021 Bias::Left,
2022 ),
2023 Bias::Left,
2024 )
2025}
2026
2027fn start_of_document(
2028 map: &DisplaySnapshot,
2029 display_point: DisplayPoint,
2030 maybe_times: Option<usize>,
2031) -> DisplayPoint {
2032 if let Some(times) = maybe_times {
2033 return go_to_line(map, display_point, times);
2034 }
2035
2036 let point = map.display_point_to_point(display_point, Bias::Left);
2037 let mut first_point = Point::zero();
2038 first_point.column = point.column;
2039
2040 map.clip_point(
2041 map.point_to_display_point(
2042 map.buffer_snapshot.clip_point(first_point, Bias::Left),
2043 Bias::Left,
2044 ),
2045 Bias::Left,
2046 )
2047}
2048
2049fn end_of_document(
2050 map: &DisplaySnapshot,
2051 display_point: DisplayPoint,
2052 maybe_times: Option<usize>,
2053) -> DisplayPoint {
2054 if let Some(times) = maybe_times {
2055 return go_to_line(map, display_point, times);
2056 };
2057 let point = map.display_point_to_point(display_point, Bias::Left);
2058 let mut last_point = map.buffer_snapshot.max_point();
2059 last_point.column = point.column;
2060
2061 map.clip_point(
2062 map.point_to_display_point(
2063 map.buffer_snapshot.clip_point(last_point, Bias::Left),
2064 Bias::Left,
2065 ),
2066 Bias::Left,
2067 )
2068}
2069
2070fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2071 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2072 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2073
2074 if head > outer.start && head < inner.start {
2075 let mut offset = inner.end.to_offset(map, Bias::Left);
2076 for c in map.buffer_snapshot.chars_at(offset) {
2077 if c == '/' || c == '\n' || c == '>' {
2078 return Some(offset.to_display_point(map));
2079 }
2080 offset += c.len_utf8();
2081 }
2082 } else {
2083 let mut offset = outer.start.to_offset(map, Bias::Left);
2084 for c in map.buffer_snapshot.chars_at(offset) {
2085 offset += c.len_utf8();
2086 if c == '<' || c == '\n' {
2087 return Some(offset.to_display_point(map));
2088 }
2089 }
2090 }
2091
2092 return None;
2093}
2094
2095fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2096 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2097 let display_point = map.clip_at_line_end(display_point);
2098 let point = display_point.to_point(map);
2099 let offset = point.to_offset(&map.buffer_snapshot);
2100
2101 // Ensure the range is contained by the current line.
2102 let mut line_end = map.next_line_boundary(point).0;
2103 if line_end == point {
2104 line_end = map.max_point().to_point(map);
2105 }
2106
2107 let line_range = map.prev_line_boundary(point).0..line_end;
2108 let visible_line_range =
2109 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2110 let ranges = map
2111 .buffer_snapshot
2112 .bracket_ranges(visible_line_range.clone());
2113 if let Some(ranges) = ranges {
2114 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2115 ..line_range.end.to_offset(&map.buffer_snapshot);
2116 let mut closest_pair_destination = None;
2117 let mut closest_distance = usize::MAX;
2118
2119 for (open_range, close_range) in ranges {
2120 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2121 if offset > open_range.start && offset < close_range.start {
2122 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2123 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2124 return display_point;
2125 }
2126 if let Some(tag) = matching_tag(map, display_point) {
2127 return tag;
2128 }
2129 } else if close_range.contains(&offset) {
2130 return open_range.start.to_display_point(map);
2131 } else if open_range.contains(&offset) {
2132 return (close_range.end - 1).to_display_point(map);
2133 }
2134 }
2135
2136 if (open_range.contains(&offset) || open_range.start >= offset)
2137 && line_range.contains(&open_range.start)
2138 {
2139 let distance = open_range.start.saturating_sub(offset);
2140 if distance < closest_distance {
2141 closest_pair_destination = Some(close_range.start);
2142 closest_distance = distance;
2143 continue;
2144 }
2145 }
2146
2147 if (close_range.contains(&offset) || close_range.start >= offset)
2148 && line_range.contains(&close_range.start)
2149 {
2150 let distance = close_range.start.saturating_sub(offset);
2151 if distance < closest_distance {
2152 closest_pair_destination = Some(open_range.start);
2153 closest_distance = distance;
2154 continue;
2155 }
2156 }
2157
2158 continue;
2159 }
2160
2161 closest_pair_destination
2162 .map(|destination| destination.to_display_point(map))
2163 .unwrap_or(display_point)
2164 } else {
2165 display_point
2166 }
2167}
2168
2169// Go to {count} percentage in the file, on the first
2170// non-blank in the line linewise. To compute the new
2171// line number this formula is used:
2172// ({count} * number-of-lines + 99) / 100
2173//
2174// https://neovim.io/doc/user/motion.html#N%25
2175fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2176 let total_lines = map.buffer_snapshot.max_point().row + 1;
2177 let target_line = (count * total_lines as usize).div_ceil(100);
2178 let target_point = DisplayPoint::new(
2179 DisplayRow(target_line.saturating_sub(1) as u32),
2180 point.column(),
2181 );
2182 map.clip_point(target_point, Bias::Left)
2183}
2184
2185fn unmatched_forward(
2186 map: &DisplaySnapshot,
2187 mut display_point: DisplayPoint,
2188 char: char,
2189 times: usize,
2190) -> DisplayPoint {
2191 for _ in 0..times {
2192 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2193 let point = display_point.to_point(map);
2194 let offset = point.to_offset(&map.buffer_snapshot);
2195
2196 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2197 let Some(ranges) = ranges else { break };
2198 let mut closest_closing_destination = None;
2199 let mut closest_distance = usize::MAX;
2200
2201 for (_, close_range) in ranges {
2202 if close_range.start > offset {
2203 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2204 if Some(char) == chars.next() {
2205 let distance = close_range.start - offset;
2206 if distance < closest_distance {
2207 closest_closing_destination = Some(close_range.start);
2208 closest_distance = distance;
2209 continue;
2210 }
2211 }
2212 }
2213 }
2214
2215 let new_point = closest_closing_destination
2216 .map(|destination| destination.to_display_point(map))
2217 .unwrap_or(display_point);
2218 if new_point == display_point {
2219 break;
2220 }
2221 display_point = new_point;
2222 }
2223 return display_point;
2224}
2225
2226fn unmatched_backward(
2227 map: &DisplaySnapshot,
2228 mut display_point: DisplayPoint,
2229 char: char,
2230 times: usize,
2231) -> DisplayPoint {
2232 for _ in 0..times {
2233 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2234 let point = display_point.to_point(map);
2235 let offset = point.to_offset(&map.buffer_snapshot);
2236
2237 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2238 let Some(ranges) = ranges else {
2239 break;
2240 };
2241
2242 let mut closest_starting_destination = None;
2243 let mut closest_distance = usize::MAX;
2244
2245 for (start_range, _) in ranges {
2246 if start_range.start < offset {
2247 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2248 if Some(char) == chars.next() {
2249 let distance = offset - start_range.start;
2250 if distance < closest_distance {
2251 closest_starting_destination = Some(start_range.start);
2252 closest_distance = distance;
2253 continue;
2254 }
2255 }
2256 }
2257 }
2258
2259 let new_point = closest_starting_destination
2260 .map(|destination| destination.to_display_point(map))
2261 .unwrap_or(display_point);
2262 if new_point == display_point {
2263 break;
2264 } else {
2265 display_point = new_point;
2266 }
2267 }
2268 display_point
2269}
2270
2271fn find_forward(
2272 map: &DisplaySnapshot,
2273 from: DisplayPoint,
2274 before: bool,
2275 target: char,
2276 times: usize,
2277 mode: FindRange,
2278 smartcase: bool,
2279) -> Option<DisplayPoint> {
2280 let mut to = from;
2281 let mut found = false;
2282
2283 for _ in 0..times {
2284 found = false;
2285 let new_to = find_boundary(map, to, mode, |_, right| {
2286 found = is_character_match(target, right, smartcase);
2287 found
2288 });
2289 if to == new_to {
2290 break;
2291 }
2292 to = new_to;
2293 }
2294
2295 if found {
2296 if before && to.column() > 0 {
2297 *to.column_mut() -= 1;
2298 Some(map.clip_point(to, Bias::Left))
2299 } else {
2300 Some(to)
2301 }
2302 } else {
2303 None
2304 }
2305}
2306
2307fn find_backward(
2308 map: &DisplaySnapshot,
2309 from: DisplayPoint,
2310 after: bool,
2311 target: char,
2312 times: usize,
2313 mode: FindRange,
2314 smartcase: bool,
2315) -> DisplayPoint {
2316 let mut to = from;
2317
2318 for _ in 0..times {
2319 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2320 is_character_match(target, right, smartcase)
2321 });
2322 if to == new_to {
2323 break;
2324 }
2325 to = new_to;
2326 }
2327
2328 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2329 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2330 if after {
2331 *to.column_mut() += 1;
2332 map.clip_point(to, Bias::Right)
2333 } else {
2334 to
2335 }
2336 } else {
2337 from
2338 }
2339}
2340
2341fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2342 if smartcase {
2343 if target.is_uppercase() {
2344 target == other
2345 } else {
2346 target == other.to_ascii_lowercase()
2347 }
2348 } else {
2349 target == other
2350 }
2351}
2352
2353fn sneak(
2354 map: &DisplaySnapshot,
2355 from: DisplayPoint,
2356 first_target: char,
2357 second_target: char,
2358 times: usize,
2359 smartcase: bool,
2360) -> Option<DisplayPoint> {
2361 let mut to = from;
2362 let mut found = false;
2363
2364 for _ in 0..times {
2365 found = false;
2366 let new_to = find_boundary(
2367 map,
2368 movement::right(map, to),
2369 FindRange::MultiLine,
2370 |left, right| {
2371 found = is_character_match(first_target, left, smartcase)
2372 && is_character_match(second_target, right, smartcase);
2373 found
2374 },
2375 );
2376 if to == new_to {
2377 break;
2378 }
2379 to = new_to;
2380 }
2381
2382 if found {
2383 Some(movement::left(map, to))
2384 } else {
2385 None
2386 }
2387}
2388
2389fn sneak_backward(
2390 map: &DisplaySnapshot,
2391 from: DisplayPoint,
2392 first_target: char,
2393 second_target: char,
2394 times: usize,
2395 smartcase: bool,
2396) -> Option<DisplayPoint> {
2397 let mut to = from;
2398 let mut found = false;
2399
2400 for _ in 0..times {
2401 found = false;
2402 let new_to =
2403 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2404 found = is_character_match(first_target, left, smartcase)
2405 && is_character_match(second_target, right, smartcase);
2406 found
2407 });
2408 if to == new_to {
2409 break;
2410 }
2411 to = new_to;
2412 }
2413
2414 if found {
2415 Some(movement::left(map, to))
2416 } else {
2417 None
2418 }
2419}
2420
2421fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2422 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2423 first_non_whitespace(map, false, correct_line)
2424}
2425
2426fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2427 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2428 first_non_whitespace(map, false, correct_line)
2429}
2430
2431fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2432 let correct_line = start_of_relative_buffer_row(map, point, 0);
2433 right(map, correct_line, times.saturating_sub(1))
2434}
2435
2436pub(crate) fn next_line_end(
2437 map: &DisplaySnapshot,
2438 mut point: DisplayPoint,
2439 times: usize,
2440) -> DisplayPoint {
2441 if times > 1 {
2442 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2443 }
2444 end_of_line(map, false, point, 1)
2445}
2446
2447fn window_top(
2448 map: &DisplaySnapshot,
2449 point: DisplayPoint,
2450 text_layout_details: &TextLayoutDetails,
2451 mut times: usize,
2452) -> (DisplayPoint, SelectionGoal) {
2453 let first_visible_line = text_layout_details
2454 .scroll_anchor
2455 .anchor
2456 .to_display_point(map);
2457
2458 if first_visible_line.row() != DisplayRow(0)
2459 && text_layout_details.vertical_scroll_margin as usize > times
2460 {
2461 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2462 }
2463
2464 if let Some(visible_rows) = text_layout_details.visible_rows {
2465 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2466 let new_row = (first_visible_line.row().0 + (times as u32))
2467 .min(bottom_row)
2468 .min(map.max_point().row().0);
2469 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2470
2471 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2472 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2473 } else {
2474 let new_row =
2475 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2476 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2477
2478 let new_point = DisplayPoint::new(new_row, new_col);
2479 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2480 }
2481}
2482
2483fn window_middle(
2484 map: &DisplaySnapshot,
2485 point: DisplayPoint,
2486 text_layout_details: &TextLayoutDetails,
2487) -> (DisplayPoint, SelectionGoal) {
2488 if let Some(visible_rows) = text_layout_details.visible_rows {
2489 let first_visible_line = text_layout_details
2490 .scroll_anchor
2491 .anchor
2492 .to_display_point(map);
2493
2494 let max_visible_rows =
2495 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2496
2497 let new_row =
2498 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2499 let new_row = DisplayRow(new_row);
2500 let new_col = point.column().min(map.line_len(new_row));
2501 let new_point = DisplayPoint::new(new_row, new_col);
2502 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2503 } else {
2504 (point, SelectionGoal::None)
2505 }
2506}
2507
2508fn window_bottom(
2509 map: &DisplaySnapshot,
2510 point: DisplayPoint,
2511 text_layout_details: &TextLayoutDetails,
2512 mut times: usize,
2513) -> (DisplayPoint, SelectionGoal) {
2514 if let Some(visible_rows) = text_layout_details.visible_rows {
2515 let first_visible_line = text_layout_details
2516 .scroll_anchor
2517 .anchor
2518 .to_display_point(map);
2519 let bottom_row = first_visible_line.row().0
2520 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2521 if bottom_row < map.max_point().row().0
2522 && text_layout_details.vertical_scroll_margin as usize > times
2523 {
2524 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2525 }
2526 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2527 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2528 {
2529 first_visible_line.row()
2530 } else {
2531 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2532 };
2533 let new_col = point.column().min(map.line_len(new_row));
2534 let new_point = DisplayPoint::new(new_row, new_col);
2535 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2536 } else {
2537 (point, SelectionGoal::None)
2538 }
2539}
2540
2541fn method_motion(
2542 map: &DisplaySnapshot,
2543 mut display_point: DisplayPoint,
2544 times: usize,
2545 direction: Direction,
2546 is_start: bool,
2547) -> DisplayPoint {
2548 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2549 return display_point;
2550 };
2551
2552 for _ in 0..times {
2553 let point = map.display_point_to_point(display_point, Bias::Left);
2554 let offset = point.to_offset(&map.buffer_snapshot);
2555 let range = if direction == Direction::Prev {
2556 0..offset
2557 } else {
2558 offset..buffer.len()
2559 };
2560
2561 let possibilities = buffer
2562 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2563 .filter_map(|(range, object)| {
2564 if !matches!(object, language::TextObject::AroundFunction) {
2565 return None;
2566 }
2567
2568 let relevant = if is_start { range.start } else { range.end };
2569 if direction == Direction::Prev && relevant < offset {
2570 Some(relevant)
2571 } else if direction == Direction::Next && relevant > offset + 1 {
2572 Some(relevant)
2573 } else {
2574 None
2575 }
2576 });
2577
2578 let dest = if direction == Direction::Prev {
2579 possibilities.max().unwrap_or(offset)
2580 } else {
2581 possibilities.min().unwrap_or(offset)
2582 };
2583 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2584 if new_point == display_point {
2585 break;
2586 }
2587 display_point = new_point;
2588 }
2589 display_point
2590}
2591
2592fn comment_motion(
2593 map: &DisplaySnapshot,
2594 mut display_point: DisplayPoint,
2595 times: usize,
2596 direction: Direction,
2597) -> DisplayPoint {
2598 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2599 return display_point;
2600 };
2601
2602 for _ in 0..times {
2603 let point = map.display_point_to_point(display_point, Bias::Left);
2604 let offset = point.to_offset(&map.buffer_snapshot);
2605 let range = if direction == Direction::Prev {
2606 0..offset
2607 } else {
2608 offset..buffer.len()
2609 };
2610
2611 let possibilities = buffer
2612 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2613 .filter_map(|(range, object)| {
2614 if !matches!(object, language::TextObject::AroundComment) {
2615 return None;
2616 }
2617
2618 let relevant = if direction == Direction::Prev {
2619 range.start
2620 } else {
2621 range.end
2622 };
2623 if direction == Direction::Prev && relevant < offset {
2624 Some(relevant)
2625 } else if direction == Direction::Next && relevant > offset + 1 {
2626 Some(relevant)
2627 } else {
2628 None
2629 }
2630 });
2631
2632 let dest = if direction == Direction::Prev {
2633 possibilities.max().unwrap_or(offset)
2634 } else {
2635 possibilities.min().unwrap_or(offset)
2636 };
2637 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2638 if new_point == display_point {
2639 break;
2640 }
2641 display_point = new_point;
2642 }
2643
2644 display_point
2645}
2646
2647fn section_motion(
2648 map: &DisplaySnapshot,
2649 mut display_point: DisplayPoint,
2650 times: usize,
2651 direction: Direction,
2652 is_start: bool,
2653) -> DisplayPoint {
2654 if map.buffer_snapshot.as_singleton().is_some() {
2655 for _ in 0..times {
2656 let offset = map
2657 .display_point_to_point(display_point, Bias::Left)
2658 .to_offset(&map.buffer_snapshot);
2659 let range = if direction == Direction::Prev {
2660 0..offset
2661 } else {
2662 offset..map.buffer_snapshot.len()
2663 };
2664
2665 // we set a max start depth here because we want a section to only be "top level"
2666 // similar to vim's default of '{' in the first column.
2667 // (and without it, ]] at the start of editor.rs is -very- slow)
2668 let mut possibilities = map
2669 .buffer_snapshot
2670 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2671 .filter(|(_, object)| {
2672 matches!(
2673 object,
2674 language::TextObject::AroundClass | language::TextObject::AroundFunction
2675 )
2676 })
2677 .collect::<Vec<_>>();
2678 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2679 let mut prev_end = None;
2680 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2681 if t == language::TextObject::AroundFunction
2682 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2683 {
2684 return None;
2685 }
2686 prev_end = Some(range.end);
2687
2688 let relevant = if is_start { range.start } else { range.end };
2689 if direction == Direction::Prev && relevant < offset {
2690 Some(relevant)
2691 } else if direction == Direction::Next && relevant > offset + 1 {
2692 Some(relevant)
2693 } else {
2694 None
2695 }
2696 });
2697
2698 let offset = if direction == Direction::Prev {
2699 possibilities.max().unwrap_or(0)
2700 } else {
2701 possibilities.min().unwrap_or(map.buffer_snapshot.len())
2702 };
2703
2704 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2705 if new_point == display_point {
2706 break;
2707 }
2708 display_point = new_point;
2709 }
2710 return display_point;
2711 };
2712
2713 for _ in 0..times {
2714 let next_point = if is_start {
2715 movement::start_of_excerpt(map, display_point, direction)
2716 } else {
2717 movement::end_of_excerpt(map, display_point, direction)
2718 };
2719 if next_point == display_point {
2720 break;
2721 }
2722 display_point = next_point;
2723 }
2724
2725 display_point
2726}
2727
2728#[cfg(test)]
2729mod test {
2730
2731 use crate::{
2732 state::Mode,
2733 test::{NeovimBackedTestContext, VimTestContext},
2734 };
2735 use editor::display_map::Inlay;
2736 use indoc::indoc;
2737 use language::Point;
2738 use multi_buffer::MultiBufferRow;
2739
2740 #[gpui::test]
2741 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2742 let mut cx = NeovimBackedTestContext::new(cx).await;
2743
2744 let initial_state = indoc! {r"ˇabc
2745 def
2746
2747 paragraph
2748 the second
2749
2750
2751
2752 third and
2753 final"};
2754
2755 // goes down once
2756 cx.set_shared_state(initial_state).await;
2757 cx.simulate_shared_keystrokes("}").await;
2758 cx.shared_state().await.assert_eq(indoc! {r"abc
2759 def
2760 ˇ
2761 paragraph
2762 the second
2763
2764
2765
2766 third and
2767 final"});
2768
2769 // goes up once
2770 cx.simulate_shared_keystrokes("{").await;
2771 cx.shared_state().await.assert_eq(initial_state);
2772
2773 // goes down twice
2774 cx.simulate_shared_keystrokes("2 }").await;
2775 cx.shared_state().await.assert_eq(indoc! {r"abc
2776 def
2777
2778 paragraph
2779 the second
2780 ˇ
2781
2782
2783 third and
2784 final"});
2785
2786 // goes down over multiple blanks
2787 cx.simulate_shared_keystrokes("}").await;
2788 cx.shared_state().await.assert_eq(indoc! {r"abc
2789 def
2790
2791 paragraph
2792 the second
2793
2794
2795
2796 third and
2797 finaˇl"});
2798
2799 // goes up twice
2800 cx.simulate_shared_keystrokes("2 {").await;
2801 cx.shared_state().await.assert_eq(indoc! {r"abc
2802 def
2803 ˇ
2804 paragraph
2805 the second
2806
2807
2808
2809 third and
2810 final"});
2811 }
2812
2813 #[gpui::test]
2814 async fn test_matching(cx: &mut gpui::TestAppContext) {
2815 let mut cx = NeovimBackedTestContext::new(cx).await;
2816
2817 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2818 do(something(with<Types>.and_arrays[0, 2]))
2819 }"})
2820 .await;
2821 cx.simulate_shared_keystrokes("%").await;
2822 cx.shared_state()
2823 .await
2824 .assert_eq(indoc! {r"func (a stringˇ) {
2825 do(something(with<Types>.and_arrays[0, 2]))
2826 }"});
2827
2828 // test it works on the last character of the line
2829 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2830 do(something(with<Types>.and_arrays[0, 2]))
2831 }"})
2832 .await;
2833 cx.simulate_shared_keystrokes("%").await;
2834 cx.shared_state()
2835 .await
2836 .assert_eq(indoc! {r"func (a string) {
2837 do(something(with<Types>.and_arrays[0, 2]))
2838 ˇ}"});
2839
2840 // test it works on immediate nesting
2841 cx.set_shared_state("ˇ{()}").await;
2842 cx.simulate_shared_keystrokes("%").await;
2843 cx.shared_state().await.assert_eq("{()ˇ}");
2844 cx.simulate_shared_keystrokes("%").await;
2845 cx.shared_state().await.assert_eq("ˇ{()}");
2846
2847 // test it works on immediate nesting inside braces
2848 cx.set_shared_state("{\n ˇ{()}\n}").await;
2849 cx.simulate_shared_keystrokes("%").await;
2850 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
2851
2852 // test it jumps to the next paren on a line
2853 cx.set_shared_state("func ˇboop() {\n}").await;
2854 cx.simulate_shared_keystrokes("%").await;
2855 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2856 }
2857
2858 #[gpui::test]
2859 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2860 let mut cx = NeovimBackedTestContext::new(cx).await;
2861
2862 // test it works with curly braces
2863 cx.set_shared_state(indoc! {r"func (a string) {
2864 do(something(with<Types>.anˇd_arrays[0, 2]))
2865 }"})
2866 .await;
2867 cx.simulate_shared_keystrokes("] }").await;
2868 cx.shared_state()
2869 .await
2870 .assert_eq(indoc! {r"func (a string) {
2871 do(something(with<Types>.and_arrays[0, 2]))
2872 ˇ}"});
2873
2874 // test it works with brackets
2875 cx.set_shared_state(indoc! {r"func (a string) {
2876 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2877 }"})
2878 .await;
2879 cx.simulate_shared_keystrokes("] )").await;
2880 cx.shared_state()
2881 .await
2882 .assert_eq(indoc! {r"func (a string) {
2883 do(something(with<Types>.and_arrays[0, 2])ˇ)
2884 }"});
2885
2886 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2887 .await;
2888 cx.simulate_shared_keystrokes("] )").await;
2889 cx.shared_state()
2890 .await
2891 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2892
2893 // test it works on immediate nesting
2894 cx.set_shared_state("{ˇ {}{}}").await;
2895 cx.simulate_shared_keystrokes("] }").await;
2896 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2897 cx.set_shared_state("(ˇ ()())").await;
2898 cx.simulate_shared_keystrokes("] )").await;
2899 cx.shared_state().await.assert_eq("( ()()ˇ)");
2900
2901 // test it works on immediate nesting inside braces
2902 cx.set_shared_state("{\n ˇ {()}\n}").await;
2903 cx.simulate_shared_keystrokes("] }").await;
2904 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
2905 cx.set_shared_state("(\n ˇ {()}\n)").await;
2906 cx.simulate_shared_keystrokes("] )").await;
2907 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
2908 }
2909
2910 #[gpui::test]
2911 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2912 let mut cx = NeovimBackedTestContext::new(cx).await;
2913
2914 // test it works with curly braces
2915 cx.set_shared_state(indoc! {r"func (a string) {
2916 do(something(with<Types>.anˇd_arrays[0, 2]))
2917 }"})
2918 .await;
2919 cx.simulate_shared_keystrokes("[ {").await;
2920 cx.shared_state()
2921 .await
2922 .assert_eq(indoc! {r"func (a string) ˇ{
2923 do(something(with<Types>.and_arrays[0, 2]))
2924 }"});
2925
2926 // test it works with brackets
2927 cx.set_shared_state(indoc! {r"func (a string) {
2928 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2929 }"})
2930 .await;
2931 cx.simulate_shared_keystrokes("[ (").await;
2932 cx.shared_state()
2933 .await
2934 .assert_eq(indoc! {r"func (a string) {
2935 doˇ(something(with<Types>.and_arrays[0, 2]))
2936 }"});
2937
2938 // test it works on immediate nesting
2939 cx.set_shared_state("{{}{} ˇ }").await;
2940 cx.simulate_shared_keystrokes("[ {").await;
2941 cx.shared_state().await.assert_eq("ˇ{{}{} }");
2942 cx.set_shared_state("(()() ˇ )").await;
2943 cx.simulate_shared_keystrokes("[ (").await;
2944 cx.shared_state().await.assert_eq("ˇ(()() )");
2945
2946 // test it works on immediate nesting inside braces
2947 cx.set_shared_state("{\n {()} ˇ\n}").await;
2948 cx.simulate_shared_keystrokes("[ {").await;
2949 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
2950 cx.set_shared_state("(\n {()} ˇ\n)").await;
2951 cx.simulate_shared_keystrokes("[ (").await;
2952 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
2953 }
2954
2955 #[gpui::test]
2956 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2957 let mut cx = NeovimBackedTestContext::new_html(cx).await;
2958
2959 cx.neovim.exec("set filetype=html").await;
2960
2961 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2962 cx.simulate_shared_keystrokes("%").await;
2963 cx.shared_state()
2964 .await
2965 .assert_eq(indoc! {r"<body><ˇ/body>"});
2966 cx.simulate_shared_keystrokes("%").await;
2967
2968 // test jumping backwards
2969 cx.shared_state()
2970 .await
2971 .assert_eq(indoc! {r"<ˇbody></body>"});
2972
2973 // test self-closing tags
2974 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2975 cx.simulate_shared_keystrokes("%").await;
2976 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2977
2978 // test tag with attributes
2979 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2980 </div>
2981 "})
2982 .await;
2983 cx.simulate_shared_keystrokes("%").await;
2984 cx.shared_state()
2985 .await
2986 .assert_eq(indoc! {r"<div class='test' id='main'>
2987 <ˇ/div>
2988 "});
2989
2990 // test multi-line self-closing tag
2991 cx.set_shared_state(indoc! {r#"<a>
2992 <br
2993 test = "test"
2994 /ˇ>
2995 </a>"#})
2996 .await;
2997 cx.simulate_shared_keystrokes("%").await;
2998 cx.shared_state().await.assert_eq(indoc! {r#"<a>
2999 ˇ<br
3000 test = "test"
3001 />
3002 </a>"#});
3003 }
3004
3005 #[gpui::test]
3006 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3007 let mut cx = NeovimBackedTestContext::new(cx).await;
3008
3009 // f and F
3010 cx.set_shared_state("ˇone two three four").await;
3011 cx.simulate_shared_keystrokes("f o").await;
3012 cx.shared_state().await.assert_eq("one twˇo three four");
3013 cx.simulate_shared_keystrokes(",").await;
3014 cx.shared_state().await.assert_eq("ˇone two three four");
3015 cx.simulate_shared_keystrokes("2 ;").await;
3016 cx.shared_state().await.assert_eq("one two three fˇour");
3017 cx.simulate_shared_keystrokes("shift-f e").await;
3018 cx.shared_state().await.assert_eq("one two threˇe four");
3019 cx.simulate_shared_keystrokes("2 ;").await;
3020 cx.shared_state().await.assert_eq("onˇe two three four");
3021 cx.simulate_shared_keystrokes(",").await;
3022 cx.shared_state().await.assert_eq("one two thrˇee four");
3023
3024 // t and T
3025 cx.set_shared_state("ˇone two three four").await;
3026 cx.simulate_shared_keystrokes("t o").await;
3027 cx.shared_state().await.assert_eq("one tˇwo three four");
3028 cx.simulate_shared_keystrokes(",").await;
3029 cx.shared_state().await.assert_eq("oˇne two three four");
3030 cx.simulate_shared_keystrokes("2 ;").await;
3031 cx.shared_state().await.assert_eq("one two three ˇfour");
3032 cx.simulate_shared_keystrokes("shift-t e").await;
3033 cx.shared_state().await.assert_eq("one two threeˇ four");
3034 cx.simulate_shared_keystrokes("3 ;").await;
3035 cx.shared_state().await.assert_eq("oneˇ two three four");
3036 cx.simulate_shared_keystrokes(",").await;
3037 cx.shared_state().await.assert_eq("one two thˇree four");
3038 }
3039
3040 #[gpui::test]
3041 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3042 let mut cx = NeovimBackedTestContext::new(cx).await;
3043 let initial_state = indoc! {r"something(ˇfoo)"};
3044 cx.set_shared_state(initial_state).await;
3045 cx.simulate_shared_keystrokes("}").await;
3046 cx.shared_state().await.assert_eq("something(fooˇ)");
3047 }
3048
3049 #[gpui::test]
3050 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3051 let mut cx = NeovimBackedTestContext::new(cx).await;
3052 cx.set_shared_state("ˇone\n two\nthree").await;
3053 cx.simulate_shared_keystrokes("enter").await;
3054 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3055 }
3056
3057 #[gpui::test]
3058 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3059 let mut cx = NeovimBackedTestContext::new(cx).await;
3060 cx.set_shared_state("ˇ one\n two \nthree").await;
3061 cx.simulate_shared_keystrokes("g _").await;
3062 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3063
3064 cx.set_shared_state("ˇ one \n two \nthree").await;
3065 cx.simulate_shared_keystrokes("g _").await;
3066 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3067 cx.simulate_shared_keystrokes("2 g _").await;
3068 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3069 }
3070
3071 #[gpui::test]
3072 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3073 let mut cx = NeovimBackedTestContext::new(cx).await;
3074 let initial_state = indoc! {r"abc
3075 def
3076 paragraph
3077 the second
3078 third ˇand
3079 final"};
3080
3081 cx.set_shared_state(initial_state).await;
3082 cx.simulate_shared_keystrokes("shift-h").await;
3083 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3084 def
3085 paragraph
3086 the second
3087 third and
3088 final"});
3089
3090 // clip point
3091 cx.set_shared_state(indoc! {r"
3092 1 2 3
3093 4 5 6
3094 7 8 ˇ9
3095 "})
3096 .await;
3097 cx.simulate_shared_keystrokes("shift-h").await;
3098 cx.shared_state().await.assert_eq(indoc! {"
3099 1 2 ˇ3
3100 4 5 6
3101 7 8 9
3102 "});
3103
3104 cx.set_shared_state(indoc! {r"
3105 1 2 3
3106 4 5 6
3107 ˇ7 8 9
3108 "})
3109 .await;
3110 cx.simulate_shared_keystrokes("shift-h").await;
3111 cx.shared_state().await.assert_eq(indoc! {"
3112 ˇ1 2 3
3113 4 5 6
3114 7 8 9
3115 "});
3116
3117 cx.set_shared_state(indoc! {r"
3118 1 2 3
3119 4 5 ˇ6
3120 7 8 9"})
3121 .await;
3122 cx.simulate_shared_keystrokes("9 shift-h").await;
3123 cx.shared_state().await.assert_eq(indoc! {"
3124 1 2 3
3125 4 5 6
3126 7 8 ˇ9"});
3127 }
3128
3129 #[gpui::test]
3130 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3131 let mut cx = NeovimBackedTestContext::new(cx).await;
3132 let initial_state = indoc! {r"abˇc
3133 def
3134 paragraph
3135 the second
3136 third and
3137 final"};
3138
3139 cx.set_shared_state(initial_state).await;
3140 cx.simulate_shared_keystrokes("shift-m").await;
3141 cx.shared_state().await.assert_eq(indoc! {r"abc
3142 def
3143 paˇragraph
3144 the second
3145 third and
3146 final"});
3147
3148 cx.set_shared_state(indoc! {r"
3149 1 2 3
3150 4 5 6
3151 7 8 ˇ9
3152 "})
3153 .await;
3154 cx.simulate_shared_keystrokes("shift-m").await;
3155 cx.shared_state().await.assert_eq(indoc! {"
3156 1 2 3
3157 4 5 ˇ6
3158 7 8 9
3159 "});
3160 cx.set_shared_state(indoc! {r"
3161 1 2 3
3162 4 5 6
3163 ˇ7 8 9
3164 "})
3165 .await;
3166 cx.simulate_shared_keystrokes("shift-m").await;
3167 cx.shared_state().await.assert_eq(indoc! {"
3168 1 2 3
3169 ˇ4 5 6
3170 7 8 9
3171 "});
3172 cx.set_shared_state(indoc! {r"
3173 ˇ1 2 3
3174 4 5 6
3175 7 8 9
3176 "})
3177 .await;
3178 cx.simulate_shared_keystrokes("shift-m").await;
3179 cx.shared_state().await.assert_eq(indoc! {"
3180 1 2 3
3181 ˇ4 5 6
3182 7 8 9
3183 "});
3184 cx.set_shared_state(indoc! {r"
3185 1 2 3
3186 ˇ4 5 6
3187 7 8 9
3188 "})
3189 .await;
3190 cx.simulate_shared_keystrokes("shift-m").await;
3191 cx.shared_state().await.assert_eq(indoc! {"
3192 1 2 3
3193 ˇ4 5 6
3194 7 8 9
3195 "});
3196 cx.set_shared_state(indoc! {r"
3197 1 2 3
3198 4 5 ˇ6
3199 7 8 9
3200 "})
3201 .await;
3202 cx.simulate_shared_keystrokes("shift-m").await;
3203 cx.shared_state().await.assert_eq(indoc! {"
3204 1 2 3
3205 4 5 ˇ6
3206 7 8 9
3207 "});
3208 }
3209
3210 #[gpui::test]
3211 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3212 let mut cx = NeovimBackedTestContext::new(cx).await;
3213 let initial_state = indoc! {r"abc
3214 deˇf
3215 paragraph
3216 the second
3217 third and
3218 final"};
3219
3220 cx.set_shared_state(initial_state).await;
3221 cx.simulate_shared_keystrokes("shift-l").await;
3222 cx.shared_state().await.assert_eq(indoc! {r"abc
3223 def
3224 paragraph
3225 the second
3226 third and
3227 fiˇnal"});
3228
3229 cx.set_shared_state(indoc! {r"
3230 1 2 3
3231 4 5 ˇ6
3232 7 8 9
3233 "})
3234 .await;
3235 cx.simulate_shared_keystrokes("shift-l").await;
3236 cx.shared_state().await.assert_eq(indoc! {"
3237 1 2 3
3238 4 5 6
3239 7 8 9
3240 ˇ"});
3241
3242 cx.set_shared_state(indoc! {r"
3243 1 2 3
3244 ˇ4 5 6
3245 7 8 9
3246 "})
3247 .await;
3248 cx.simulate_shared_keystrokes("shift-l").await;
3249 cx.shared_state().await.assert_eq(indoc! {"
3250 1 2 3
3251 4 5 6
3252 7 8 9
3253 ˇ"});
3254
3255 cx.set_shared_state(indoc! {r"
3256 1 2 ˇ3
3257 4 5 6
3258 7 8 9
3259 "})
3260 .await;
3261 cx.simulate_shared_keystrokes("shift-l").await;
3262 cx.shared_state().await.assert_eq(indoc! {"
3263 1 2 3
3264 4 5 6
3265 7 8 9
3266 ˇ"});
3267
3268 cx.set_shared_state(indoc! {r"
3269 ˇ1 2 3
3270 4 5 6
3271 7 8 9
3272 "})
3273 .await;
3274 cx.simulate_shared_keystrokes("shift-l").await;
3275 cx.shared_state().await.assert_eq(indoc! {"
3276 1 2 3
3277 4 5 6
3278 7 8 9
3279 ˇ"});
3280
3281 cx.set_shared_state(indoc! {r"
3282 1 2 3
3283 4 5 ˇ6
3284 7 8 9
3285 "})
3286 .await;
3287 cx.simulate_shared_keystrokes("9 shift-l").await;
3288 cx.shared_state().await.assert_eq(indoc! {"
3289 1 2 ˇ3
3290 4 5 6
3291 7 8 9
3292 "});
3293 }
3294
3295 #[gpui::test]
3296 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3297 let mut cx = NeovimBackedTestContext::new(cx).await;
3298 cx.set_shared_state(indoc! {r"
3299 456 5ˇ67 678
3300 "})
3301 .await;
3302 cx.simulate_shared_keystrokes("g e").await;
3303 cx.shared_state().await.assert_eq(indoc! {"
3304 45ˇ6 567 678
3305 "});
3306
3307 // Test times
3308 cx.set_shared_state(indoc! {r"
3309 123 234 345
3310 456 5ˇ67 678
3311 "})
3312 .await;
3313 cx.simulate_shared_keystrokes("4 g e").await;
3314 cx.shared_state().await.assert_eq(indoc! {"
3315 12ˇ3 234 345
3316 456 567 678
3317 "});
3318
3319 // With punctuation
3320 cx.set_shared_state(indoc! {r"
3321 123 234 345
3322 4;5.6 5ˇ67 678
3323 789 890 901
3324 "})
3325 .await;
3326 cx.simulate_shared_keystrokes("g e").await;
3327 cx.shared_state().await.assert_eq(indoc! {"
3328 123 234 345
3329 4;5.ˇ6 567 678
3330 789 890 901
3331 "});
3332
3333 // With punctuation and count
3334 cx.set_shared_state(indoc! {r"
3335 123 234 345
3336 4;5.6 5ˇ67 678
3337 789 890 901
3338 "})
3339 .await;
3340 cx.simulate_shared_keystrokes("5 g e").await;
3341 cx.shared_state().await.assert_eq(indoc! {"
3342 123 234 345
3343 ˇ4;5.6 567 678
3344 789 890 901
3345 "});
3346
3347 // newlines
3348 cx.set_shared_state(indoc! {r"
3349 123 234 345
3350
3351 78ˇ9 890 901
3352 "})
3353 .await;
3354 cx.simulate_shared_keystrokes("g e").await;
3355 cx.shared_state().await.assert_eq(indoc! {"
3356 123 234 345
3357 ˇ
3358 789 890 901
3359 "});
3360 cx.simulate_shared_keystrokes("g e").await;
3361 cx.shared_state().await.assert_eq(indoc! {"
3362 123 234 34ˇ5
3363
3364 789 890 901
3365 "});
3366
3367 // With punctuation
3368 cx.set_shared_state(indoc! {r"
3369 123 234 345
3370 4;5.ˇ6 567 678
3371 789 890 901
3372 "})
3373 .await;
3374 cx.simulate_shared_keystrokes("g shift-e").await;
3375 cx.shared_state().await.assert_eq(indoc! {"
3376 123 234 34ˇ5
3377 4;5.6 567 678
3378 789 890 901
3379 "});
3380 }
3381
3382 #[gpui::test]
3383 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3384 let mut cx = NeovimBackedTestContext::new(cx).await;
3385
3386 cx.set_shared_state(indoc! {"
3387 fn aˇ() {
3388 return
3389 }
3390 "})
3391 .await;
3392 cx.simulate_shared_keystrokes("v $ %").await;
3393 cx.shared_state().await.assert_eq(indoc! {"
3394 fn a«() {
3395 return
3396 }ˇ»
3397 "});
3398 }
3399
3400 #[gpui::test]
3401 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3402 let mut cx = VimTestContext::new(cx, true).await;
3403
3404 cx.set_state(
3405 indoc! {"
3406 struct Foo {
3407 ˇ
3408 }
3409 "},
3410 Mode::Normal,
3411 );
3412
3413 cx.update_editor(|editor, _window, cx| {
3414 let range = editor.selections.newest_anchor().range();
3415 let inlay_text = " field: int,\n field2: string\n field3: float";
3416 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3417 editor.splice_inlays(&[], vec![inlay], cx);
3418 });
3419
3420 cx.simulate_keystrokes("j");
3421 cx.assert_state(
3422 indoc! {"
3423 struct Foo {
3424
3425 ˇ}
3426 "},
3427 Mode::Normal,
3428 );
3429 }
3430
3431 #[gpui::test]
3432 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3433 let mut cx = VimTestContext::new(cx, true).await;
3434
3435 cx.set_state(
3436 indoc! {"
3437 ˇstruct Foo {
3438
3439 }
3440 "},
3441 Mode::Normal,
3442 );
3443 cx.update_editor(|editor, _window, cx| {
3444 let snapshot = editor.buffer().read(cx).snapshot(cx);
3445 let end_of_line =
3446 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3447 let inlay_text = " hint";
3448 let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3449 editor.splice_inlays(&[], vec![inlay], cx);
3450 });
3451 cx.simulate_keystrokes("$");
3452 cx.assert_state(
3453 indoc! {"
3454 struct Foo ˇ{
3455
3456 }
3457 "},
3458 Mode::Normal,
3459 );
3460 }
3461
3462 #[gpui::test]
3463 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3464 let mut cx = NeovimBackedTestContext::new(cx).await;
3465 // Normal mode
3466 cx.set_shared_state(indoc! {"
3467 The ˇquick brown
3468 fox jumps over
3469 the lazy dog
3470 The quick brown
3471 fox jumps over
3472 the lazy dog
3473 The quick brown
3474 fox jumps over
3475 the lazy dog"})
3476 .await;
3477 cx.simulate_shared_keystrokes("2 0 %").await;
3478 cx.shared_state().await.assert_eq(indoc! {"
3479 The quick brown
3480 fox ˇjumps over
3481 the lazy dog
3482 The quick brown
3483 fox jumps over
3484 the lazy dog
3485 The quick brown
3486 fox jumps over
3487 the lazy dog"});
3488
3489 cx.simulate_shared_keystrokes("2 5 %").await;
3490 cx.shared_state().await.assert_eq(indoc! {"
3491 The quick brown
3492 fox jumps over
3493 the ˇlazy dog
3494 The quick brown
3495 fox jumps over
3496 the lazy dog
3497 The quick brown
3498 fox jumps over
3499 the lazy dog"});
3500
3501 cx.simulate_shared_keystrokes("7 5 %").await;
3502 cx.shared_state().await.assert_eq(indoc! {"
3503 The quick brown
3504 fox jumps over
3505 the lazy dog
3506 The quick brown
3507 fox jumps over
3508 the lazy dog
3509 The ˇquick brown
3510 fox jumps over
3511 the lazy dog"});
3512
3513 // Visual mode
3514 cx.set_shared_state(indoc! {"
3515 The ˇquick brown
3516 fox jumps over
3517 the lazy dog
3518 The quick brown
3519 fox jumps over
3520 the lazy dog
3521 The quick brown
3522 fox jumps over
3523 the lazy dog"})
3524 .await;
3525 cx.simulate_shared_keystrokes("v 5 0 %").await;
3526 cx.shared_state().await.assert_eq(indoc! {"
3527 The «quick brown
3528 fox jumps over
3529 the lazy dog
3530 The quick brown
3531 fox jˇ»umps over
3532 the lazy dog
3533 The quick brown
3534 fox jumps over
3535 the lazy dog"});
3536
3537 cx.set_shared_state(indoc! {"
3538 The ˇquick brown
3539 fox jumps over
3540 the lazy dog
3541 The quick brown
3542 fox jumps over
3543 the lazy dog
3544 The quick brown
3545 fox jumps over
3546 the lazy dog"})
3547 .await;
3548 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3549 cx.shared_state().await.assert_eq(indoc! {"
3550 The «quick brown
3551 fox jumps over
3552 the lazy dog
3553 The quick brown
3554 fox jumps over
3555 the lazy dog
3556 The quick brown
3557 fox jumps over
3558 the lˇ»azy dog"});
3559 }
3560
3561 #[gpui::test]
3562 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3563 let mut cx = NeovimBackedTestContext::new(cx).await;
3564
3565 cx.set_shared_state("ˇπππππ").await;
3566 cx.simulate_shared_keystrokes("3 space").await;
3567 cx.shared_state().await.assert_eq("πππˇππ");
3568 }
3569
3570 #[gpui::test]
3571 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3572 let mut cx = NeovimBackedTestContext::new(cx).await;
3573
3574 cx.set_shared_state(indoc! {"
3575 ππππˇπ
3576 πanotherline"})
3577 .await;
3578 cx.simulate_shared_keystrokes("4 space").await;
3579 cx.shared_state().await.assert_eq(indoc! {"
3580 πππππ
3581 πanˇotherline"});
3582 }
3583
3584 #[gpui::test]
3585 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3586 let mut cx = NeovimBackedTestContext::new(cx).await;
3587
3588 cx.set_shared_state(indoc! {"
3589 ππππ
3590 πanˇotherline"})
3591 .await;
3592 cx.simulate_shared_keystrokes("4 backspace").await;
3593 cx.shared_state().await.assert_eq(indoc! {"
3594 πππˇπ
3595 πanotherline"});
3596 }
3597}