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