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 point.column += 1;
1705 }
1706 for _ in 0..times {
1707 let new_point = movement::find_preceding_boundary_point(
1708 &map.buffer_snapshot,
1709 point,
1710 FindRange::MultiLine,
1711 |left, right| {
1712 let left_kind = classifier.kind(left);
1713 let right_kind = classifier.kind(right);
1714 match (left_kind, right_kind) {
1715 (CharKind::Punctuation, CharKind::Whitespace)
1716 | (CharKind::Punctuation, CharKind::Word)
1717 | (CharKind::Word, CharKind::Whitespace)
1718 | (CharKind::Word, CharKind::Punctuation) => true,
1719 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1720 _ => false,
1721 }
1722 },
1723 );
1724 if new_point == point {
1725 break;
1726 }
1727 point = new_point;
1728 }
1729 movement::saturating_left(map, point.to_display_point(map))
1730}
1731
1732fn next_subword_start(
1733 map: &DisplaySnapshot,
1734 mut point: DisplayPoint,
1735 ignore_punctuation: bool,
1736 times: usize,
1737) -> DisplayPoint {
1738 let classifier = map
1739 .buffer_snapshot
1740 .char_classifier_at(point.to_point(map))
1741 .ignore_punctuation(ignore_punctuation);
1742 for _ in 0..times {
1743 let mut crossed_newline = false;
1744 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1745 let left_kind = classifier.kind(left);
1746 let right_kind = classifier.kind(right);
1747 let at_newline = right == '\n';
1748
1749 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1750 let is_subword_start =
1751 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1752
1753 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1754 || at_newline && crossed_newline
1755 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1756
1757 crossed_newline |= at_newline;
1758 found
1759 });
1760 if point == new_point {
1761 break;
1762 }
1763 point = new_point;
1764 }
1765 point
1766}
1767
1768pub(crate) fn next_subword_end(
1769 map: &DisplaySnapshot,
1770 mut point: DisplayPoint,
1771 ignore_punctuation: bool,
1772 times: usize,
1773 allow_cross_newline: bool,
1774) -> DisplayPoint {
1775 let classifier = map
1776 .buffer_snapshot
1777 .char_classifier_at(point.to_point(map))
1778 .ignore_punctuation(ignore_punctuation);
1779 for _ in 0..times {
1780 let new_point = next_char(map, point, allow_cross_newline);
1781
1782 let mut crossed_newline = false;
1783 let mut need_backtrack = false;
1784 let new_point =
1785 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1786 let left_kind = classifier.kind(left);
1787 let right_kind = classifier.kind(right);
1788 let at_newline = right == '\n';
1789
1790 if !allow_cross_newline && at_newline {
1791 return true;
1792 }
1793
1794 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1795 let is_subword_end =
1796 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1797
1798 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1799
1800 if found && (is_word_end || is_subword_end) {
1801 need_backtrack = true;
1802 }
1803
1804 crossed_newline |= at_newline;
1805 found
1806 });
1807 let mut new_point = map.clip_point(new_point, Bias::Left);
1808 if need_backtrack {
1809 *new_point.column_mut() -= 1;
1810 }
1811 let new_point = map.clip_point(new_point, Bias::Left);
1812 if point == new_point {
1813 break;
1814 }
1815 point = new_point;
1816 }
1817 point
1818}
1819
1820fn previous_subword_start(
1821 map: &DisplaySnapshot,
1822 mut point: DisplayPoint,
1823 ignore_punctuation: bool,
1824 times: usize,
1825) -> DisplayPoint {
1826 let classifier = map
1827 .buffer_snapshot
1828 .char_classifier_at(point.to_point(map))
1829 .ignore_punctuation(ignore_punctuation);
1830 for _ in 0..times {
1831 let mut crossed_newline = false;
1832 // This works even though find_preceding_boundary is called for every character in the line containing
1833 // cursor because the newline is checked only once.
1834 let new_point = movement::find_preceding_boundary_display_point(
1835 map,
1836 point,
1837 FindRange::MultiLine,
1838 |left, right| {
1839 let left_kind = classifier.kind(left);
1840 let right_kind = classifier.kind(right);
1841 let at_newline = right == '\n';
1842
1843 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1844 let is_subword_start =
1845 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1846
1847 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1848 || at_newline && crossed_newline
1849 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1850
1851 crossed_newline |= at_newline;
1852
1853 found
1854 },
1855 );
1856 if point == new_point {
1857 break;
1858 }
1859 point = new_point;
1860 }
1861 point
1862}
1863
1864fn previous_subword_end(
1865 map: &DisplaySnapshot,
1866 point: DisplayPoint,
1867 ignore_punctuation: bool,
1868 times: usize,
1869) -> DisplayPoint {
1870 let classifier = map
1871 .buffer_snapshot
1872 .char_classifier_at(point.to_point(map))
1873 .ignore_punctuation(ignore_punctuation);
1874 let mut point = point.to_point(map);
1875
1876 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1877 point.column += 1;
1878 }
1879 for _ in 0..times {
1880 let new_point = movement::find_preceding_boundary_point(
1881 &map.buffer_snapshot,
1882 point,
1883 FindRange::MultiLine,
1884 |left, right| {
1885 let left_kind = classifier.kind(left);
1886 let right_kind = classifier.kind(right);
1887
1888 let is_subword_end =
1889 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1890
1891 if is_subword_end {
1892 return true;
1893 }
1894
1895 match (left_kind, right_kind) {
1896 (CharKind::Word, CharKind::Whitespace)
1897 | (CharKind::Word, CharKind::Punctuation) => true,
1898 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1899 _ => false,
1900 }
1901 },
1902 );
1903 if new_point == point {
1904 break;
1905 }
1906 point = new_point;
1907 }
1908 movement::saturating_left(map, point.to_display_point(map))
1909}
1910
1911pub(crate) fn first_non_whitespace(
1912 map: &DisplaySnapshot,
1913 display_lines: bool,
1914 from: DisplayPoint,
1915) -> DisplayPoint {
1916 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1917 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1918 for (ch, offset) in map.buffer_chars_at(start_offset) {
1919 if ch == '\n' {
1920 return from;
1921 }
1922
1923 start_offset = offset;
1924
1925 if classifier.kind(ch) != CharKind::Whitespace {
1926 break;
1927 }
1928 }
1929
1930 start_offset.to_display_point(map)
1931}
1932
1933pub(crate) fn last_non_whitespace(
1934 map: &DisplaySnapshot,
1935 from: DisplayPoint,
1936 count: usize,
1937) -> DisplayPoint {
1938 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1939 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1940
1941 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1942 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1943 if classifier.kind(ch) != CharKind::Whitespace {
1944 return end_of_line.to_display_point(map);
1945 }
1946 }
1947
1948 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1949 if ch == '\n' {
1950 break;
1951 }
1952 end_of_line = offset;
1953 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1954 break;
1955 }
1956 }
1957
1958 end_of_line.to_display_point(map)
1959}
1960
1961pub(crate) fn start_of_line(
1962 map: &DisplaySnapshot,
1963 display_lines: bool,
1964 point: DisplayPoint,
1965) -> DisplayPoint {
1966 if display_lines {
1967 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1968 } else {
1969 map.prev_line_boundary(point.to_point(map)).1
1970 }
1971}
1972
1973pub(crate) fn middle_of_line(
1974 map: &DisplaySnapshot,
1975 display_lines: bool,
1976 point: DisplayPoint,
1977 times: Option<usize>,
1978) -> DisplayPoint {
1979 let percent = if let Some(times) = times.filter(|&t| t <= 100) {
1980 times as f64 / 100.
1981 } else {
1982 0.5
1983 };
1984 if display_lines {
1985 map.clip_point(
1986 DisplayPoint::new(
1987 point.row(),
1988 (map.line_len(point.row()) as f64 * percent) as u32,
1989 ),
1990 Bias::Left,
1991 )
1992 } else {
1993 let mut buffer_point = point.to_point(map);
1994 buffer_point.column = (map
1995 .buffer_snapshot
1996 .line_len(MultiBufferRow(buffer_point.row)) as f64
1997 * percent) as u32;
1998
1999 map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2000 }
2001}
2002
2003pub(crate) fn end_of_line(
2004 map: &DisplaySnapshot,
2005 display_lines: bool,
2006 mut point: DisplayPoint,
2007 times: usize,
2008) -> DisplayPoint {
2009 if times > 1 {
2010 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2011 }
2012 if display_lines {
2013 map.clip_point(
2014 DisplayPoint::new(point.row(), map.line_len(point.row())),
2015 Bias::Left,
2016 )
2017 } else {
2018 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2019 }
2020}
2021
2022pub(crate) fn sentence_backwards(
2023 map: &DisplaySnapshot,
2024 point: DisplayPoint,
2025 mut times: usize,
2026) -> DisplayPoint {
2027 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
2028 let mut chars = map.reverse_buffer_chars_at(start).peekable();
2029
2030 let mut was_newline = map
2031 .buffer_chars_at(start)
2032 .next()
2033 .is_some_and(|(c, _)| c == '\n');
2034
2035 while let Some((ch, offset)) = chars.next() {
2036 let start_of_next_sentence = if was_newline && ch == '\n' {
2037 Some(offset + ch.len_utf8())
2038 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2039 Some(next_non_blank(map, offset + ch.len_utf8()))
2040 } else if ch == '.' || ch == '?' || ch == '!' {
2041 start_of_next_sentence(map, offset + ch.len_utf8())
2042 } else {
2043 None
2044 };
2045
2046 if let Some(start_of_next_sentence) = start_of_next_sentence {
2047 if start_of_next_sentence < start {
2048 times = times.saturating_sub(1);
2049 }
2050 if times == 0 || offset == 0 {
2051 return map.clip_point(
2052 start_of_next_sentence
2053 .to_offset(&map.buffer_snapshot)
2054 .to_display_point(map),
2055 Bias::Left,
2056 );
2057 }
2058 }
2059 if was_newline {
2060 start = offset;
2061 }
2062 was_newline = ch == '\n';
2063 }
2064
2065 DisplayPoint::zero()
2066}
2067
2068pub(crate) fn sentence_forwards(
2069 map: &DisplaySnapshot,
2070 point: DisplayPoint,
2071 mut times: usize,
2072) -> DisplayPoint {
2073 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
2074 let mut chars = map.buffer_chars_at(start).peekable();
2075
2076 let mut was_newline = map
2077 .reverse_buffer_chars_at(start)
2078 .next()
2079 .is_some_and(|(c, _)| c == '\n')
2080 && chars.peek().is_some_and(|(c, _)| *c == '\n');
2081
2082 while let Some((ch, offset)) = chars.next() {
2083 if was_newline && ch == '\n' {
2084 continue;
2085 }
2086 let start_of_next_sentence = if was_newline {
2087 Some(next_non_blank(map, offset))
2088 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2089 Some(next_non_blank(map, offset + ch.len_utf8()))
2090 } else if ch == '.' || ch == '?' || ch == '!' {
2091 start_of_next_sentence(map, offset + ch.len_utf8())
2092 } else {
2093 None
2094 };
2095
2096 if let Some(start_of_next_sentence) = start_of_next_sentence {
2097 times = times.saturating_sub(1);
2098 if times == 0 {
2099 return map.clip_point(
2100 start_of_next_sentence
2101 .to_offset(&map.buffer_snapshot)
2102 .to_display_point(map),
2103 Bias::Right,
2104 );
2105 }
2106 }
2107
2108 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2109 }
2110
2111 map.max_point()
2112}
2113
2114fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
2115 for (c, o) in map.buffer_chars_at(start) {
2116 if c == '\n' || !c.is_whitespace() {
2117 return o;
2118 }
2119 }
2120
2121 map.buffer_snapshot.len()
2122}
2123
2124// given the offset after a ., !, or ? find the start of the next sentence.
2125// if this is not a sentence boundary, returns None.
2126fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
2127 let chars = map.buffer_chars_at(end_of_sentence);
2128 let mut seen_space = false;
2129
2130 for (char, offset) in chars {
2131 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2132 continue;
2133 }
2134
2135 if char == '\n' && seen_space {
2136 return Some(offset);
2137 } else if char.is_whitespace() {
2138 seen_space = true;
2139 } else if seen_space {
2140 return Some(offset);
2141 } else {
2142 return None;
2143 }
2144 }
2145
2146 Some(map.buffer_snapshot.len())
2147}
2148
2149fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2150 let point = map.display_point_to_point(display_point, Bias::Left);
2151 let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2152 return display_point;
2153 };
2154 let offset = excerpt.buffer().point_to_offset(
2155 excerpt
2156 .buffer()
2157 .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2158 );
2159 let buffer_range = excerpt.buffer_range();
2160 if offset >= buffer_range.start && offset <= buffer_range.end {
2161 let point = map
2162 .buffer_snapshot
2163 .offset_to_point(excerpt.map_offset_from_buffer(offset));
2164 return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2165 }
2166 let mut last_position = None;
2167 for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2168 let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
2169 ..language::ToOffset::to_offset(&range.context.end, &buffer);
2170 if offset >= excerpt_range.start && offset <= excerpt_range.end {
2171 let text_anchor = buffer.anchor_after(offset);
2172 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2173 return anchor.to_display_point(map);
2174 } else if offset <= excerpt_range.start {
2175 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2176 return anchor.to_display_point(map);
2177 } else {
2178 last_position = Some(Anchor::in_buffer(
2179 excerpt,
2180 buffer.remote_id(),
2181 range.context.end,
2182 ));
2183 }
2184 }
2185
2186 let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2187 last_point.column = point.column;
2188
2189 map.clip_point(
2190 map.point_to_display_point(
2191 map.buffer_snapshot.clip_point(point, Bias::Left),
2192 Bias::Left,
2193 ),
2194 Bias::Left,
2195 )
2196}
2197
2198fn start_of_document(
2199 map: &DisplaySnapshot,
2200 display_point: DisplayPoint,
2201 maybe_times: Option<usize>,
2202) -> DisplayPoint {
2203 if let Some(times) = maybe_times {
2204 return go_to_line(map, display_point, times);
2205 }
2206
2207 let point = map.display_point_to_point(display_point, Bias::Left);
2208 let mut first_point = Point::zero();
2209 first_point.column = point.column;
2210
2211 map.clip_point(
2212 map.point_to_display_point(
2213 map.buffer_snapshot.clip_point(first_point, Bias::Left),
2214 Bias::Left,
2215 ),
2216 Bias::Left,
2217 )
2218}
2219
2220fn end_of_document(
2221 map: &DisplaySnapshot,
2222 display_point: DisplayPoint,
2223 maybe_times: Option<usize>,
2224) -> DisplayPoint {
2225 if let Some(times) = maybe_times {
2226 return go_to_line(map, display_point, times);
2227 };
2228 let point = map.display_point_to_point(display_point, Bias::Left);
2229 let mut last_point = map.buffer_snapshot.max_point();
2230 last_point.column = point.column;
2231
2232 map.clip_point(
2233 map.point_to_display_point(
2234 map.buffer_snapshot.clip_point(last_point, Bias::Left),
2235 Bias::Left,
2236 ),
2237 Bias::Left,
2238 )
2239}
2240
2241fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2242 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2243 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2244
2245 if head > outer.start && head < inner.start {
2246 let mut offset = inner.end.to_offset(map, Bias::Left);
2247 for c in map.buffer_snapshot.chars_at(offset) {
2248 if c == '/' || c == '\n' || c == '>' {
2249 return Some(offset.to_display_point(map));
2250 }
2251 offset += c.len_utf8();
2252 }
2253 } else {
2254 let mut offset = outer.start.to_offset(map, Bias::Left);
2255 for c in map.buffer_snapshot.chars_at(offset) {
2256 offset += c.len_utf8();
2257 if c == '<' || c == '\n' {
2258 return Some(offset.to_display_point(map));
2259 }
2260 }
2261 }
2262
2263 return None;
2264}
2265
2266fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2267 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2268 let display_point = map.clip_at_line_end(display_point);
2269 let point = display_point.to_point(map);
2270 let offset = point.to_offset(&map.buffer_snapshot);
2271
2272 // Ensure the range is contained by the current line.
2273 let mut line_end = map.next_line_boundary(point).0;
2274 if line_end == point {
2275 line_end = map.max_point().to_point(map);
2276 }
2277
2278 let line_range = map.prev_line_boundary(point).0..line_end;
2279 let visible_line_range =
2280 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2281 let ranges = map
2282 .buffer_snapshot
2283 .bracket_ranges(visible_line_range.clone());
2284 if let Some(ranges) = ranges {
2285 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2286 ..line_range.end.to_offset(&map.buffer_snapshot);
2287 let mut closest_pair_destination = None;
2288 let mut closest_distance = usize::MAX;
2289
2290 for (open_range, close_range) in ranges {
2291 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2292 if offset > open_range.start && offset < close_range.start {
2293 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2294 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2295 return display_point;
2296 }
2297 if let Some(tag) = matching_tag(map, display_point) {
2298 return tag;
2299 }
2300 } else if close_range.contains(&offset) {
2301 return open_range.start.to_display_point(map);
2302 } else if open_range.contains(&offset) {
2303 return (close_range.end - 1).to_display_point(map);
2304 }
2305 }
2306
2307 if (open_range.contains(&offset) || open_range.start >= offset)
2308 && line_range.contains(&open_range.start)
2309 {
2310 let distance = open_range.start.saturating_sub(offset);
2311 if distance < closest_distance {
2312 closest_pair_destination = Some(close_range.start);
2313 closest_distance = distance;
2314 continue;
2315 }
2316 }
2317
2318 if (close_range.contains(&offset) || close_range.start >= offset)
2319 && line_range.contains(&close_range.start)
2320 {
2321 let distance = close_range.start.saturating_sub(offset);
2322 if distance < closest_distance {
2323 closest_pair_destination = Some(open_range.start);
2324 closest_distance = distance;
2325 continue;
2326 }
2327 }
2328
2329 continue;
2330 }
2331
2332 closest_pair_destination
2333 .map(|destination| destination.to_display_point(map))
2334 .unwrap_or(display_point)
2335 } else {
2336 display_point
2337 }
2338}
2339
2340// Go to {count} percentage in the file, on the first
2341// non-blank in the line linewise. To compute the new
2342// line number this formula is used:
2343// ({count} * number-of-lines + 99) / 100
2344//
2345// https://neovim.io/doc/user/motion.html#N%25
2346fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2347 let total_lines = map.buffer_snapshot.max_point().row + 1;
2348 let target_line = (count * total_lines as usize).div_ceil(100);
2349 let target_point = DisplayPoint::new(
2350 DisplayRow(target_line.saturating_sub(1) as u32),
2351 point.column(),
2352 );
2353 map.clip_point(target_point, Bias::Left)
2354}
2355
2356fn unmatched_forward(
2357 map: &DisplaySnapshot,
2358 mut display_point: DisplayPoint,
2359 char: char,
2360 times: usize,
2361) -> DisplayPoint {
2362 for _ in 0..times {
2363 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2364 let point = display_point.to_point(map);
2365 let offset = point.to_offset(&map.buffer_snapshot);
2366
2367 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2368 let Some(ranges) = ranges else { break };
2369 let mut closest_closing_destination = None;
2370 let mut closest_distance = usize::MAX;
2371
2372 for (_, close_range) in ranges {
2373 if close_range.start > offset {
2374 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2375 if Some(char) == chars.next() {
2376 let distance = close_range.start - offset;
2377 if distance < closest_distance {
2378 closest_closing_destination = Some(close_range.start);
2379 closest_distance = distance;
2380 continue;
2381 }
2382 }
2383 }
2384 }
2385
2386 let new_point = closest_closing_destination
2387 .map(|destination| destination.to_display_point(map))
2388 .unwrap_or(display_point);
2389 if new_point == display_point {
2390 break;
2391 }
2392 display_point = new_point;
2393 }
2394 return display_point;
2395}
2396
2397fn unmatched_backward(
2398 map: &DisplaySnapshot,
2399 mut display_point: DisplayPoint,
2400 char: char,
2401 times: usize,
2402) -> DisplayPoint {
2403 for _ in 0..times {
2404 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2405 let point = display_point.to_point(map);
2406 let offset = point.to_offset(&map.buffer_snapshot);
2407
2408 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2409 let Some(ranges) = ranges else {
2410 break;
2411 };
2412
2413 let mut closest_starting_destination = None;
2414 let mut closest_distance = usize::MAX;
2415
2416 for (start_range, _) in ranges {
2417 if start_range.start < offset {
2418 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2419 if Some(char) == chars.next() {
2420 let distance = offset - start_range.start;
2421 if distance < closest_distance {
2422 closest_starting_destination = Some(start_range.start);
2423 closest_distance = distance;
2424 continue;
2425 }
2426 }
2427 }
2428 }
2429
2430 let new_point = closest_starting_destination
2431 .map(|destination| destination.to_display_point(map))
2432 .unwrap_or(display_point);
2433 if new_point == display_point {
2434 break;
2435 } else {
2436 display_point = new_point;
2437 }
2438 }
2439 display_point
2440}
2441
2442fn find_forward(
2443 map: &DisplaySnapshot,
2444 from: DisplayPoint,
2445 before: bool,
2446 target: char,
2447 times: usize,
2448 mode: FindRange,
2449 smartcase: bool,
2450) -> Option<DisplayPoint> {
2451 let mut to = from;
2452 let mut found = false;
2453
2454 for _ in 0..times {
2455 found = false;
2456 let new_to = find_boundary(map, to, mode, |_, right| {
2457 found = is_character_match(target, right, smartcase);
2458 found
2459 });
2460 if to == new_to {
2461 break;
2462 }
2463 to = new_to;
2464 }
2465
2466 if found {
2467 if before && to.column() > 0 {
2468 *to.column_mut() -= 1;
2469 Some(map.clip_point(to, Bias::Left))
2470 } else if before && to.row().0 > 0 {
2471 *to.row_mut() -= 1;
2472 *to.column_mut() = map.line(to.row()).len() as u32;
2473 Some(map.clip_point(to, Bias::Left))
2474 } else {
2475 Some(to)
2476 }
2477 } else {
2478 None
2479 }
2480}
2481
2482fn find_backward(
2483 map: &DisplaySnapshot,
2484 from: DisplayPoint,
2485 after: bool,
2486 target: char,
2487 times: usize,
2488 mode: FindRange,
2489 smartcase: bool,
2490) -> DisplayPoint {
2491 let mut to = from;
2492
2493 for _ in 0..times {
2494 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2495 is_character_match(target, right, smartcase)
2496 });
2497 if to == new_to {
2498 break;
2499 }
2500 to = new_to;
2501 }
2502
2503 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2504 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2505 if after {
2506 *to.column_mut() += 1;
2507 map.clip_point(to, Bias::Right)
2508 } else {
2509 to
2510 }
2511 } else {
2512 from
2513 }
2514}
2515
2516fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2517 if smartcase {
2518 if target.is_uppercase() {
2519 target == other
2520 } else {
2521 target == other.to_ascii_lowercase()
2522 }
2523 } else {
2524 target == other
2525 }
2526}
2527
2528fn sneak(
2529 map: &DisplaySnapshot,
2530 from: DisplayPoint,
2531 first_target: char,
2532 second_target: char,
2533 times: usize,
2534 smartcase: bool,
2535) -> Option<DisplayPoint> {
2536 let mut to = from;
2537 let mut found = false;
2538
2539 for _ in 0..times {
2540 found = false;
2541 let new_to = find_boundary(
2542 map,
2543 movement::right(map, to),
2544 FindRange::MultiLine,
2545 |left, right| {
2546 found = is_character_match(first_target, left, smartcase)
2547 && is_character_match(second_target, right, smartcase);
2548 found
2549 },
2550 );
2551 if to == new_to {
2552 break;
2553 }
2554 to = new_to;
2555 }
2556
2557 if found {
2558 Some(movement::left(map, to))
2559 } else {
2560 None
2561 }
2562}
2563
2564fn sneak_backward(
2565 map: &DisplaySnapshot,
2566 from: DisplayPoint,
2567 first_target: char,
2568 second_target: char,
2569 times: usize,
2570 smartcase: bool,
2571) -> Option<DisplayPoint> {
2572 let mut to = from;
2573 let mut found = false;
2574
2575 for _ in 0..times {
2576 found = false;
2577 let new_to =
2578 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2579 found = is_character_match(first_target, left, smartcase)
2580 && is_character_match(second_target, right, smartcase);
2581 found
2582 });
2583 if to == new_to {
2584 break;
2585 }
2586 to = new_to;
2587 }
2588
2589 if found {
2590 Some(movement::left(map, to))
2591 } else {
2592 None
2593 }
2594}
2595
2596fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2597 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2598 first_non_whitespace(map, false, correct_line)
2599}
2600
2601fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2602 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2603 first_non_whitespace(map, false, correct_line)
2604}
2605
2606fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2607 let correct_line = start_of_relative_buffer_row(map, point, 0);
2608 right(map, correct_line, times.saturating_sub(1))
2609}
2610
2611pub(crate) fn next_line_end(
2612 map: &DisplaySnapshot,
2613 mut point: DisplayPoint,
2614 times: usize,
2615) -> DisplayPoint {
2616 if times > 1 {
2617 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2618 }
2619 end_of_line(map, false, point, 1)
2620}
2621
2622fn window_top(
2623 map: &DisplaySnapshot,
2624 point: DisplayPoint,
2625 text_layout_details: &TextLayoutDetails,
2626 mut times: usize,
2627) -> (DisplayPoint, SelectionGoal) {
2628 let first_visible_line = text_layout_details
2629 .scroll_anchor
2630 .anchor
2631 .to_display_point(map);
2632
2633 if first_visible_line.row() != DisplayRow(0)
2634 && text_layout_details.vertical_scroll_margin as usize > times
2635 {
2636 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2637 }
2638
2639 if let Some(visible_rows) = text_layout_details.visible_rows {
2640 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2641 let new_row = (first_visible_line.row().0 + (times as u32))
2642 .min(bottom_row)
2643 .min(map.max_point().row().0);
2644 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2645
2646 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2647 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2648 } else {
2649 let new_row =
2650 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2651 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2652
2653 let new_point = DisplayPoint::new(new_row, new_col);
2654 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2655 }
2656}
2657
2658fn window_middle(
2659 map: &DisplaySnapshot,
2660 point: DisplayPoint,
2661 text_layout_details: &TextLayoutDetails,
2662) -> (DisplayPoint, SelectionGoal) {
2663 if let Some(visible_rows) = text_layout_details.visible_rows {
2664 let first_visible_line = text_layout_details
2665 .scroll_anchor
2666 .anchor
2667 .to_display_point(map);
2668
2669 let max_visible_rows =
2670 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2671
2672 let new_row =
2673 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2674 let new_row = DisplayRow(new_row);
2675 let new_col = point.column().min(map.line_len(new_row));
2676 let new_point = DisplayPoint::new(new_row, new_col);
2677 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2678 } else {
2679 (point, SelectionGoal::None)
2680 }
2681}
2682
2683fn window_bottom(
2684 map: &DisplaySnapshot,
2685 point: DisplayPoint,
2686 text_layout_details: &TextLayoutDetails,
2687 mut times: usize,
2688) -> (DisplayPoint, SelectionGoal) {
2689 if let Some(visible_rows) = text_layout_details.visible_rows {
2690 let first_visible_line = text_layout_details
2691 .scroll_anchor
2692 .anchor
2693 .to_display_point(map);
2694 let bottom_row = first_visible_line.row().0
2695 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2696 if bottom_row < map.max_point().row().0
2697 && text_layout_details.vertical_scroll_margin as usize > times
2698 {
2699 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2700 }
2701 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2702 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2703 {
2704 first_visible_line.row()
2705 } else {
2706 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2707 };
2708 let new_col = point.column().min(map.line_len(new_row));
2709 let new_point = DisplayPoint::new(new_row, new_col);
2710 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2711 } else {
2712 (point, SelectionGoal::None)
2713 }
2714}
2715
2716fn method_motion(
2717 map: &DisplaySnapshot,
2718 mut display_point: DisplayPoint,
2719 times: usize,
2720 direction: Direction,
2721 is_start: bool,
2722) -> DisplayPoint {
2723 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2724 return display_point;
2725 };
2726
2727 for _ in 0..times {
2728 let point = map.display_point_to_point(display_point, Bias::Left);
2729 let offset = point.to_offset(&map.buffer_snapshot);
2730 let range = if direction == Direction::Prev {
2731 0..offset
2732 } else {
2733 offset..buffer.len()
2734 };
2735
2736 let possibilities = buffer
2737 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2738 .filter_map(|(range, object)| {
2739 if !matches!(object, language::TextObject::AroundFunction) {
2740 return None;
2741 }
2742
2743 let relevant = if is_start { range.start } else { range.end };
2744 if direction == Direction::Prev && relevant < offset {
2745 Some(relevant)
2746 } else if direction == Direction::Next && relevant > offset + 1 {
2747 Some(relevant)
2748 } else {
2749 None
2750 }
2751 });
2752
2753 let dest = if direction == Direction::Prev {
2754 possibilities.max().unwrap_or(offset)
2755 } else {
2756 possibilities.min().unwrap_or(offset)
2757 };
2758 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2759 if new_point == display_point {
2760 break;
2761 }
2762 display_point = new_point;
2763 }
2764 display_point
2765}
2766
2767fn comment_motion(
2768 map: &DisplaySnapshot,
2769 mut display_point: DisplayPoint,
2770 times: usize,
2771 direction: Direction,
2772) -> DisplayPoint {
2773 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2774 return display_point;
2775 };
2776
2777 for _ in 0..times {
2778 let point = map.display_point_to_point(display_point, Bias::Left);
2779 let offset = point.to_offset(&map.buffer_snapshot);
2780 let range = if direction == Direction::Prev {
2781 0..offset
2782 } else {
2783 offset..buffer.len()
2784 };
2785
2786 let possibilities = buffer
2787 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2788 .filter_map(|(range, object)| {
2789 if !matches!(object, language::TextObject::AroundComment) {
2790 return None;
2791 }
2792
2793 let relevant = if direction == Direction::Prev {
2794 range.start
2795 } else {
2796 range.end
2797 };
2798 if direction == Direction::Prev && relevant < offset {
2799 Some(relevant)
2800 } else if direction == Direction::Next && relevant > offset + 1 {
2801 Some(relevant)
2802 } else {
2803 None
2804 }
2805 });
2806
2807 let dest = if direction == Direction::Prev {
2808 possibilities.max().unwrap_or(offset)
2809 } else {
2810 possibilities.min().unwrap_or(offset)
2811 };
2812 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2813 if new_point == display_point {
2814 break;
2815 }
2816 display_point = new_point;
2817 }
2818
2819 display_point
2820}
2821
2822fn section_motion(
2823 map: &DisplaySnapshot,
2824 mut display_point: DisplayPoint,
2825 times: usize,
2826 direction: Direction,
2827 is_start: bool,
2828) -> DisplayPoint {
2829 if map.buffer_snapshot.as_singleton().is_some() {
2830 for _ in 0..times {
2831 let offset = map
2832 .display_point_to_point(display_point, Bias::Left)
2833 .to_offset(&map.buffer_snapshot);
2834 let range = if direction == Direction::Prev {
2835 0..offset
2836 } else {
2837 offset..map.buffer_snapshot.len()
2838 };
2839
2840 // we set a max start depth here because we want a section to only be "top level"
2841 // similar to vim's default of '{' in the first column.
2842 // (and without it, ]] at the start of editor.rs is -very- slow)
2843 let mut possibilities = map
2844 .buffer_snapshot
2845 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2846 .filter(|(_, object)| {
2847 matches!(
2848 object,
2849 language::TextObject::AroundClass | language::TextObject::AroundFunction
2850 )
2851 })
2852 .collect::<Vec<_>>();
2853 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2854 let mut prev_end = None;
2855 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2856 if t == language::TextObject::AroundFunction
2857 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2858 {
2859 return None;
2860 }
2861 prev_end = Some(range.end);
2862
2863 let relevant = if is_start { range.start } else { range.end };
2864 if direction == Direction::Prev && relevant < offset {
2865 Some(relevant)
2866 } else if direction == Direction::Next && relevant > offset + 1 {
2867 Some(relevant)
2868 } else {
2869 None
2870 }
2871 });
2872
2873 let offset = if direction == Direction::Prev {
2874 possibilities.max().unwrap_or(0)
2875 } else {
2876 possibilities.min().unwrap_or(map.buffer_snapshot.len())
2877 };
2878
2879 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2880 if new_point == display_point {
2881 break;
2882 }
2883 display_point = new_point;
2884 }
2885 return display_point;
2886 };
2887
2888 for _ in 0..times {
2889 let next_point = if is_start {
2890 movement::start_of_excerpt(map, display_point, direction)
2891 } else {
2892 movement::end_of_excerpt(map, display_point, direction)
2893 };
2894 if next_point == display_point {
2895 break;
2896 }
2897 display_point = next_point;
2898 }
2899
2900 display_point
2901}
2902
2903fn matches_indent_type(
2904 target_indent: &text::LineIndent,
2905 current_indent: &text::LineIndent,
2906 indent_type: IndentType,
2907) -> bool {
2908 match indent_type {
2909 IndentType::Lesser => {
2910 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
2911 }
2912 IndentType::Greater => {
2913 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
2914 }
2915 IndentType::Same => {
2916 target_indent.spaces == current_indent.spaces
2917 && target_indent.tabs == current_indent.tabs
2918 }
2919 }
2920}
2921
2922fn indent_motion(
2923 map: &DisplaySnapshot,
2924 mut display_point: DisplayPoint,
2925 times: usize,
2926 direction: Direction,
2927 indent_type: IndentType,
2928) -> DisplayPoint {
2929 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
2930 let current_row = MultiBufferRow(buffer_point.row);
2931 let current_indent = map.line_indent_for_buffer_row(current_row);
2932 if current_indent.is_line_empty() {
2933 return display_point;
2934 }
2935 let max_row = map.max_point().to_point(map).row;
2936
2937 for _ in 0..times {
2938 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
2939
2940 let target_row = match direction {
2941 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
2942 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
2943 !indent.is_line_empty()
2944 && matches_indent_type(&indent, ¤t_indent, indent_type)
2945 }),
2946 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
2947 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
2948 !indent.is_line_empty()
2949 && matches_indent_type(&indent, ¤t_indent, indent_type)
2950 }),
2951 }
2952 .unwrap_or(current_buffer_row);
2953
2954 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
2955 let new_point = first_non_whitespace(map, false, new_point);
2956 if new_point == display_point {
2957 break;
2958 }
2959 display_point = new_point;
2960 }
2961 display_point
2962}
2963
2964#[cfg(test)]
2965mod test {
2966
2967 use crate::{
2968 state::Mode,
2969 test::{NeovimBackedTestContext, VimTestContext},
2970 };
2971 use editor::display_map::Inlay;
2972 use indoc::indoc;
2973 use language::Point;
2974 use multi_buffer::MultiBufferRow;
2975
2976 #[gpui::test]
2977 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2978 let mut cx = NeovimBackedTestContext::new(cx).await;
2979
2980 let initial_state = indoc! {r"ˇabc
2981 def
2982
2983 paragraph
2984 the second
2985
2986
2987
2988 third and
2989 final"};
2990
2991 // goes down once
2992 cx.set_shared_state(initial_state).await;
2993 cx.simulate_shared_keystrokes("}").await;
2994 cx.shared_state().await.assert_eq(indoc! {r"abc
2995 def
2996 ˇ
2997 paragraph
2998 the second
2999
3000
3001
3002 third and
3003 final"});
3004
3005 // goes up once
3006 cx.simulate_shared_keystrokes("{").await;
3007 cx.shared_state().await.assert_eq(initial_state);
3008
3009 // goes down twice
3010 cx.simulate_shared_keystrokes("2 }").await;
3011 cx.shared_state().await.assert_eq(indoc! {r"abc
3012 def
3013
3014 paragraph
3015 the second
3016 ˇ
3017
3018
3019 third and
3020 final"});
3021
3022 // goes down over multiple blanks
3023 cx.simulate_shared_keystrokes("}").await;
3024 cx.shared_state().await.assert_eq(indoc! {r"abc
3025 def
3026
3027 paragraph
3028 the second
3029
3030
3031
3032 third and
3033 finaˇl"});
3034
3035 // goes up twice
3036 cx.simulate_shared_keystrokes("2 {").await;
3037 cx.shared_state().await.assert_eq(indoc! {r"abc
3038 def
3039 ˇ
3040 paragraph
3041 the second
3042
3043
3044
3045 third and
3046 final"});
3047 }
3048
3049 #[gpui::test]
3050 async fn test_matching(cx: &mut gpui::TestAppContext) {
3051 let mut cx = NeovimBackedTestContext::new(cx).await;
3052
3053 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3054 do(something(with<Types>.and_arrays[0, 2]))
3055 }"})
3056 .await;
3057 cx.simulate_shared_keystrokes("%").await;
3058 cx.shared_state()
3059 .await
3060 .assert_eq(indoc! {r"func (a stringˇ) {
3061 do(something(with<Types>.and_arrays[0, 2]))
3062 }"});
3063
3064 // test it works on the last character of the line
3065 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3066 do(something(with<Types>.and_arrays[0, 2]))
3067 }"})
3068 .await;
3069 cx.simulate_shared_keystrokes("%").await;
3070 cx.shared_state()
3071 .await
3072 .assert_eq(indoc! {r"func (a string) {
3073 do(something(with<Types>.and_arrays[0, 2]))
3074 ˇ}"});
3075
3076 // test it works on immediate nesting
3077 cx.set_shared_state("ˇ{()}").await;
3078 cx.simulate_shared_keystrokes("%").await;
3079 cx.shared_state().await.assert_eq("{()ˇ}");
3080 cx.simulate_shared_keystrokes("%").await;
3081 cx.shared_state().await.assert_eq("ˇ{()}");
3082
3083 // test it works on immediate nesting inside braces
3084 cx.set_shared_state("{\n ˇ{()}\n}").await;
3085 cx.simulate_shared_keystrokes("%").await;
3086 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3087
3088 // test it jumps to the next paren on a line
3089 cx.set_shared_state("func ˇboop() {\n}").await;
3090 cx.simulate_shared_keystrokes("%").await;
3091 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3092 }
3093
3094 #[gpui::test]
3095 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3096 let mut cx = NeovimBackedTestContext::new(cx).await;
3097
3098 // test it works with curly braces
3099 cx.set_shared_state(indoc! {r"func (a string) {
3100 do(something(with<Types>.anˇd_arrays[0, 2]))
3101 }"})
3102 .await;
3103 cx.simulate_shared_keystrokes("] }").await;
3104 cx.shared_state()
3105 .await
3106 .assert_eq(indoc! {r"func (a string) {
3107 do(something(with<Types>.and_arrays[0, 2]))
3108 ˇ}"});
3109
3110 // test it works with brackets
3111 cx.set_shared_state(indoc! {r"func (a string) {
3112 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3113 }"})
3114 .await;
3115 cx.simulate_shared_keystrokes("] )").await;
3116 cx.shared_state()
3117 .await
3118 .assert_eq(indoc! {r"func (a string) {
3119 do(something(with<Types>.and_arrays[0, 2])ˇ)
3120 }"});
3121
3122 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3123 .await;
3124 cx.simulate_shared_keystrokes("] )").await;
3125 cx.shared_state()
3126 .await
3127 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3128
3129 // test it works on immediate nesting
3130 cx.set_shared_state("{ˇ {}{}}").await;
3131 cx.simulate_shared_keystrokes("] }").await;
3132 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3133 cx.set_shared_state("(ˇ ()())").await;
3134 cx.simulate_shared_keystrokes("] )").await;
3135 cx.shared_state().await.assert_eq("( ()()ˇ)");
3136
3137 // test it works on immediate nesting inside braces
3138 cx.set_shared_state("{\n ˇ {()}\n}").await;
3139 cx.simulate_shared_keystrokes("] }").await;
3140 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3141 cx.set_shared_state("(\n ˇ {()}\n)").await;
3142 cx.simulate_shared_keystrokes("] )").await;
3143 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3144 }
3145
3146 #[gpui::test]
3147 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3148 let mut cx = NeovimBackedTestContext::new(cx).await;
3149
3150 // test it works with curly braces
3151 cx.set_shared_state(indoc! {r"func (a string) {
3152 do(something(with<Types>.anˇd_arrays[0, 2]))
3153 }"})
3154 .await;
3155 cx.simulate_shared_keystrokes("[ {").await;
3156 cx.shared_state()
3157 .await
3158 .assert_eq(indoc! {r"func (a string) ˇ{
3159 do(something(with<Types>.and_arrays[0, 2]))
3160 }"});
3161
3162 // test it works with brackets
3163 cx.set_shared_state(indoc! {r"func (a string) {
3164 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3165 }"})
3166 .await;
3167 cx.simulate_shared_keystrokes("[ (").await;
3168 cx.shared_state()
3169 .await
3170 .assert_eq(indoc! {r"func (a string) {
3171 doˇ(something(with<Types>.and_arrays[0, 2]))
3172 }"});
3173
3174 // test it works on immediate nesting
3175 cx.set_shared_state("{{}{} ˇ }").await;
3176 cx.simulate_shared_keystrokes("[ {").await;
3177 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3178 cx.set_shared_state("(()() ˇ )").await;
3179 cx.simulate_shared_keystrokes("[ (").await;
3180 cx.shared_state().await.assert_eq("ˇ(()() )");
3181
3182 // test it works on immediate nesting inside braces
3183 cx.set_shared_state("{\n {()} ˇ\n}").await;
3184 cx.simulate_shared_keystrokes("[ {").await;
3185 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3186 cx.set_shared_state("(\n {()} ˇ\n)").await;
3187 cx.simulate_shared_keystrokes("[ (").await;
3188 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3189 }
3190
3191 #[gpui::test]
3192 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3193 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3194
3195 cx.neovim.exec("set filetype=html").await;
3196
3197 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3198 cx.simulate_shared_keystrokes("%").await;
3199 cx.shared_state()
3200 .await
3201 .assert_eq(indoc! {r"<body><ˇ/body>"});
3202 cx.simulate_shared_keystrokes("%").await;
3203
3204 // test jumping backwards
3205 cx.shared_state()
3206 .await
3207 .assert_eq(indoc! {r"<ˇbody></body>"});
3208
3209 // test self-closing tags
3210 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3211 cx.simulate_shared_keystrokes("%").await;
3212 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3213
3214 // test tag with attributes
3215 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3216 </div>
3217 "})
3218 .await;
3219 cx.simulate_shared_keystrokes("%").await;
3220 cx.shared_state()
3221 .await
3222 .assert_eq(indoc! {r"<div class='test' id='main'>
3223 <ˇ/div>
3224 "});
3225
3226 // test multi-line self-closing tag
3227 cx.set_shared_state(indoc! {r#"<a>
3228 <br
3229 test = "test"
3230 /ˇ>
3231 </a>"#})
3232 .await;
3233 cx.simulate_shared_keystrokes("%").await;
3234 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3235 ˇ<br
3236 test = "test"
3237 />
3238 </a>"#});
3239 }
3240
3241 #[gpui::test]
3242 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3243 let mut cx = NeovimBackedTestContext::new(cx).await;
3244
3245 // f and F
3246 cx.set_shared_state("ˇone two three four").await;
3247 cx.simulate_shared_keystrokes("f o").await;
3248 cx.shared_state().await.assert_eq("one twˇo three four");
3249 cx.simulate_shared_keystrokes(",").await;
3250 cx.shared_state().await.assert_eq("ˇone two three four");
3251 cx.simulate_shared_keystrokes("2 ;").await;
3252 cx.shared_state().await.assert_eq("one two three fˇour");
3253 cx.simulate_shared_keystrokes("shift-f e").await;
3254 cx.shared_state().await.assert_eq("one two threˇe four");
3255 cx.simulate_shared_keystrokes("2 ;").await;
3256 cx.shared_state().await.assert_eq("onˇe two three four");
3257 cx.simulate_shared_keystrokes(",").await;
3258 cx.shared_state().await.assert_eq("one two thrˇee four");
3259
3260 // t and T
3261 cx.set_shared_state("ˇone two three four").await;
3262 cx.simulate_shared_keystrokes("t o").await;
3263 cx.shared_state().await.assert_eq("one tˇwo three four");
3264 cx.simulate_shared_keystrokes(",").await;
3265 cx.shared_state().await.assert_eq("oˇne two three four");
3266 cx.simulate_shared_keystrokes("2 ;").await;
3267 cx.shared_state().await.assert_eq("one two three ˇfour");
3268 cx.simulate_shared_keystrokes("shift-t e").await;
3269 cx.shared_state().await.assert_eq("one two threeˇ four");
3270 cx.simulate_shared_keystrokes("3 ;").await;
3271 cx.shared_state().await.assert_eq("oneˇ two three four");
3272 cx.simulate_shared_keystrokes(",").await;
3273 cx.shared_state().await.assert_eq("one two thˇree four");
3274 }
3275
3276 #[gpui::test]
3277 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3278 let mut cx = NeovimBackedTestContext::new(cx).await;
3279 let initial_state = indoc! {r"something(ˇfoo)"};
3280 cx.set_shared_state(initial_state).await;
3281 cx.simulate_shared_keystrokes("}").await;
3282 cx.shared_state().await.assert_eq("something(fooˇ)");
3283 }
3284
3285 #[gpui::test]
3286 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3287 let mut cx = NeovimBackedTestContext::new(cx).await;
3288 cx.set_shared_state("ˇone\n two\nthree").await;
3289 cx.simulate_shared_keystrokes("enter").await;
3290 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3291 }
3292
3293 #[gpui::test]
3294 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3295 let mut cx = NeovimBackedTestContext::new(cx).await;
3296 cx.set_shared_state("ˇ one\n two \nthree").await;
3297 cx.simulate_shared_keystrokes("g _").await;
3298 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3299
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 cx.simulate_shared_keystrokes("2 g _").await;
3304 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3305 }
3306
3307 #[gpui::test]
3308 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3309 let mut cx = NeovimBackedTestContext::new(cx).await;
3310 let initial_state = indoc! {r"abc
3311 def
3312 paragraph
3313 the second
3314 third ˇand
3315 final"};
3316
3317 cx.set_shared_state(initial_state).await;
3318 cx.simulate_shared_keystrokes("shift-h").await;
3319 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3320 def
3321 paragraph
3322 the second
3323 third and
3324 final"});
3325
3326 // clip point
3327 cx.set_shared_state(indoc! {r"
3328 1 2 3
3329 4 5 6
3330 7 8 ˇ9
3331 "})
3332 .await;
3333 cx.simulate_shared_keystrokes("shift-h").await;
3334 cx.shared_state().await.assert_eq(indoc! {"
3335 1 2 ˇ3
3336 4 5 6
3337 7 8 9
3338 "});
3339
3340 cx.set_shared_state(indoc! {r"
3341 1 2 3
3342 4 5 6
3343 ˇ7 8 9
3344 "})
3345 .await;
3346 cx.simulate_shared_keystrokes("shift-h").await;
3347 cx.shared_state().await.assert_eq(indoc! {"
3348 ˇ1 2 3
3349 4 5 6
3350 7 8 9
3351 "});
3352
3353 cx.set_shared_state(indoc! {r"
3354 1 2 3
3355 4 5 ˇ6
3356 7 8 9"})
3357 .await;
3358 cx.simulate_shared_keystrokes("9 shift-h").await;
3359 cx.shared_state().await.assert_eq(indoc! {"
3360 1 2 3
3361 4 5 6
3362 7 8 ˇ9"});
3363 }
3364
3365 #[gpui::test]
3366 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3367 let mut cx = NeovimBackedTestContext::new(cx).await;
3368 let initial_state = indoc! {r"abˇc
3369 def
3370 paragraph
3371 the second
3372 third and
3373 final"};
3374
3375 cx.set_shared_state(initial_state).await;
3376 cx.simulate_shared_keystrokes("shift-m").await;
3377 cx.shared_state().await.assert_eq(indoc! {r"abc
3378 def
3379 paˇragraph
3380 the second
3381 third and
3382 final"});
3383
3384 cx.set_shared_state(indoc! {r"
3385 1 2 3
3386 4 5 6
3387 7 8 ˇ9
3388 "})
3389 .await;
3390 cx.simulate_shared_keystrokes("shift-m").await;
3391 cx.shared_state().await.assert_eq(indoc! {"
3392 1 2 3
3393 4 5 ˇ6
3394 7 8 9
3395 "});
3396 cx.set_shared_state(indoc! {r"
3397 1 2 3
3398 4 5 6
3399 ˇ7 8 9
3400 "})
3401 .await;
3402 cx.simulate_shared_keystrokes("shift-m").await;
3403 cx.shared_state().await.assert_eq(indoc! {"
3404 1 2 3
3405 ˇ4 5 6
3406 7 8 9
3407 "});
3408 cx.set_shared_state(indoc! {r"
3409 ˇ1 2 3
3410 4 5 6
3411 7 8 9
3412 "})
3413 .await;
3414 cx.simulate_shared_keystrokes("shift-m").await;
3415 cx.shared_state().await.assert_eq(indoc! {"
3416 1 2 3
3417 ˇ4 5 6
3418 7 8 9
3419 "});
3420 cx.set_shared_state(indoc! {r"
3421 1 2 3
3422 ˇ4 5 6
3423 7 8 9
3424 "})
3425 .await;
3426 cx.simulate_shared_keystrokes("shift-m").await;
3427 cx.shared_state().await.assert_eq(indoc! {"
3428 1 2 3
3429 ˇ4 5 6
3430 7 8 9
3431 "});
3432 cx.set_shared_state(indoc! {r"
3433 1 2 3
3434 4 5 ˇ6
3435 7 8 9
3436 "})
3437 .await;
3438 cx.simulate_shared_keystrokes("shift-m").await;
3439 cx.shared_state().await.assert_eq(indoc! {"
3440 1 2 3
3441 4 5 ˇ6
3442 7 8 9
3443 "});
3444 }
3445
3446 #[gpui::test]
3447 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3448 let mut cx = NeovimBackedTestContext::new(cx).await;
3449 let initial_state = indoc! {r"abc
3450 deˇf
3451 paragraph
3452 the second
3453 third and
3454 final"};
3455
3456 cx.set_shared_state(initial_state).await;
3457 cx.simulate_shared_keystrokes("shift-l").await;
3458 cx.shared_state().await.assert_eq(indoc! {r"abc
3459 def
3460 paragraph
3461 the second
3462 third and
3463 fiˇnal"});
3464
3465 cx.set_shared_state(indoc! {r"
3466 1 2 3
3467 4 5 ˇ6
3468 7 8 9
3469 "})
3470 .await;
3471 cx.simulate_shared_keystrokes("shift-l").await;
3472 cx.shared_state().await.assert_eq(indoc! {"
3473 1 2 3
3474 4 5 6
3475 7 8 9
3476 ˇ"});
3477
3478 cx.set_shared_state(indoc! {r"
3479 1 2 3
3480 ˇ4 5 6
3481 7 8 9
3482 "})
3483 .await;
3484 cx.simulate_shared_keystrokes("shift-l").await;
3485 cx.shared_state().await.assert_eq(indoc! {"
3486 1 2 3
3487 4 5 6
3488 7 8 9
3489 ˇ"});
3490
3491 cx.set_shared_state(indoc! {r"
3492 1 2 ˇ3
3493 4 5 6
3494 7 8 9
3495 "})
3496 .await;
3497 cx.simulate_shared_keystrokes("shift-l").await;
3498 cx.shared_state().await.assert_eq(indoc! {"
3499 1 2 3
3500 4 5 6
3501 7 8 9
3502 ˇ"});
3503
3504 cx.set_shared_state(indoc! {r"
3505 ˇ1 2 3
3506 4 5 6
3507 7 8 9
3508 "})
3509 .await;
3510 cx.simulate_shared_keystrokes("shift-l").await;
3511 cx.shared_state().await.assert_eq(indoc! {"
3512 1 2 3
3513 4 5 6
3514 7 8 9
3515 ˇ"});
3516
3517 cx.set_shared_state(indoc! {r"
3518 1 2 3
3519 4 5 ˇ6
3520 7 8 9
3521 "})
3522 .await;
3523 cx.simulate_shared_keystrokes("9 shift-l").await;
3524 cx.shared_state().await.assert_eq(indoc! {"
3525 1 2 ˇ3
3526 4 5 6
3527 7 8 9
3528 "});
3529 }
3530
3531 #[gpui::test]
3532 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3533 let mut cx = NeovimBackedTestContext::new(cx).await;
3534 cx.set_shared_state(indoc! {r"
3535 456 5ˇ67 678
3536 "})
3537 .await;
3538 cx.simulate_shared_keystrokes("g e").await;
3539 cx.shared_state().await.assert_eq(indoc! {"
3540 45ˇ6 567 678
3541 "});
3542
3543 // Test times
3544 cx.set_shared_state(indoc! {r"
3545 123 234 345
3546 456 5ˇ67 678
3547 "})
3548 .await;
3549 cx.simulate_shared_keystrokes("4 g e").await;
3550 cx.shared_state().await.assert_eq(indoc! {"
3551 12ˇ3 234 345
3552 456 567 678
3553 "});
3554
3555 // With punctuation
3556 cx.set_shared_state(indoc! {r"
3557 123 234 345
3558 4;5.6 5ˇ67 678
3559 789 890 901
3560 "})
3561 .await;
3562 cx.simulate_shared_keystrokes("g e").await;
3563 cx.shared_state().await.assert_eq(indoc! {"
3564 123 234 345
3565 4;5.ˇ6 567 678
3566 789 890 901
3567 "});
3568
3569 // With punctuation and count
3570 cx.set_shared_state(indoc! {r"
3571 123 234 345
3572 4;5.6 5ˇ67 678
3573 789 890 901
3574 "})
3575 .await;
3576 cx.simulate_shared_keystrokes("5 g e").await;
3577 cx.shared_state().await.assert_eq(indoc! {"
3578 123 234 345
3579 ˇ4;5.6 567 678
3580 789 890 901
3581 "});
3582
3583 // newlines
3584 cx.set_shared_state(indoc! {r"
3585 123 234 345
3586
3587 78ˇ9 890 901
3588 "})
3589 .await;
3590 cx.simulate_shared_keystrokes("g e").await;
3591 cx.shared_state().await.assert_eq(indoc! {"
3592 123 234 345
3593 ˇ
3594 789 890 901
3595 "});
3596 cx.simulate_shared_keystrokes("g e").await;
3597 cx.shared_state().await.assert_eq(indoc! {"
3598 123 234 34ˇ5
3599
3600 789 890 901
3601 "});
3602
3603 // With punctuation
3604 cx.set_shared_state(indoc! {r"
3605 123 234 345
3606 4;5.ˇ6 567 678
3607 789 890 901
3608 "})
3609 .await;
3610 cx.simulate_shared_keystrokes("g shift-e").await;
3611 cx.shared_state().await.assert_eq(indoc! {"
3612 123 234 34ˇ5
3613 4;5.6 567 678
3614 789 890 901
3615 "});
3616 }
3617
3618 #[gpui::test]
3619 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3620 let mut cx = NeovimBackedTestContext::new(cx).await;
3621
3622 cx.set_shared_state(indoc! {"
3623 fn aˇ() {
3624 return
3625 }
3626 "})
3627 .await;
3628 cx.simulate_shared_keystrokes("v $ %").await;
3629 cx.shared_state().await.assert_eq(indoc! {"
3630 fn a«() {
3631 return
3632 }ˇ»
3633 "});
3634 }
3635
3636 #[gpui::test]
3637 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3638 let mut cx = VimTestContext::new(cx, true).await;
3639
3640 cx.set_state(
3641 indoc! {"
3642 struct Foo {
3643 ˇ
3644 }
3645 "},
3646 Mode::Normal,
3647 );
3648
3649 cx.update_editor(|editor, _window, cx| {
3650 let range = editor.selections.newest_anchor().range();
3651 let inlay_text = " field: int,\n field2: string\n field3: float";
3652 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3653 editor.splice_inlays(&[], vec![inlay], cx);
3654 });
3655
3656 cx.simulate_keystrokes("j");
3657 cx.assert_state(
3658 indoc! {"
3659 struct Foo {
3660
3661 ˇ}
3662 "},
3663 Mode::Normal,
3664 );
3665 }
3666
3667 #[gpui::test]
3668 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3669 let mut cx = VimTestContext::new(cx, true).await;
3670
3671 cx.set_state(
3672 indoc! {"
3673 ˇstruct Foo {
3674
3675 }
3676 "},
3677 Mode::Normal,
3678 );
3679 cx.update_editor(|editor, _window, cx| {
3680 let snapshot = editor.buffer().read(cx).snapshot(cx);
3681 let end_of_line =
3682 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3683 let inlay_text = " hint";
3684 let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3685 editor.splice_inlays(&[], vec![inlay], cx);
3686 });
3687 cx.simulate_keystrokes("$");
3688 cx.assert_state(
3689 indoc! {"
3690 struct Foo ˇ{
3691
3692 }
3693 "},
3694 Mode::Normal,
3695 );
3696 }
3697
3698 #[gpui::test]
3699 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3700 let mut cx = NeovimBackedTestContext::new(cx).await;
3701 // Normal mode
3702 cx.set_shared_state(indoc! {"
3703 The ˇquick brown
3704 fox jumps over
3705 the lazy dog
3706 The quick brown
3707 fox jumps over
3708 the lazy dog
3709 The quick brown
3710 fox jumps over
3711 the lazy dog"})
3712 .await;
3713 cx.simulate_shared_keystrokes("2 0 %").await;
3714 cx.shared_state().await.assert_eq(indoc! {"
3715 The quick brown
3716 fox ˇjumps over
3717 the lazy dog
3718 The quick brown
3719 fox jumps over
3720 the lazy dog
3721 The quick brown
3722 fox jumps over
3723 the lazy dog"});
3724
3725 cx.simulate_shared_keystrokes("2 5 %").await;
3726 cx.shared_state().await.assert_eq(indoc! {"
3727 The quick brown
3728 fox jumps over
3729 the ˇlazy dog
3730 The quick brown
3731 fox jumps over
3732 the lazy dog
3733 The quick brown
3734 fox jumps over
3735 the lazy dog"});
3736
3737 cx.simulate_shared_keystrokes("7 5 %").await;
3738 cx.shared_state().await.assert_eq(indoc! {"
3739 The quick brown
3740 fox jumps over
3741 the lazy dog
3742 The quick brown
3743 fox jumps over
3744 the lazy dog
3745 The ˇquick brown
3746 fox jumps over
3747 the lazy dog"});
3748
3749 // Visual mode
3750 cx.set_shared_state(indoc! {"
3751 The ˇquick brown
3752 fox jumps over
3753 the lazy dog
3754 The quick brown
3755 fox jumps over
3756 the lazy dog
3757 The quick brown
3758 fox jumps over
3759 the lazy dog"})
3760 .await;
3761 cx.simulate_shared_keystrokes("v 5 0 %").await;
3762 cx.shared_state().await.assert_eq(indoc! {"
3763 The «quick brown
3764 fox jumps over
3765 the lazy dog
3766 The quick brown
3767 fox jˇ»umps over
3768 the lazy dog
3769 The quick brown
3770 fox jumps over
3771 the lazy dog"});
3772
3773 cx.set_shared_state(indoc! {"
3774 The ˇquick brown
3775 fox jumps over
3776 the lazy dog
3777 The quick brown
3778 fox jumps over
3779 the lazy dog
3780 The quick brown
3781 fox jumps over
3782 the lazy dog"})
3783 .await;
3784 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3785 cx.shared_state().await.assert_eq(indoc! {"
3786 The «quick brown
3787 fox jumps over
3788 the lazy dog
3789 The quick brown
3790 fox jumps over
3791 the lazy dog
3792 The quick brown
3793 fox jumps over
3794 the lˇ»azy dog"});
3795 }
3796
3797 #[gpui::test]
3798 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3799 let mut cx = NeovimBackedTestContext::new(cx).await;
3800
3801 cx.set_shared_state("ˇπππππ").await;
3802 cx.simulate_shared_keystrokes("3 space").await;
3803 cx.shared_state().await.assert_eq("πππˇππ");
3804 }
3805
3806 #[gpui::test]
3807 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3808 let mut cx = NeovimBackedTestContext::new(cx).await;
3809
3810 cx.set_shared_state(indoc! {"
3811 ππππˇπ
3812 πanotherline"})
3813 .await;
3814 cx.simulate_shared_keystrokes("4 space").await;
3815 cx.shared_state().await.assert_eq(indoc! {"
3816 πππππ
3817 πanˇotherline"});
3818 }
3819
3820 #[gpui::test]
3821 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3822 let mut cx = NeovimBackedTestContext::new(cx).await;
3823
3824 cx.set_shared_state(indoc! {"
3825 ππππ
3826 πanˇotherline"})
3827 .await;
3828 cx.simulate_shared_keystrokes("4 backspace").await;
3829 cx.shared_state().await.assert_eq(indoc! {"
3830 πππˇπ
3831 πanotherline"});
3832 }
3833
3834 #[gpui::test]
3835 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
3836 let mut cx = VimTestContext::new(cx, true).await;
3837 cx.set_state(
3838 indoc! {
3839 "func empty(a string) bool {
3840 ˇif a == \"\" {
3841 return true
3842 }
3843 return false
3844 }"
3845 },
3846 Mode::Normal,
3847 );
3848 cx.simulate_keystrokes("[ -");
3849 cx.assert_state(
3850 indoc! {
3851 "ˇfunc empty(a string) bool {
3852 if a == \"\" {
3853 return true
3854 }
3855 return false
3856 }"
3857 },
3858 Mode::Normal,
3859 );
3860 cx.simulate_keystrokes("] =");
3861 cx.assert_state(
3862 indoc! {
3863 "func empty(a string) bool {
3864 if a == \"\" {
3865 return true
3866 }
3867 return false
3868 ˇ}"
3869 },
3870 Mode::Normal,
3871 );
3872 cx.simulate_keystrokes("[ +");
3873 cx.assert_state(
3874 indoc! {
3875 "func empty(a string) bool {
3876 if a == \"\" {
3877 return true
3878 }
3879 ˇreturn false
3880 }"
3881 },
3882 Mode::Normal,
3883 );
3884 cx.simulate_keystrokes("2 [ =");
3885 cx.assert_state(
3886 indoc! {
3887 "func empty(a string) bool {
3888 ˇif a == \"\" {
3889 return true
3890 }
3891 return false
3892 }"
3893 },
3894 Mode::Normal,
3895 );
3896 cx.simulate_keystrokes("] +");
3897 cx.assert_state(
3898 indoc! {
3899 "func empty(a string) bool {
3900 if a == \"\" {
3901 ˇreturn true
3902 }
3903 return false
3904 }"
3905 },
3906 Mode::Normal,
3907 );
3908 cx.simulate_keystrokes("] -");
3909 cx.assert_state(
3910 indoc! {
3911 "func empty(a string) bool {
3912 if a == \"\" {
3913 return true
3914 ˇ}
3915 return false
3916 }"
3917 },
3918 Mode::Normal,
3919 );
3920 }
3921
3922 #[gpui::test]
3923 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
3924 let mut cx = NeovimBackedTestContext::new(cx).await;
3925 cx.set_shared_state("abˇc").await;
3926 cx.simulate_shared_keystrokes("delete").await;
3927 cx.shared_state().await.assert_eq("aˇb");
3928 }
3929
3930 #[gpui::test]
3931 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
3932 let mut cx = NeovimBackedTestContext::new(cx).await;
3933
3934 cx.set_shared_state(indoc! {"
3935 ˇthe quick brown fox
3936 jumped over the lazy dog"})
3937 .await;
3938 cx.simulate_shared_keystrokes("d v 0").await;
3939 cx.shared_state().await.assert_eq(indoc! {"
3940 ˇhe quick brown fox
3941 jumped over the lazy dog"});
3942 assert_eq!(cx.cx.forced_motion(), false);
3943
3944 cx.set_shared_state(indoc! {"
3945 the quick bˇrown fox
3946 jumped over the lazy dog"})
3947 .await;
3948 cx.simulate_shared_keystrokes("d v 0").await;
3949 cx.shared_state().await.assert_eq(indoc! {"
3950 ˇown fox
3951 jumped over the lazy dog"});
3952 assert_eq!(cx.cx.forced_motion(), false);
3953
3954 cx.set_shared_state(indoc! {"
3955 the quick brown foˇx
3956 jumped over the lazy dog"})
3957 .await;
3958 cx.simulate_shared_keystrokes("d v 0").await;
3959 cx.shared_state().await.assert_eq(indoc! {"
3960 ˇ
3961 jumped over the lazy dog"});
3962 assert_eq!(cx.cx.forced_motion(), false);
3963 }
3964
3965 #[gpui::test]
3966 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
3967 let mut cx = NeovimBackedTestContext::new(cx).await;
3968
3969 cx.set_shared_state(indoc! {"
3970 ˇthe quick brown fox
3971 jumped over the lazy dog"})
3972 .await;
3973 cx.simulate_shared_keystrokes("d v g shift-m").await;
3974 cx.shared_state().await.assert_eq(indoc! {"
3975 ˇbrown fox
3976 jumped over the lazy dog"});
3977 assert_eq!(cx.cx.forced_motion(), false);
3978
3979 cx.set_shared_state(indoc! {"
3980 the quick bˇrown fox
3981 jumped over the lazy dog"})
3982 .await;
3983 cx.simulate_shared_keystrokes("d v g shift-m").await;
3984 cx.shared_state().await.assert_eq(indoc! {"
3985 the quickˇown fox
3986 jumped over the lazy dog"});
3987 assert_eq!(cx.cx.forced_motion(), false);
3988
3989 cx.set_shared_state(indoc! {"
3990 the quick brown foˇx
3991 jumped over the lazy dog"})
3992 .await;
3993 cx.simulate_shared_keystrokes("d v g shift-m").await;
3994 cx.shared_state().await.assert_eq(indoc! {"
3995 the quicˇk
3996 jumped over the lazy dog"});
3997 assert_eq!(cx.cx.forced_motion(), false);
3998
3999 cx.set_shared_state(indoc! {"
4000 ˇthe quick brown fox
4001 jumped over the lazy dog"})
4002 .await;
4003 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
4004 cx.shared_state().await.assert_eq(indoc! {"
4005 ˇ fox
4006 jumped over the lazy dog"});
4007 assert_eq!(cx.cx.forced_motion(), false);
4008
4009 cx.set_shared_state(indoc! {"
4010 ˇthe quick brown fox
4011 jumped over the lazy dog"})
4012 .await;
4013 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
4014 cx.shared_state().await.assert_eq(indoc! {"
4015 ˇuick brown fox
4016 jumped over the lazy dog"});
4017 assert_eq!(cx.cx.forced_motion(), false);
4018 }
4019
4020 #[gpui::test]
4021 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
4022 let mut cx = NeovimBackedTestContext::new(cx).await;
4023
4024 cx.set_shared_state(indoc! {"
4025 the quick brown foˇx
4026 jumped over the lazy dog"})
4027 .await;
4028 cx.simulate_shared_keystrokes("d v $").await;
4029 cx.shared_state().await.assert_eq(indoc! {"
4030 the quick brown foˇx
4031 jumped over the lazy dog"});
4032 assert_eq!(cx.cx.forced_motion(), false);
4033
4034 cx.set_shared_state(indoc! {"
4035 ˇthe quick brown fox
4036 jumped over the lazy dog"})
4037 .await;
4038 cx.simulate_shared_keystrokes("d v $").await;
4039 cx.shared_state().await.assert_eq(indoc! {"
4040 ˇx
4041 jumped over the lazy dog"});
4042 assert_eq!(cx.cx.forced_motion(), false);
4043 }
4044
4045 #[gpui::test]
4046 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
4047 let mut cx = NeovimBackedTestContext::new(cx).await;
4048
4049 cx.set_shared_state(indoc! {"
4050 ˇthe quick brown fox
4051 jumped over the lazy dog"})
4052 .await;
4053 cx.simulate_shared_keystrokes("y v j p").await;
4054 cx.shared_state().await.assert_eq(indoc! {"
4055 the quick brown fox
4056 ˇthe quick brown fox
4057 jumped over the lazy dog"});
4058 assert_eq!(cx.cx.forced_motion(), false);
4059
4060 cx.set_shared_state(indoc! {"
4061 the quick bˇrown fox
4062 jumped over the lazy dog"})
4063 .await;
4064 cx.simulate_shared_keystrokes("y v j p").await;
4065 cx.shared_state().await.assert_eq(indoc! {"
4066 the quick brˇrown fox
4067 jumped overown fox
4068 jumped over the lazy dog"});
4069 assert_eq!(cx.cx.forced_motion(), false);
4070
4071 cx.set_shared_state(indoc! {"
4072 the quick brown foˇx
4073 jumped over the lazy dog"})
4074 .await;
4075 cx.simulate_shared_keystrokes("y v j p").await;
4076 cx.shared_state().await.assert_eq(indoc! {"
4077 the quick brown foxˇx
4078 jumped over the la
4079 jumped over the lazy dog"});
4080 assert_eq!(cx.cx.forced_motion(), false);
4081
4082 cx.set_shared_state(indoc! {"
4083 the quick brown fox
4084 jˇumped over the lazy dog"})
4085 .await;
4086 cx.simulate_shared_keystrokes("y v k p").await;
4087 cx.shared_state().await.assert_eq(indoc! {"
4088 thˇhe quick brown fox
4089 je quick brown fox
4090 jumped over the lazy dog"});
4091 assert_eq!(cx.cx.forced_motion(), false);
4092 }
4093
4094 #[gpui::test]
4095 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
4096 let mut cx = NeovimBackedTestContext::new(cx).await;
4097
4098 cx.set_shared_state(indoc! {"
4099 ˇthe quick brown fox
4100 jumped over the lazy dog"})
4101 .await;
4102 cx.simulate_shared_keystrokes("d v e").await;
4103 cx.shared_state().await.assert_eq(indoc! {"
4104 ˇe quick brown fox
4105 jumped over the lazy dog"});
4106 assert_eq!(cx.cx.forced_motion(), false);
4107
4108 cx.set_shared_state(indoc! {"
4109 the quick bˇrown fox
4110 jumped over the lazy dog"})
4111 .await;
4112 cx.simulate_shared_keystrokes("d v e").await;
4113 cx.shared_state().await.assert_eq(indoc! {"
4114 the quick bˇn fox
4115 jumped over the lazy dog"});
4116 assert_eq!(cx.cx.forced_motion(), false);
4117
4118 cx.set_shared_state(indoc! {"
4119 the quick brown foˇx
4120 jumped over the lazy dog"})
4121 .await;
4122 cx.simulate_shared_keystrokes("d v e").await;
4123 cx.shared_state().await.assert_eq(indoc! {"
4124 the quick brown foˇd over the lazy dog"});
4125 assert_eq!(cx.cx.forced_motion(), false);
4126 }
4127}