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, mut point: DisplayPoint) -> DisplayPoint {
1277 let max_column = map.line_len(point.row()).saturating_sub(1);
1278 if point.column() < max_column {
1279 *point.column_mut() += 1;
1280 point = map.clip_point(point, Bias::Right);
1281 } else if point.row() < map.max_point().row() {
1282 *point.row_mut() += 1;
1283 *point.column_mut() = 0;
1284 }
1285 point
1286}
1287
1288pub(crate) fn start_of_relative_buffer_row(
1289 map: &DisplaySnapshot,
1290 point: DisplayPoint,
1291 times: isize,
1292) -> DisplayPoint {
1293 let start = map.display_point_to_fold_point(point, Bias::Left);
1294 let target = start.row() as isize + times;
1295 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1296
1297 map.clip_point(
1298 map.fold_point_to_display_point(
1299 map.fold_snapshot
1300 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1301 ),
1302 Bias::Right,
1303 )
1304}
1305
1306fn up_down_buffer_rows(
1307 map: &DisplaySnapshot,
1308 mut point: DisplayPoint,
1309 mut goal: SelectionGoal,
1310 mut times: isize,
1311 text_layout_details: &TextLayoutDetails,
1312) -> (DisplayPoint, SelectionGoal) {
1313 let bias = if times < 0 { Bias::Left } else { Bias::Right };
1314
1315 while map.is_folded_buffer_header(point.row()) {
1316 if times < 0 {
1317 (point, _) = movement::up(map, point, goal, true, text_layout_details);
1318 times += 1;
1319 } else if times > 0 {
1320 (point, _) = movement::down(map, point, goal, true, text_layout_details);
1321 times -= 1;
1322 } else {
1323 break;
1324 }
1325 }
1326
1327 let start = map.display_point_to_fold_point(point, Bias::Left);
1328 let begin_folded_line = map.fold_point_to_display_point(
1329 map.fold_snapshot
1330 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1331 );
1332 let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1333
1334 let (goal_wrap, goal_x) = match goal {
1335 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1336 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1337 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1338 _ => {
1339 let x = map.x_for_display_point(point, text_layout_details);
1340 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1341 (select_nth_wrapped_row, x.0)
1342 }
1343 };
1344
1345 let target = start.row() as isize + times;
1346 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1347
1348 let mut begin_folded_line = map.fold_point_to_display_point(
1349 map.fold_snapshot
1350 .clip_point(FoldPoint::new(new_row, 0), bias),
1351 );
1352
1353 let mut i = 0;
1354 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1355 let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1356 if map
1357 .display_point_to_fold_point(next_folded_line, bias)
1358 .row()
1359 == new_row
1360 {
1361 i += 1;
1362 begin_folded_line = next_folded_line;
1363 } else {
1364 break;
1365 }
1366 }
1367
1368 let new_col = if i == goal_wrap {
1369 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1370 } else {
1371 map.line_len(begin_folded_line.row())
1372 };
1373
1374 (
1375 map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
1376 goal,
1377 )
1378}
1379
1380fn down_display(
1381 map: &DisplaySnapshot,
1382 mut point: DisplayPoint,
1383 mut goal: SelectionGoal,
1384 times: usize,
1385 text_layout_details: &TextLayoutDetails,
1386) -> (DisplayPoint, SelectionGoal) {
1387 for _ in 0..times {
1388 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1389 }
1390
1391 (point, goal)
1392}
1393
1394fn up_display(
1395 map: &DisplaySnapshot,
1396 mut point: DisplayPoint,
1397 mut goal: SelectionGoal,
1398 times: usize,
1399 text_layout_details: &TextLayoutDetails,
1400) -> (DisplayPoint, SelectionGoal) {
1401 for _ in 0..times {
1402 (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1403 }
1404
1405 (point, goal)
1406}
1407
1408pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1409 for _ in 0..times {
1410 let new_point = movement::saturating_right(map, point);
1411 if point == new_point {
1412 break;
1413 }
1414 point = new_point;
1415 }
1416 point
1417}
1418
1419pub(crate) fn next_char(
1420 map: &DisplaySnapshot,
1421 point: DisplayPoint,
1422 allow_cross_newline: bool,
1423) -> DisplayPoint {
1424 let mut new_point = point;
1425 let mut max_column = map.line_len(new_point.row());
1426 if !allow_cross_newline {
1427 max_column -= 1;
1428 }
1429 if new_point.column() < max_column {
1430 *new_point.column_mut() += 1;
1431 } else if new_point < map.max_point() && allow_cross_newline {
1432 *new_point.row_mut() += 1;
1433 *new_point.column_mut() = 0;
1434 }
1435 map.clip_ignoring_line_ends(new_point, Bias::Right)
1436}
1437
1438pub(crate) fn next_word_start(
1439 map: &DisplaySnapshot,
1440 mut point: DisplayPoint,
1441 ignore_punctuation: bool,
1442 times: usize,
1443) -> DisplayPoint {
1444 let classifier = map
1445 .buffer_snapshot
1446 .char_classifier_at(point.to_point(map))
1447 .ignore_punctuation(ignore_punctuation);
1448 for _ in 0..times {
1449 let mut crossed_newline = false;
1450 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1451 let left_kind = classifier.kind(left);
1452 let right_kind = classifier.kind(right);
1453 let at_newline = right == '\n';
1454
1455 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1456 || at_newline && crossed_newline
1457 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1458
1459 crossed_newline |= at_newline;
1460 found
1461 });
1462 if point == new_point {
1463 break;
1464 }
1465 point = new_point;
1466 }
1467 point
1468}
1469
1470pub(crate) fn next_word_end(
1471 map: &DisplaySnapshot,
1472 mut point: DisplayPoint,
1473 ignore_punctuation: bool,
1474 times: usize,
1475 allow_cross_newline: bool,
1476) -> DisplayPoint {
1477 let classifier = map
1478 .buffer_snapshot
1479 .char_classifier_at(point.to_point(map))
1480 .ignore_punctuation(ignore_punctuation);
1481 for _ in 0..times {
1482 let new_point = next_char(map, point, allow_cross_newline);
1483 let mut need_next_char = false;
1484 let new_point = movement::find_boundary_exclusive(
1485 map,
1486 new_point,
1487 FindRange::MultiLine,
1488 |left, right| {
1489 let left_kind = classifier.kind(left);
1490 let right_kind = classifier.kind(right);
1491 let at_newline = right == '\n';
1492
1493 if !allow_cross_newline && at_newline {
1494 need_next_char = true;
1495 return true;
1496 }
1497
1498 left_kind != right_kind && left_kind != CharKind::Whitespace
1499 },
1500 );
1501 let new_point = if need_next_char {
1502 next_char(map, new_point, true)
1503 } else {
1504 new_point
1505 };
1506 let new_point = map.clip_point(new_point, Bias::Left);
1507 if point == new_point {
1508 break;
1509 }
1510 point = new_point;
1511 }
1512 point
1513}
1514
1515fn previous_word_start(
1516 map: &DisplaySnapshot,
1517 mut point: DisplayPoint,
1518 ignore_punctuation: bool,
1519 times: usize,
1520) -> DisplayPoint {
1521 let classifier = map
1522 .buffer_snapshot
1523 .char_classifier_at(point.to_point(map))
1524 .ignore_punctuation(ignore_punctuation);
1525 for _ in 0..times {
1526 // This works even though find_preceding_boundary is called for every character in the line containing
1527 // cursor because the newline is checked only once.
1528 let new_point = movement::find_preceding_boundary_display_point(
1529 map,
1530 point,
1531 FindRange::MultiLine,
1532 |left, right| {
1533 let left_kind = classifier.kind(left);
1534 let right_kind = classifier.kind(right);
1535
1536 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1537 },
1538 );
1539 if point == new_point {
1540 break;
1541 }
1542 point = new_point;
1543 }
1544 point
1545}
1546
1547fn previous_word_end(
1548 map: &DisplaySnapshot,
1549 point: DisplayPoint,
1550 ignore_punctuation: bool,
1551 times: usize,
1552) -> DisplayPoint {
1553 let classifier = map
1554 .buffer_snapshot
1555 .char_classifier_at(point.to_point(map))
1556 .ignore_punctuation(ignore_punctuation);
1557 let mut point = point.to_point(map);
1558
1559 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1560 point.column += 1;
1561 }
1562 for _ in 0..times {
1563 let new_point = movement::find_preceding_boundary_point(
1564 &map.buffer_snapshot,
1565 point,
1566 FindRange::MultiLine,
1567 |left, right| {
1568 let left_kind = classifier.kind(left);
1569 let right_kind = classifier.kind(right);
1570 match (left_kind, right_kind) {
1571 (CharKind::Punctuation, CharKind::Whitespace)
1572 | (CharKind::Punctuation, CharKind::Word)
1573 | (CharKind::Word, CharKind::Whitespace)
1574 | (CharKind::Word, CharKind::Punctuation) => true,
1575 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1576 _ => false,
1577 }
1578 },
1579 );
1580 if new_point == point {
1581 break;
1582 }
1583 point = new_point;
1584 }
1585 movement::saturating_left(map, point.to_display_point(map))
1586}
1587
1588fn next_subword_start(
1589 map: &DisplaySnapshot,
1590 mut point: DisplayPoint,
1591 ignore_punctuation: bool,
1592 times: usize,
1593) -> DisplayPoint {
1594 let classifier = map
1595 .buffer_snapshot
1596 .char_classifier_at(point.to_point(map))
1597 .ignore_punctuation(ignore_punctuation);
1598 for _ in 0..times {
1599 let mut crossed_newline = false;
1600 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1601 let left_kind = classifier.kind(left);
1602 let right_kind = classifier.kind(right);
1603 let at_newline = right == '\n';
1604
1605 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1606 let is_subword_start =
1607 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1608
1609 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1610 || at_newline && crossed_newline
1611 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1612
1613 crossed_newline |= at_newline;
1614 found
1615 });
1616 if point == new_point {
1617 break;
1618 }
1619 point = new_point;
1620 }
1621 point
1622}
1623
1624pub(crate) fn next_subword_end(
1625 map: &DisplaySnapshot,
1626 mut point: DisplayPoint,
1627 ignore_punctuation: bool,
1628 times: usize,
1629 allow_cross_newline: bool,
1630) -> DisplayPoint {
1631 let classifier = map
1632 .buffer_snapshot
1633 .char_classifier_at(point.to_point(map))
1634 .ignore_punctuation(ignore_punctuation);
1635 for _ in 0..times {
1636 let new_point = next_char(map, point, allow_cross_newline);
1637
1638 let mut crossed_newline = false;
1639 let mut need_backtrack = false;
1640 let new_point =
1641 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1642 let left_kind = classifier.kind(left);
1643 let right_kind = classifier.kind(right);
1644 let at_newline = right == '\n';
1645
1646 if !allow_cross_newline && at_newline {
1647 return true;
1648 }
1649
1650 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1651 let is_subword_end =
1652 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1653
1654 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1655
1656 if found && (is_word_end || is_subword_end) {
1657 need_backtrack = true;
1658 }
1659
1660 crossed_newline |= at_newline;
1661 found
1662 });
1663 let mut new_point = map.clip_point(new_point, Bias::Left);
1664 if need_backtrack {
1665 *new_point.column_mut() -= 1;
1666 }
1667 let new_point = map.clip_point(new_point, Bias::Left);
1668 if point == new_point {
1669 break;
1670 }
1671 point = new_point;
1672 }
1673 point
1674}
1675
1676fn previous_subword_start(
1677 map: &DisplaySnapshot,
1678 mut point: DisplayPoint,
1679 ignore_punctuation: bool,
1680 times: usize,
1681) -> DisplayPoint {
1682 let classifier = map
1683 .buffer_snapshot
1684 .char_classifier_at(point.to_point(map))
1685 .ignore_punctuation(ignore_punctuation);
1686 for _ in 0..times {
1687 let mut crossed_newline = false;
1688 // This works even though find_preceding_boundary is called for every character in the line containing
1689 // cursor because the newline is checked only once.
1690 let new_point = movement::find_preceding_boundary_display_point(
1691 map,
1692 point,
1693 FindRange::MultiLine,
1694 |left, right| {
1695 let left_kind = classifier.kind(left);
1696 let right_kind = classifier.kind(right);
1697 let at_newline = right == '\n';
1698
1699 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1700 let is_subword_start =
1701 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1702
1703 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1704 || at_newline && crossed_newline
1705 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1706
1707 crossed_newline |= at_newline;
1708
1709 found
1710 },
1711 );
1712 if point == new_point {
1713 break;
1714 }
1715 point = new_point;
1716 }
1717 point
1718}
1719
1720fn previous_subword_end(
1721 map: &DisplaySnapshot,
1722 point: DisplayPoint,
1723 ignore_punctuation: bool,
1724 times: usize,
1725) -> DisplayPoint {
1726 let classifier = map
1727 .buffer_snapshot
1728 .char_classifier_at(point.to_point(map))
1729 .ignore_punctuation(ignore_punctuation);
1730 let mut point = point.to_point(map);
1731
1732 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1733 point.column += 1;
1734 }
1735 for _ in 0..times {
1736 let new_point = movement::find_preceding_boundary_point(
1737 &map.buffer_snapshot,
1738 point,
1739 FindRange::MultiLine,
1740 |left, right| {
1741 let left_kind = classifier.kind(left);
1742 let right_kind = classifier.kind(right);
1743
1744 let is_subword_end =
1745 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1746
1747 if is_subword_end {
1748 return true;
1749 }
1750
1751 match (left_kind, right_kind) {
1752 (CharKind::Word, CharKind::Whitespace)
1753 | (CharKind::Word, CharKind::Punctuation) => true,
1754 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1755 _ => false,
1756 }
1757 },
1758 );
1759 if new_point == point {
1760 break;
1761 }
1762 point = new_point;
1763 }
1764 movement::saturating_left(map, point.to_display_point(map))
1765}
1766
1767pub(crate) fn first_non_whitespace(
1768 map: &DisplaySnapshot,
1769 display_lines: bool,
1770 from: DisplayPoint,
1771) -> DisplayPoint {
1772 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1773 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1774 for (ch, offset) in map.buffer_chars_at(start_offset) {
1775 if ch == '\n' {
1776 return from;
1777 }
1778
1779 start_offset = offset;
1780
1781 if classifier.kind(ch) != CharKind::Whitespace {
1782 break;
1783 }
1784 }
1785
1786 start_offset.to_display_point(map)
1787}
1788
1789pub(crate) fn last_non_whitespace(
1790 map: &DisplaySnapshot,
1791 from: DisplayPoint,
1792 count: usize,
1793) -> DisplayPoint {
1794 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1795 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1796
1797 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1798 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1799 if classifier.kind(ch) != CharKind::Whitespace {
1800 return end_of_line.to_display_point(map);
1801 }
1802 }
1803
1804 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1805 if ch == '\n' {
1806 break;
1807 }
1808 end_of_line = offset;
1809 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1810 break;
1811 }
1812 }
1813
1814 end_of_line.to_display_point(map)
1815}
1816
1817pub(crate) fn start_of_line(
1818 map: &DisplaySnapshot,
1819 display_lines: bool,
1820 point: DisplayPoint,
1821) -> DisplayPoint {
1822 if display_lines {
1823 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1824 } else {
1825 map.prev_line_boundary(point.to_point(map)).1
1826 }
1827}
1828
1829pub(crate) fn end_of_line(
1830 map: &DisplaySnapshot,
1831 display_lines: bool,
1832 mut point: DisplayPoint,
1833 times: usize,
1834) -> DisplayPoint {
1835 if times > 1 {
1836 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1837 }
1838 if display_lines {
1839 map.clip_point(
1840 DisplayPoint::new(point.row(), map.line_len(point.row())),
1841 Bias::Left,
1842 )
1843 } else {
1844 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1845 }
1846}
1847
1848pub(crate) fn sentence_backwards(
1849 map: &DisplaySnapshot,
1850 point: DisplayPoint,
1851 mut times: usize,
1852) -> DisplayPoint {
1853 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1854 let mut chars = map.reverse_buffer_chars_at(start).peekable();
1855
1856 let mut was_newline = map
1857 .buffer_chars_at(start)
1858 .next()
1859 .is_some_and(|(c, _)| c == '\n');
1860
1861 while let Some((ch, offset)) = chars.next() {
1862 let start_of_next_sentence = if was_newline && ch == '\n' {
1863 Some(offset + ch.len_utf8())
1864 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1865 Some(next_non_blank(map, offset + ch.len_utf8()))
1866 } else if ch == '.' || ch == '?' || ch == '!' {
1867 start_of_next_sentence(map, offset + ch.len_utf8())
1868 } else {
1869 None
1870 };
1871
1872 if let Some(start_of_next_sentence) = start_of_next_sentence {
1873 if start_of_next_sentence < start {
1874 times = times.saturating_sub(1);
1875 }
1876 if times == 0 || offset == 0 {
1877 return map.clip_point(
1878 start_of_next_sentence
1879 .to_offset(&map.buffer_snapshot)
1880 .to_display_point(map),
1881 Bias::Left,
1882 );
1883 }
1884 }
1885 if was_newline {
1886 start = offset;
1887 }
1888 was_newline = ch == '\n';
1889 }
1890
1891 DisplayPoint::zero()
1892}
1893
1894pub(crate) fn sentence_forwards(
1895 map: &DisplaySnapshot,
1896 point: DisplayPoint,
1897 mut times: usize,
1898) -> DisplayPoint {
1899 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1900 let mut chars = map.buffer_chars_at(start).peekable();
1901
1902 let mut was_newline = map
1903 .reverse_buffer_chars_at(start)
1904 .next()
1905 .is_some_and(|(c, _)| c == '\n')
1906 && chars.peek().is_some_and(|(c, _)| *c == '\n');
1907
1908 while let Some((ch, offset)) = chars.next() {
1909 if was_newline && ch == '\n' {
1910 continue;
1911 }
1912 let start_of_next_sentence = if was_newline {
1913 Some(next_non_blank(map, offset))
1914 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1915 Some(next_non_blank(map, offset + ch.len_utf8()))
1916 } else if ch == '.' || ch == '?' || ch == '!' {
1917 start_of_next_sentence(map, offset + ch.len_utf8())
1918 } else {
1919 None
1920 };
1921
1922 if let Some(start_of_next_sentence) = start_of_next_sentence {
1923 times = times.saturating_sub(1);
1924 if times == 0 {
1925 return map.clip_point(
1926 start_of_next_sentence
1927 .to_offset(&map.buffer_snapshot)
1928 .to_display_point(map),
1929 Bias::Right,
1930 );
1931 }
1932 }
1933
1934 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1935 }
1936
1937 map.max_point()
1938}
1939
1940fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1941 for (c, o) in map.buffer_chars_at(start) {
1942 if c == '\n' || !c.is_whitespace() {
1943 return o;
1944 }
1945 }
1946
1947 map.buffer_snapshot.len()
1948}
1949
1950// given the offset after a ., !, or ? find the start of the next sentence.
1951// if this is not a sentence boundary, returns None.
1952fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1953 let chars = map.buffer_chars_at(end_of_sentence);
1954 let mut seen_space = false;
1955
1956 for (char, offset) in chars {
1957 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1958 continue;
1959 }
1960
1961 if char == '\n' && seen_space {
1962 return Some(offset);
1963 } else if char.is_whitespace() {
1964 seen_space = true;
1965 } else if seen_space {
1966 return Some(offset);
1967 } else {
1968 return None;
1969 }
1970 }
1971
1972 Some(map.buffer_snapshot.len())
1973}
1974
1975fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
1976 let point = map.display_point_to_point(display_point, Bias::Left);
1977 let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
1978 return display_point;
1979 };
1980 let offset = excerpt.buffer().point_to_offset(
1981 excerpt
1982 .buffer()
1983 .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
1984 );
1985 let buffer_range = excerpt.buffer_range();
1986 if offset >= buffer_range.start && offset <= buffer_range.end {
1987 let point = map
1988 .buffer_snapshot
1989 .offset_to_point(excerpt.map_offset_from_buffer(offset));
1990 return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
1991 }
1992 let mut last_position = None;
1993 for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
1994 let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
1995 ..language::ToOffset::to_offset(&range.context.end, &buffer);
1996 if offset >= excerpt_range.start && offset <= excerpt_range.end {
1997 let text_anchor = buffer.anchor_after(offset);
1998 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
1999 return anchor.to_display_point(map);
2000 } else if offset <= excerpt_range.start {
2001 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2002 return anchor.to_display_point(map);
2003 } else {
2004 last_position = Some(Anchor::in_buffer(
2005 excerpt,
2006 buffer.remote_id(),
2007 range.context.end,
2008 ));
2009 }
2010 }
2011
2012 let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2013 last_point.column = point.column;
2014
2015 map.clip_point(
2016 map.point_to_display_point(
2017 map.buffer_snapshot.clip_point(point, Bias::Left),
2018 Bias::Left,
2019 ),
2020 Bias::Left,
2021 )
2022}
2023
2024fn start_of_document(
2025 map: &DisplaySnapshot,
2026 display_point: DisplayPoint,
2027 maybe_times: Option<usize>,
2028) -> DisplayPoint {
2029 if let Some(times) = maybe_times {
2030 return go_to_line(map, display_point, times);
2031 }
2032
2033 let point = map.display_point_to_point(display_point, Bias::Left);
2034 let mut first_point = Point::zero();
2035 first_point.column = point.column;
2036
2037 map.clip_point(
2038 map.point_to_display_point(
2039 map.buffer_snapshot.clip_point(first_point, Bias::Left),
2040 Bias::Left,
2041 ),
2042 Bias::Left,
2043 )
2044}
2045
2046fn end_of_document(
2047 map: &DisplaySnapshot,
2048 display_point: DisplayPoint,
2049 maybe_times: Option<usize>,
2050) -> DisplayPoint {
2051 if let Some(times) = maybe_times {
2052 return go_to_line(map, display_point, times);
2053 };
2054 let point = map.display_point_to_point(display_point, Bias::Left);
2055 let mut last_point = map.buffer_snapshot.max_point();
2056 last_point.column = point.column;
2057
2058 map.clip_point(
2059 map.point_to_display_point(
2060 map.buffer_snapshot.clip_point(last_point, Bias::Left),
2061 Bias::Left,
2062 ),
2063 Bias::Left,
2064 )
2065}
2066
2067fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2068 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2069 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2070
2071 if head > outer.start && head < inner.start {
2072 let mut offset = inner.end.to_offset(map, Bias::Left);
2073 for c in map.buffer_snapshot.chars_at(offset) {
2074 if c == '/' || c == '\n' || c == '>' {
2075 return Some(offset.to_display_point(map));
2076 }
2077 offset += c.len_utf8();
2078 }
2079 } else {
2080 let mut offset = outer.start.to_offset(map, Bias::Left);
2081 for c in map.buffer_snapshot.chars_at(offset) {
2082 offset += c.len_utf8();
2083 if c == '<' || c == '\n' {
2084 return Some(offset.to_display_point(map));
2085 }
2086 }
2087 }
2088
2089 return None;
2090}
2091
2092fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2093 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2094 let display_point = map.clip_at_line_end(display_point);
2095 let point = display_point.to_point(map);
2096 let offset = point.to_offset(&map.buffer_snapshot);
2097
2098 // Ensure the range is contained by the current line.
2099 let mut line_end = map.next_line_boundary(point).0;
2100 if line_end == point {
2101 line_end = map.max_point().to_point(map);
2102 }
2103
2104 let line_range = map.prev_line_boundary(point).0..line_end;
2105 let visible_line_range =
2106 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2107 let ranges = map
2108 .buffer_snapshot
2109 .bracket_ranges(visible_line_range.clone());
2110 if let Some(ranges) = ranges {
2111 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2112 ..line_range.end.to_offset(&map.buffer_snapshot);
2113 let mut closest_pair_destination = None;
2114 let mut closest_distance = usize::MAX;
2115
2116 for (open_range, close_range) in ranges {
2117 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2118 if offset > open_range.start && offset < close_range.start {
2119 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2120 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2121 return display_point;
2122 }
2123 if let Some(tag) = matching_tag(map, display_point) {
2124 return tag;
2125 }
2126 } else if close_range.contains(&offset) {
2127 return open_range.start.to_display_point(map);
2128 } else if open_range.contains(&offset) {
2129 return (close_range.end - 1).to_display_point(map);
2130 }
2131 }
2132
2133 if (open_range.contains(&offset) || open_range.start >= offset)
2134 && line_range.contains(&open_range.start)
2135 {
2136 let distance = open_range.start.saturating_sub(offset);
2137 if distance < closest_distance {
2138 closest_pair_destination = Some(close_range.start);
2139 closest_distance = distance;
2140 continue;
2141 }
2142 }
2143
2144 if (close_range.contains(&offset) || close_range.start >= offset)
2145 && line_range.contains(&close_range.start)
2146 {
2147 let distance = close_range.start.saturating_sub(offset);
2148 if distance < closest_distance {
2149 closest_pair_destination = Some(open_range.start);
2150 closest_distance = distance;
2151 continue;
2152 }
2153 }
2154
2155 continue;
2156 }
2157
2158 closest_pair_destination
2159 .map(|destination| destination.to_display_point(map))
2160 .unwrap_or(display_point)
2161 } else {
2162 display_point
2163 }
2164}
2165
2166// Go to {count} percentage in the file, on the first
2167// non-blank in the line linewise. To compute the new
2168// line number this formula is used:
2169// ({count} * number-of-lines + 99) / 100
2170//
2171// https://neovim.io/doc/user/motion.html#N%25
2172fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2173 let total_lines = map.buffer_snapshot.max_point().row + 1;
2174 let target_line = (count * total_lines as usize + 99) / 100;
2175 let target_point = DisplayPoint::new(
2176 DisplayRow(target_line.saturating_sub(1) as u32),
2177 point.column(),
2178 );
2179 map.clip_point(target_point, Bias::Left)
2180}
2181
2182fn unmatched_forward(
2183 map: &DisplaySnapshot,
2184 mut display_point: DisplayPoint,
2185 char: char,
2186 times: usize,
2187) -> DisplayPoint {
2188 for _ in 0..times {
2189 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2190 let point = display_point.to_point(map);
2191 let offset = point.to_offset(&map.buffer_snapshot);
2192
2193 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2194 let Some(ranges) = ranges else { break };
2195 let mut closest_closing_destination = None;
2196 let mut closest_distance = usize::MAX;
2197
2198 for (_, close_range) in ranges {
2199 if close_range.start > offset {
2200 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2201 if Some(char) == chars.next() {
2202 let distance = close_range.start - offset;
2203 if distance < closest_distance {
2204 closest_closing_destination = Some(close_range.start);
2205 closest_distance = distance;
2206 continue;
2207 }
2208 }
2209 }
2210 }
2211
2212 let new_point = closest_closing_destination
2213 .map(|destination| destination.to_display_point(map))
2214 .unwrap_or(display_point);
2215 if new_point == display_point {
2216 break;
2217 }
2218 display_point = new_point;
2219 }
2220 return display_point;
2221}
2222
2223fn unmatched_backward(
2224 map: &DisplaySnapshot,
2225 mut display_point: DisplayPoint,
2226 char: char,
2227 times: usize,
2228) -> DisplayPoint {
2229 for _ in 0..times {
2230 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2231 let point = display_point.to_point(map);
2232 let offset = point.to_offset(&map.buffer_snapshot);
2233
2234 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2235 let Some(ranges) = ranges else {
2236 break;
2237 };
2238
2239 let mut closest_starting_destination = None;
2240 let mut closest_distance = usize::MAX;
2241
2242 for (start_range, _) in ranges {
2243 if start_range.start < offset {
2244 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2245 if Some(char) == chars.next() {
2246 let distance = offset - start_range.start;
2247 if distance < closest_distance {
2248 closest_starting_destination = Some(start_range.start);
2249 closest_distance = distance;
2250 continue;
2251 }
2252 }
2253 }
2254 }
2255
2256 let new_point = closest_starting_destination
2257 .map(|destination| destination.to_display_point(map))
2258 .unwrap_or(display_point);
2259 if new_point == display_point {
2260 break;
2261 } else {
2262 display_point = new_point;
2263 }
2264 }
2265 display_point
2266}
2267
2268fn find_forward(
2269 map: &DisplaySnapshot,
2270 from: DisplayPoint,
2271 before: bool,
2272 target: char,
2273 times: usize,
2274 mode: FindRange,
2275 smartcase: bool,
2276) -> Option<DisplayPoint> {
2277 let mut to = from;
2278 let mut found = false;
2279
2280 for _ in 0..times {
2281 found = false;
2282 let new_to = find_boundary(map, to, mode, |_, right| {
2283 found = is_character_match(target, right, smartcase);
2284 found
2285 });
2286 if to == new_to {
2287 break;
2288 }
2289 to = new_to;
2290 }
2291
2292 if found {
2293 if before && to.column() > 0 {
2294 *to.column_mut() -= 1;
2295 Some(map.clip_point(to, Bias::Left))
2296 } else {
2297 Some(to)
2298 }
2299 } else {
2300 None
2301 }
2302}
2303
2304fn find_backward(
2305 map: &DisplaySnapshot,
2306 from: DisplayPoint,
2307 after: bool,
2308 target: char,
2309 times: usize,
2310 mode: FindRange,
2311 smartcase: bool,
2312) -> DisplayPoint {
2313 let mut to = from;
2314
2315 for _ in 0..times {
2316 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2317 is_character_match(target, right, smartcase)
2318 });
2319 if to == new_to {
2320 break;
2321 }
2322 to = new_to;
2323 }
2324
2325 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2326 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2327 if after {
2328 *to.column_mut() += 1;
2329 map.clip_point(to, Bias::Right)
2330 } else {
2331 to
2332 }
2333 } else {
2334 from
2335 }
2336}
2337
2338fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2339 if smartcase {
2340 if target.is_uppercase() {
2341 target == other
2342 } else {
2343 target == other.to_ascii_lowercase()
2344 }
2345 } else {
2346 target == other
2347 }
2348}
2349
2350fn sneak(
2351 map: &DisplaySnapshot,
2352 from: DisplayPoint,
2353 first_target: char,
2354 second_target: char,
2355 times: usize,
2356 smartcase: bool,
2357) -> Option<DisplayPoint> {
2358 let mut to = from;
2359 let mut found = false;
2360
2361 for _ in 0..times {
2362 found = false;
2363 let new_to = find_boundary(
2364 map,
2365 movement::right(map, to),
2366 FindRange::MultiLine,
2367 |left, right| {
2368 found = is_character_match(first_target, left, smartcase)
2369 && is_character_match(second_target, right, smartcase);
2370 found
2371 },
2372 );
2373 if to == new_to {
2374 break;
2375 }
2376 to = new_to;
2377 }
2378
2379 if found {
2380 Some(movement::left(map, to))
2381 } else {
2382 None
2383 }
2384}
2385
2386fn sneak_backward(
2387 map: &DisplaySnapshot,
2388 from: DisplayPoint,
2389 first_target: char,
2390 second_target: char,
2391 times: usize,
2392 smartcase: bool,
2393) -> Option<DisplayPoint> {
2394 let mut to = from;
2395 let mut found = false;
2396
2397 for _ in 0..times {
2398 found = false;
2399 let new_to =
2400 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2401 found = is_character_match(first_target, left, smartcase)
2402 && is_character_match(second_target, right, smartcase);
2403 found
2404 });
2405 if to == new_to {
2406 break;
2407 }
2408 to = new_to;
2409 }
2410
2411 if found {
2412 Some(movement::left(map, to))
2413 } else {
2414 None
2415 }
2416}
2417
2418fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2419 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2420 first_non_whitespace(map, false, correct_line)
2421}
2422
2423fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2424 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2425 first_non_whitespace(map, false, correct_line)
2426}
2427
2428fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2429 let correct_line = start_of_relative_buffer_row(map, point, 0);
2430 right(map, correct_line, times.saturating_sub(1))
2431}
2432
2433pub(crate) fn next_line_end(
2434 map: &DisplaySnapshot,
2435 mut point: DisplayPoint,
2436 times: usize,
2437) -> DisplayPoint {
2438 if times > 1 {
2439 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2440 }
2441 end_of_line(map, false, point, 1)
2442}
2443
2444fn window_top(
2445 map: &DisplaySnapshot,
2446 point: DisplayPoint,
2447 text_layout_details: &TextLayoutDetails,
2448 mut times: usize,
2449) -> (DisplayPoint, SelectionGoal) {
2450 let first_visible_line = text_layout_details
2451 .scroll_anchor
2452 .anchor
2453 .to_display_point(map);
2454
2455 if first_visible_line.row() != DisplayRow(0)
2456 && text_layout_details.vertical_scroll_margin as usize > times
2457 {
2458 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2459 }
2460
2461 if let Some(visible_rows) = text_layout_details.visible_rows {
2462 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2463 let new_row = (first_visible_line.row().0 + (times as u32))
2464 .min(bottom_row)
2465 .min(map.max_point().row().0);
2466 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2467
2468 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2469 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2470 } else {
2471 let new_row =
2472 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2473 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2474
2475 let new_point = DisplayPoint::new(new_row, new_col);
2476 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2477 }
2478}
2479
2480fn window_middle(
2481 map: &DisplaySnapshot,
2482 point: DisplayPoint,
2483 text_layout_details: &TextLayoutDetails,
2484) -> (DisplayPoint, SelectionGoal) {
2485 if let Some(visible_rows) = text_layout_details.visible_rows {
2486 let first_visible_line = text_layout_details
2487 .scroll_anchor
2488 .anchor
2489 .to_display_point(map);
2490
2491 let max_visible_rows =
2492 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2493
2494 let new_row =
2495 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2496 let new_row = DisplayRow(new_row);
2497 let new_col = point.column().min(map.line_len(new_row));
2498 let new_point = DisplayPoint::new(new_row, new_col);
2499 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2500 } else {
2501 (point, SelectionGoal::None)
2502 }
2503}
2504
2505fn window_bottom(
2506 map: &DisplaySnapshot,
2507 point: DisplayPoint,
2508 text_layout_details: &TextLayoutDetails,
2509 mut times: usize,
2510) -> (DisplayPoint, SelectionGoal) {
2511 if let Some(visible_rows) = text_layout_details.visible_rows {
2512 let first_visible_line = text_layout_details
2513 .scroll_anchor
2514 .anchor
2515 .to_display_point(map);
2516 let bottom_row = first_visible_line.row().0
2517 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2518 if bottom_row < map.max_point().row().0
2519 && text_layout_details.vertical_scroll_margin as usize > times
2520 {
2521 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2522 }
2523 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2524 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2525 {
2526 first_visible_line.row()
2527 } else {
2528 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2529 };
2530 let new_col = point.column().min(map.line_len(new_row));
2531 let new_point = DisplayPoint::new(new_row, new_col);
2532 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2533 } else {
2534 (point, SelectionGoal::None)
2535 }
2536}
2537
2538fn method_motion(
2539 map: &DisplaySnapshot,
2540 mut display_point: DisplayPoint,
2541 times: usize,
2542 direction: Direction,
2543 is_start: bool,
2544) -> DisplayPoint {
2545 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2546 return display_point;
2547 };
2548
2549 for _ in 0..times {
2550 let point = map.display_point_to_point(display_point, Bias::Left);
2551 let offset = point.to_offset(&map.buffer_snapshot);
2552 let range = if direction == Direction::Prev {
2553 0..offset
2554 } else {
2555 offset..buffer.len()
2556 };
2557
2558 let possibilities = buffer
2559 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2560 .filter_map(|(range, object)| {
2561 if !matches!(object, language::TextObject::AroundFunction) {
2562 return None;
2563 }
2564
2565 let relevant = if is_start { range.start } else { range.end };
2566 if direction == Direction::Prev && relevant < offset {
2567 Some(relevant)
2568 } else if direction == Direction::Next && relevant > offset + 1 {
2569 Some(relevant)
2570 } else {
2571 None
2572 }
2573 });
2574
2575 let dest = if direction == Direction::Prev {
2576 possibilities.max().unwrap_or(offset)
2577 } else {
2578 possibilities.min().unwrap_or(offset)
2579 };
2580 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2581 if new_point == display_point {
2582 break;
2583 }
2584 display_point = new_point;
2585 }
2586 display_point
2587}
2588
2589fn comment_motion(
2590 map: &DisplaySnapshot,
2591 mut display_point: DisplayPoint,
2592 times: usize,
2593 direction: Direction,
2594) -> DisplayPoint {
2595 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2596 return display_point;
2597 };
2598
2599 for _ in 0..times {
2600 let point = map.display_point_to_point(display_point, Bias::Left);
2601 let offset = point.to_offset(&map.buffer_snapshot);
2602 let range = if direction == Direction::Prev {
2603 0..offset
2604 } else {
2605 offset..buffer.len()
2606 };
2607
2608 let possibilities = buffer
2609 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2610 .filter_map(|(range, object)| {
2611 if !matches!(object, language::TextObject::AroundComment) {
2612 return None;
2613 }
2614
2615 let relevant = if direction == Direction::Prev {
2616 range.start
2617 } else {
2618 range.end
2619 };
2620 if direction == Direction::Prev && relevant < offset {
2621 Some(relevant)
2622 } else if direction == Direction::Next && relevant > offset + 1 {
2623 Some(relevant)
2624 } else {
2625 None
2626 }
2627 });
2628
2629 let dest = if direction == Direction::Prev {
2630 possibilities.max().unwrap_or(offset)
2631 } else {
2632 possibilities.min().unwrap_or(offset)
2633 };
2634 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2635 if new_point == display_point {
2636 break;
2637 }
2638 display_point = new_point;
2639 }
2640
2641 display_point
2642}
2643
2644fn section_motion(
2645 map: &DisplaySnapshot,
2646 mut display_point: DisplayPoint,
2647 times: usize,
2648 direction: Direction,
2649 is_start: bool,
2650) -> DisplayPoint {
2651 if map.buffer_snapshot.as_singleton().is_some() {
2652 for _ in 0..times {
2653 let offset = map
2654 .display_point_to_point(display_point, Bias::Left)
2655 .to_offset(&map.buffer_snapshot);
2656 let range = if direction == Direction::Prev {
2657 0..offset
2658 } else {
2659 offset..map.buffer_snapshot.len()
2660 };
2661
2662 // we set a max start depth here because we want a section to only be "top level"
2663 // similar to vim's default of '{' in the first column.
2664 // (and without it, ]] at the start of editor.rs is -very- slow)
2665 let mut possibilities = map
2666 .buffer_snapshot
2667 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2668 .filter(|(_, object)| {
2669 matches!(
2670 object,
2671 language::TextObject::AroundClass | language::TextObject::AroundFunction
2672 )
2673 })
2674 .collect::<Vec<_>>();
2675 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2676 let mut prev_end = None;
2677 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2678 if t == language::TextObject::AroundFunction
2679 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2680 {
2681 return None;
2682 }
2683 prev_end = Some(range.end);
2684
2685 let relevant = if is_start { range.start } else { range.end };
2686 if direction == Direction::Prev && relevant < offset {
2687 Some(relevant)
2688 } else if direction == Direction::Next && relevant > offset + 1 {
2689 Some(relevant)
2690 } else {
2691 None
2692 }
2693 });
2694
2695 let offset = if direction == Direction::Prev {
2696 possibilities.max().unwrap_or(0)
2697 } else {
2698 possibilities.min().unwrap_or(map.buffer_snapshot.len())
2699 };
2700
2701 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2702 if new_point == display_point {
2703 break;
2704 }
2705 display_point = new_point;
2706 }
2707 return display_point;
2708 };
2709
2710 for _ in 0..times {
2711 let next_point = if is_start {
2712 movement::start_of_excerpt(map, display_point, direction)
2713 } else {
2714 movement::end_of_excerpt(map, display_point, direction)
2715 };
2716 if next_point == display_point {
2717 break;
2718 }
2719 display_point = next_point;
2720 }
2721
2722 display_point
2723}
2724
2725#[cfg(test)]
2726mod test {
2727
2728 use crate::{
2729 state::Mode,
2730 test::{NeovimBackedTestContext, VimTestContext},
2731 };
2732 use editor::display_map::Inlay;
2733 use indoc::indoc;
2734 use language::Point;
2735 use multi_buffer::MultiBufferRow;
2736
2737 #[gpui::test]
2738 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2739 let mut cx = NeovimBackedTestContext::new(cx).await;
2740
2741 let initial_state = indoc! {r"ˇabc
2742 def
2743
2744 paragraph
2745 the second
2746
2747
2748
2749 third and
2750 final"};
2751
2752 // goes down once
2753 cx.set_shared_state(initial_state).await;
2754 cx.simulate_shared_keystrokes("}").await;
2755 cx.shared_state().await.assert_eq(indoc! {r"abc
2756 def
2757 ˇ
2758 paragraph
2759 the second
2760
2761
2762
2763 third and
2764 final"});
2765
2766 // goes up once
2767 cx.simulate_shared_keystrokes("{").await;
2768 cx.shared_state().await.assert_eq(initial_state);
2769
2770 // goes down twice
2771 cx.simulate_shared_keystrokes("2 }").await;
2772 cx.shared_state().await.assert_eq(indoc! {r"abc
2773 def
2774
2775 paragraph
2776 the second
2777 ˇ
2778
2779
2780 third and
2781 final"});
2782
2783 // goes down over multiple blanks
2784 cx.simulate_shared_keystrokes("}").await;
2785 cx.shared_state().await.assert_eq(indoc! {r"abc
2786 def
2787
2788 paragraph
2789 the second
2790
2791
2792
2793 third and
2794 finaˇl"});
2795
2796 // goes up twice
2797 cx.simulate_shared_keystrokes("2 {").await;
2798 cx.shared_state().await.assert_eq(indoc! {r"abc
2799 def
2800 ˇ
2801 paragraph
2802 the second
2803
2804
2805
2806 third and
2807 final"});
2808 }
2809
2810 #[gpui::test]
2811 async fn test_matching(cx: &mut gpui::TestAppContext) {
2812 let mut cx = NeovimBackedTestContext::new(cx).await;
2813
2814 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2815 do(something(with<Types>.and_arrays[0, 2]))
2816 }"})
2817 .await;
2818 cx.simulate_shared_keystrokes("%").await;
2819 cx.shared_state()
2820 .await
2821 .assert_eq(indoc! {r"func (a stringˇ) {
2822 do(something(with<Types>.and_arrays[0, 2]))
2823 }"});
2824
2825 // test it works on the last character of the line
2826 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2827 do(something(with<Types>.and_arrays[0, 2]))
2828 }"})
2829 .await;
2830 cx.simulate_shared_keystrokes("%").await;
2831 cx.shared_state()
2832 .await
2833 .assert_eq(indoc! {r"func (a string) {
2834 do(something(with<Types>.and_arrays[0, 2]))
2835 ˇ}"});
2836
2837 // test it works on immediate nesting
2838 cx.set_shared_state("ˇ{()}").await;
2839 cx.simulate_shared_keystrokes("%").await;
2840 cx.shared_state().await.assert_eq("{()ˇ}");
2841 cx.simulate_shared_keystrokes("%").await;
2842 cx.shared_state().await.assert_eq("ˇ{()}");
2843
2844 // test it works on immediate nesting inside braces
2845 cx.set_shared_state("{\n ˇ{()}\n}").await;
2846 cx.simulate_shared_keystrokes("%").await;
2847 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
2848
2849 // test it jumps to the next paren on a line
2850 cx.set_shared_state("func ˇboop() {\n}").await;
2851 cx.simulate_shared_keystrokes("%").await;
2852 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2853 }
2854
2855 #[gpui::test]
2856 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2857 let mut cx = NeovimBackedTestContext::new(cx).await;
2858
2859 // test it works with curly braces
2860 cx.set_shared_state(indoc! {r"func (a string) {
2861 do(something(with<Types>.anˇd_arrays[0, 2]))
2862 }"})
2863 .await;
2864 cx.simulate_shared_keystrokes("] }").await;
2865 cx.shared_state()
2866 .await
2867 .assert_eq(indoc! {r"func (a string) {
2868 do(something(with<Types>.and_arrays[0, 2]))
2869 ˇ}"});
2870
2871 // test it works with brackets
2872 cx.set_shared_state(indoc! {r"func (a string) {
2873 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2874 }"})
2875 .await;
2876 cx.simulate_shared_keystrokes("] )").await;
2877 cx.shared_state()
2878 .await
2879 .assert_eq(indoc! {r"func (a string) {
2880 do(something(with<Types>.and_arrays[0, 2])ˇ)
2881 }"});
2882
2883 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2884 .await;
2885 cx.simulate_shared_keystrokes("] )").await;
2886 cx.shared_state()
2887 .await
2888 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2889
2890 // test it works on immediate nesting
2891 cx.set_shared_state("{ˇ {}{}}").await;
2892 cx.simulate_shared_keystrokes("] }").await;
2893 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2894 cx.set_shared_state("(ˇ ()())").await;
2895 cx.simulate_shared_keystrokes("] )").await;
2896 cx.shared_state().await.assert_eq("( ()()ˇ)");
2897
2898 // test it works on immediate nesting inside braces
2899 cx.set_shared_state("{\n ˇ {()}\n}").await;
2900 cx.simulate_shared_keystrokes("] }").await;
2901 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
2902 cx.set_shared_state("(\n ˇ {()}\n)").await;
2903 cx.simulate_shared_keystrokes("] )").await;
2904 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
2905 }
2906
2907 #[gpui::test]
2908 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2909 let mut cx = NeovimBackedTestContext::new(cx).await;
2910
2911 // test it works with curly braces
2912 cx.set_shared_state(indoc! {r"func (a string) {
2913 do(something(with<Types>.anˇd_arrays[0, 2]))
2914 }"})
2915 .await;
2916 cx.simulate_shared_keystrokes("[ {").await;
2917 cx.shared_state()
2918 .await
2919 .assert_eq(indoc! {r"func (a string) ˇ{
2920 do(something(with<Types>.and_arrays[0, 2]))
2921 }"});
2922
2923 // test it works with brackets
2924 cx.set_shared_state(indoc! {r"func (a string) {
2925 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2926 }"})
2927 .await;
2928 cx.simulate_shared_keystrokes("[ (").await;
2929 cx.shared_state()
2930 .await
2931 .assert_eq(indoc! {r"func (a string) {
2932 doˇ(something(with<Types>.and_arrays[0, 2]))
2933 }"});
2934
2935 // test it works on immediate nesting
2936 cx.set_shared_state("{{}{} ˇ }").await;
2937 cx.simulate_shared_keystrokes("[ {").await;
2938 cx.shared_state().await.assert_eq("ˇ{{}{} }");
2939 cx.set_shared_state("(()() ˇ )").await;
2940 cx.simulate_shared_keystrokes("[ (").await;
2941 cx.shared_state().await.assert_eq("ˇ(()() )");
2942
2943 // test it works on immediate nesting inside braces
2944 cx.set_shared_state("{\n {()} ˇ\n}").await;
2945 cx.simulate_shared_keystrokes("[ {").await;
2946 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
2947 cx.set_shared_state("(\n {()} ˇ\n)").await;
2948 cx.simulate_shared_keystrokes("[ (").await;
2949 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
2950 }
2951
2952 #[gpui::test]
2953 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2954 let mut cx = NeovimBackedTestContext::new_html(cx).await;
2955
2956 cx.neovim.exec("set filetype=html").await;
2957
2958 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2959 cx.simulate_shared_keystrokes("%").await;
2960 cx.shared_state()
2961 .await
2962 .assert_eq(indoc! {r"<body><ˇ/body>"});
2963 cx.simulate_shared_keystrokes("%").await;
2964
2965 // test jumping backwards
2966 cx.shared_state()
2967 .await
2968 .assert_eq(indoc! {r"<ˇbody></body>"});
2969
2970 // test self-closing tags
2971 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2972 cx.simulate_shared_keystrokes("%").await;
2973 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
2974
2975 // test tag with attributes
2976 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
2977 </div>
2978 "})
2979 .await;
2980 cx.simulate_shared_keystrokes("%").await;
2981 cx.shared_state()
2982 .await
2983 .assert_eq(indoc! {r"<div class='test' id='main'>
2984 <ˇ/div>
2985 "});
2986
2987 // test multi-line self-closing tag
2988 cx.set_shared_state(indoc! {r#"<a>
2989 <br
2990 test = "test"
2991 /ˇ>
2992 </a>"#})
2993 .await;
2994 cx.simulate_shared_keystrokes("%").await;
2995 cx.shared_state().await.assert_eq(indoc! {r#"<a>
2996 ˇ<br
2997 test = "test"
2998 />
2999 </a>"#});
3000 }
3001
3002 #[gpui::test]
3003 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3004 let mut cx = NeovimBackedTestContext::new(cx).await;
3005
3006 // f and F
3007 cx.set_shared_state("ˇone two three four").await;
3008 cx.simulate_shared_keystrokes("f o").await;
3009 cx.shared_state().await.assert_eq("one twˇo three four");
3010 cx.simulate_shared_keystrokes(",").await;
3011 cx.shared_state().await.assert_eq("ˇone two three four");
3012 cx.simulate_shared_keystrokes("2 ;").await;
3013 cx.shared_state().await.assert_eq("one two three fˇour");
3014 cx.simulate_shared_keystrokes("shift-f e").await;
3015 cx.shared_state().await.assert_eq("one two threˇe four");
3016 cx.simulate_shared_keystrokes("2 ;").await;
3017 cx.shared_state().await.assert_eq("onˇe two three four");
3018 cx.simulate_shared_keystrokes(",").await;
3019 cx.shared_state().await.assert_eq("one two thrˇee four");
3020
3021 // t and T
3022 cx.set_shared_state("ˇone two three four").await;
3023 cx.simulate_shared_keystrokes("t o").await;
3024 cx.shared_state().await.assert_eq("one tˇwo three four");
3025 cx.simulate_shared_keystrokes(",").await;
3026 cx.shared_state().await.assert_eq("oˇne two three four");
3027 cx.simulate_shared_keystrokes("2 ;").await;
3028 cx.shared_state().await.assert_eq("one two three ˇfour");
3029 cx.simulate_shared_keystrokes("shift-t e").await;
3030 cx.shared_state().await.assert_eq("one two threeˇ four");
3031 cx.simulate_shared_keystrokes("3 ;").await;
3032 cx.shared_state().await.assert_eq("oneˇ two three four");
3033 cx.simulate_shared_keystrokes(",").await;
3034 cx.shared_state().await.assert_eq("one two thˇree four");
3035 }
3036
3037 #[gpui::test]
3038 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3039 let mut cx = NeovimBackedTestContext::new(cx).await;
3040 let initial_state = indoc! {r"something(ˇfoo)"};
3041 cx.set_shared_state(initial_state).await;
3042 cx.simulate_shared_keystrokes("}").await;
3043 cx.shared_state().await.assert_eq("something(fooˇ)");
3044 }
3045
3046 #[gpui::test]
3047 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3048 let mut cx = NeovimBackedTestContext::new(cx).await;
3049 cx.set_shared_state("ˇone\n two\nthree").await;
3050 cx.simulate_shared_keystrokes("enter").await;
3051 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3052 }
3053
3054 #[gpui::test]
3055 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3056 let mut cx = NeovimBackedTestContext::new(cx).await;
3057 cx.set_shared_state("ˇ one\n two \nthree").await;
3058 cx.simulate_shared_keystrokes("g _").await;
3059 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3060
3061 cx.set_shared_state("ˇ one \n two \nthree").await;
3062 cx.simulate_shared_keystrokes("g _").await;
3063 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3064 cx.simulate_shared_keystrokes("2 g _").await;
3065 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3066 }
3067
3068 #[gpui::test]
3069 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3070 let mut cx = NeovimBackedTestContext::new(cx).await;
3071 let initial_state = indoc! {r"abc
3072 def
3073 paragraph
3074 the second
3075 third ˇand
3076 final"};
3077
3078 cx.set_shared_state(initial_state).await;
3079 cx.simulate_shared_keystrokes("shift-h").await;
3080 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3081 def
3082 paragraph
3083 the second
3084 third and
3085 final"});
3086
3087 // clip point
3088 cx.set_shared_state(indoc! {r"
3089 1 2 3
3090 4 5 6
3091 7 8 ˇ9
3092 "})
3093 .await;
3094 cx.simulate_shared_keystrokes("shift-h").await;
3095 cx.shared_state().await.assert_eq(indoc! {"
3096 1 2 ˇ3
3097 4 5 6
3098 7 8 9
3099 "});
3100
3101 cx.set_shared_state(indoc! {r"
3102 1 2 3
3103 4 5 6
3104 ˇ7 8 9
3105 "})
3106 .await;
3107 cx.simulate_shared_keystrokes("shift-h").await;
3108 cx.shared_state().await.assert_eq(indoc! {"
3109 ˇ1 2 3
3110 4 5 6
3111 7 8 9
3112 "});
3113
3114 cx.set_shared_state(indoc! {r"
3115 1 2 3
3116 4 5 ˇ6
3117 7 8 9"})
3118 .await;
3119 cx.simulate_shared_keystrokes("9 shift-h").await;
3120 cx.shared_state().await.assert_eq(indoc! {"
3121 1 2 3
3122 4 5 6
3123 7 8 ˇ9"});
3124 }
3125
3126 #[gpui::test]
3127 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3128 let mut cx = NeovimBackedTestContext::new(cx).await;
3129 let initial_state = indoc! {r"abˇc
3130 def
3131 paragraph
3132 the second
3133 third and
3134 final"};
3135
3136 cx.set_shared_state(initial_state).await;
3137 cx.simulate_shared_keystrokes("shift-m").await;
3138 cx.shared_state().await.assert_eq(indoc! {r"abc
3139 def
3140 paˇragraph
3141 the second
3142 third and
3143 final"});
3144
3145 cx.set_shared_state(indoc! {r"
3146 1 2 3
3147 4 5 6
3148 7 8 ˇ9
3149 "})
3150 .await;
3151 cx.simulate_shared_keystrokes("shift-m").await;
3152 cx.shared_state().await.assert_eq(indoc! {"
3153 1 2 3
3154 4 5 ˇ6
3155 7 8 9
3156 "});
3157 cx.set_shared_state(indoc! {r"
3158 1 2 3
3159 4 5 6
3160 ˇ7 8 9
3161 "})
3162 .await;
3163 cx.simulate_shared_keystrokes("shift-m").await;
3164 cx.shared_state().await.assert_eq(indoc! {"
3165 1 2 3
3166 ˇ4 5 6
3167 7 8 9
3168 "});
3169 cx.set_shared_state(indoc! {r"
3170 ˇ1 2 3
3171 4 5 6
3172 7 8 9
3173 "})
3174 .await;
3175 cx.simulate_shared_keystrokes("shift-m").await;
3176 cx.shared_state().await.assert_eq(indoc! {"
3177 1 2 3
3178 ˇ4 5 6
3179 7 8 9
3180 "});
3181 cx.set_shared_state(indoc! {r"
3182 1 2 3
3183 ˇ4 5 6
3184 7 8 9
3185 "})
3186 .await;
3187 cx.simulate_shared_keystrokes("shift-m").await;
3188 cx.shared_state().await.assert_eq(indoc! {"
3189 1 2 3
3190 ˇ4 5 6
3191 7 8 9
3192 "});
3193 cx.set_shared_state(indoc! {r"
3194 1 2 3
3195 4 5 ˇ6
3196 7 8 9
3197 "})
3198 .await;
3199 cx.simulate_shared_keystrokes("shift-m").await;
3200 cx.shared_state().await.assert_eq(indoc! {"
3201 1 2 3
3202 4 5 ˇ6
3203 7 8 9
3204 "});
3205 }
3206
3207 #[gpui::test]
3208 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3209 let mut cx = NeovimBackedTestContext::new(cx).await;
3210 let initial_state = indoc! {r"abc
3211 deˇf
3212 paragraph
3213 the second
3214 third and
3215 final"};
3216
3217 cx.set_shared_state(initial_state).await;
3218 cx.simulate_shared_keystrokes("shift-l").await;
3219 cx.shared_state().await.assert_eq(indoc! {r"abc
3220 def
3221 paragraph
3222 the second
3223 third and
3224 fiˇnal"});
3225
3226 cx.set_shared_state(indoc! {r"
3227 1 2 3
3228 4 5 ˇ6
3229 7 8 9
3230 "})
3231 .await;
3232 cx.simulate_shared_keystrokes("shift-l").await;
3233 cx.shared_state().await.assert_eq(indoc! {"
3234 1 2 3
3235 4 5 6
3236 7 8 9
3237 ˇ"});
3238
3239 cx.set_shared_state(indoc! {r"
3240 1 2 3
3241 ˇ4 5 6
3242 7 8 9
3243 "})
3244 .await;
3245 cx.simulate_shared_keystrokes("shift-l").await;
3246 cx.shared_state().await.assert_eq(indoc! {"
3247 1 2 3
3248 4 5 6
3249 7 8 9
3250 ˇ"});
3251
3252 cx.set_shared_state(indoc! {r"
3253 1 2 ˇ3
3254 4 5 6
3255 7 8 9
3256 "})
3257 .await;
3258 cx.simulate_shared_keystrokes("shift-l").await;
3259 cx.shared_state().await.assert_eq(indoc! {"
3260 1 2 3
3261 4 5 6
3262 7 8 9
3263 ˇ"});
3264
3265 cx.set_shared_state(indoc! {r"
3266 ˇ1 2 3
3267 4 5 6
3268 7 8 9
3269 "})
3270 .await;
3271 cx.simulate_shared_keystrokes("shift-l").await;
3272 cx.shared_state().await.assert_eq(indoc! {"
3273 1 2 3
3274 4 5 6
3275 7 8 9
3276 ˇ"});
3277
3278 cx.set_shared_state(indoc! {r"
3279 1 2 3
3280 4 5 ˇ6
3281 7 8 9
3282 "})
3283 .await;
3284 cx.simulate_shared_keystrokes("9 shift-l").await;
3285 cx.shared_state().await.assert_eq(indoc! {"
3286 1 2 ˇ3
3287 4 5 6
3288 7 8 9
3289 "});
3290 }
3291
3292 #[gpui::test]
3293 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3294 let mut cx = NeovimBackedTestContext::new(cx).await;
3295 cx.set_shared_state(indoc! {r"
3296 456 5ˇ67 678
3297 "})
3298 .await;
3299 cx.simulate_shared_keystrokes("g e").await;
3300 cx.shared_state().await.assert_eq(indoc! {"
3301 45ˇ6 567 678
3302 "});
3303
3304 // Test times
3305 cx.set_shared_state(indoc! {r"
3306 123 234 345
3307 456 5ˇ67 678
3308 "})
3309 .await;
3310 cx.simulate_shared_keystrokes("4 g e").await;
3311 cx.shared_state().await.assert_eq(indoc! {"
3312 12ˇ3 234 345
3313 456 567 678
3314 "});
3315
3316 // With punctuation
3317 cx.set_shared_state(indoc! {r"
3318 123 234 345
3319 4;5.6 5ˇ67 678
3320 789 890 901
3321 "})
3322 .await;
3323 cx.simulate_shared_keystrokes("g e").await;
3324 cx.shared_state().await.assert_eq(indoc! {"
3325 123 234 345
3326 4;5.ˇ6 567 678
3327 789 890 901
3328 "});
3329
3330 // With punctuation and count
3331 cx.set_shared_state(indoc! {r"
3332 123 234 345
3333 4;5.6 5ˇ67 678
3334 789 890 901
3335 "})
3336 .await;
3337 cx.simulate_shared_keystrokes("5 g e").await;
3338 cx.shared_state().await.assert_eq(indoc! {"
3339 123 234 345
3340 ˇ4;5.6 567 678
3341 789 890 901
3342 "});
3343
3344 // newlines
3345 cx.set_shared_state(indoc! {r"
3346 123 234 345
3347
3348 78ˇ9 890 901
3349 "})
3350 .await;
3351 cx.simulate_shared_keystrokes("g e").await;
3352 cx.shared_state().await.assert_eq(indoc! {"
3353 123 234 345
3354 ˇ
3355 789 890 901
3356 "});
3357 cx.simulate_shared_keystrokes("g e").await;
3358 cx.shared_state().await.assert_eq(indoc! {"
3359 123 234 34ˇ5
3360
3361 789 890 901
3362 "});
3363
3364 // With punctuation
3365 cx.set_shared_state(indoc! {r"
3366 123 234 345
3367 4;5.ˇ6 567 678
3368 789 890 901
3369 "})
3370 .await;
3371 cx.simulate_shared_keystrokes("g shift-e").await;
3372 cx.shared_state().await.assert_eq(indoc! {"
3373 123 234 34ˇ5
3374 4;5.6 567 678
3375 789 890 901
3376 "});
3377 }
3378
3379 #[gpui::test]
3380 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3381 let mut cx = NeovimBackedTestContext::new(cx).await;
3382
3383 cx.set_shared_state(indoc! {"
3384 fn aˇ() {
3385 return
3386 }
3387 "})
3388 .await;
3389 cx.simulate_shared_keystrokes("v $ %").await;
3390 cx.shared_state().await.assert_eq(indoc! {"
3391 fn a«() {
3392 return
3393 }ˇ»
3394 "});
3395 }
3396
3397 #[gpui::test]
3398 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3399 let mut cx = VimTestContext::new(cx, true).await;
3400
3401 cx.set_state(
3402 indoc! {"
3403 struct Foo {
3404 ˇ
3405 }
3406 "},
3407 Mode::Normal,
3408 );
3409
3410 cx.update_editor(|editor, _window, cx| {
3411 let range = editor.selections.newest_anchor().range();
3412 let inlay_text = " field: int,\n field2: string\n field3: float";
3413 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3414 editor.splice_inlays(&[], vec![inlay], cx);
3415 });
3416
3417 cx.simulate_keystrokes("j");
3418 cx.assert_state(
3419 indoc! {"
3420 struct Foo {
3421
3422 ˇ}
3423 "},
3424 Mode::Normal,
3425 );
3426 }
3427
3428 #[gpui::test]
3429 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3430 let mut cx = VimTestContext::new(cx, true).await;
3431
3432 cx.set_state(
3433 indoc! {"
3434 ˇstruct Foo {
3435
3436 }
3437 "},
3438 Mode::Normal,
3439 );
3440 cx.update_editor(|editor, _window, cx| {
3441 let snapshot = editor.buffer().read(cx).snapshot(cx);
3442 let end_of_line =
3443 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3444 let inlay_text = " hint";
3445 let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3446 editor.splice_inlays(&[], vec![inlay], cx);
3447 });
3448 cx.simulate_keystrokes("$");
3449 cx.assert_state(
3450 indoc! {"
3451 struct Foo ˇ{
3452
3453 }
3454 "},
3455 Mode::Normal,
3456 );
3457 }
3458
3459 #[gpui::test]
3460 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3461 let mut cx = NeovimBackedTestContext::new(cx).await;
3462 // Normal mode
3463 cx.set_shared_state(indoc! {"
3464 The ˇquick brown
3465 fox jumps over
3466 the lazy dog
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 .await;
3474 cx.simulate_shared_keystrokes("2 0 %").await;
3475 cx.shared_state().await.assert_eq(indoc! {"
3476 The quick brown
3477 fox ˇjumps over
3478 the lazy dog
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
3486 cx.simulate_shared_keystrokes("2 5 %").await;
3487 cx.shared_state().await.assert_eq(indoc! {"
3488 The quick brown
3489 fox jumps over
3490 the ˇlazy dog
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
3498 cx.simulate_shared_keystrokes("7 5 %").await;
3499 cx.shared_state().await.assert_eq(indoc! {"
3500 The quick brown
3501 fox jumps over
3502 the lazy dog
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
3510 // Visual mode
3511 cx.set_shared_state(indoc! {"
3512 The ˇquick brown
3513 fox jumps over
3514 the lazy dog
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 .await;
3522 cx.simulate_shared_keystrokes("v 5 0 %").await;
3523 cx.shared_state().await.assert_eq(indoc! {"
3524 The «quick brown
3525 fox jumps over
3526 the lazy dog
3527 The quick brown
3528 fox jˇ»umps over
3529 the lazy dog
3530 The quick brown
3531 fox jumps over
3532 the lazy dog"});
3533
3534 cx.set_shared_state(indoc! {"
3535 The ˇquick brown
3536 fox jumps over
3537 the lazy dog
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 .await;
3545 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3546 cx.shared_state().await.assert_eq(indoc! {"
3547 The «quick brown
3548 fox jumps over
3549 the lazy dog
3550 The quick brown
3551 fox jumps over
3552 the lazy dog
3553 The quick brown
3554 fox jumps over
3555 the lˇ»azy dog"});
3556 }
3557
3558 #[gpui::test]
3559 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3560 let mut cx = NeovimBackedTestContext::new(cx).await;
3561
3562 cx.set_shared_state("ˇπππππ").await;
3563 cx.simulate_shared_keystrokes("3 space").await;
3564 cx.shared_state().await.assert_eq("πππˇππ");
3565 }
3566}