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