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