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