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 {
2405 Some(to)
2406 }
2407 } else {
2408 None
2409 }
2410}
2411
2412fn find_backward(
2413 map: &DisplaySnapshot,
2414 from: DisplayPoint,
2415 after: bool,
2416 target: char,
2417 times: usize,
2418 mode: FindRange,
2419 smartcase: bool,
2420) -> DisplayPoint {
2421 let mut to = from;
2422
2423 for _ in 0..times {
2424 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2425 is_character_match(target, right, smartcase)
2426 });
2427 if to == new_to {
2428 break;
2429 }
2430 to = new_to;
2431 }
2432
2433 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2434 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2435 if after {
2436 *to.column_mut() += 1;
2437 map.clip_point(to, Bias::Right)
2438 } else {
2439 to
2440 }
2441 } else {
2442 from
2443 }
2444}
2445
2446fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2447 if smartcase {
2448 if target.is_uppercase() {
2449 target == other
2450 } else {
2451 target == other.to_ascii_lowercase()
2452 }
2453 } else {
2454 target == other
2455 }
2456}
2457
2458fn sneak(
2459 map: &DisplaySnapshot,
2460 from: DisplayPoint,
2461 first_target: char,
2462 second_target: char,
2463 times: usize,
2464 smartcase: bool,
2465) -> Option<DisplayPoint> {
2466 let mut to = from;
2467 let mut found = false;
2468
2469 for _ in 0..times {
2470 found = false;
2471 let new_to = find_boundary(
2472 map,
2473 movement::right(map, to),
2474 FindRange::MultiLine,
2475 |left, right| {
2476 found = is_character_match(first_target, left, smartcase)
2477 && is_character_match(second_target, right, smartcase);
2478 found
2479 },
2480 );
2481 if to == new_to {
2482 break;
2483 }
2484 to = new_to;
2485 }
2486
2487 if found {
2488 Some(movement::left(map, to))
2489 } else {
2490 None
2491 }
2492}
2493
2494fn sneak_backward(
2495 map: &DisplaySnapshot,
2496 from: DisplayPoint,
2497 first_target: char,
2498 second_target: char,
2499 times: usize,
2500 smartcase: bool,
2501) -> Option<DisplayPoint> {
2502 let mut to = from;
2503 let mut found = false;
2504
2505 for _ in 0..times {
2506 found = false;
2507 let new_to =
2508 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2509 found = is_character_match(first_target, left, smartcase)
2510 && is_character_match(second_target, right, smartcase);
2511 found
2512 });
2513 if to == new_to {
2514 break;
2515 }
2516 to = new_to;
2517 }
2518
2519 if found {
2520 Some(movement::left(map, to))
2521 } else {
2522 None
2523 }
2524}
2525
2526fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2527 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2528 first_non_whitespace(map, false, correct_line)
2529}
2530
2531fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2532 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2533 first_non_whitespace(map, false, correct_line)
2534}
2535
2536fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2537 let correct_line = start_of_relative_buffer_row(map, point, 0);
2538 right(map, correct_line, times.saturating_sub(1))
2539}
2540
2541pub(crate) fn next_line_end(
2542 map: &DisplaySnapshot,
2543 mut point: DisplayPoint,
2544 times: usize,
2545) -> DisplayPoint {
2546 if times > 1 {
2547 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2548 }
2549 end_of_line(map, false, point, 1)
2550}
2551
2552fn window_top(
2553 map: &DisplaySnapshot,
2554 point: DisplayPoint,
2555 text_layout_details: &TextLayoutDetails,
2556 mut times: usize,
2557) -> (DisplayPoint, SelectionGoal) {
2558 let first_visible_line = text_layout_details
2559 .scroll_anchor
2560 .anchor
2561 .to_display_point(map);
2562
2563 if first_visible_line.row() != DisplayRow(0)
2564 && text_layout_details.vertical_scroll_margin as usize > times
2565 {
2566 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2567 }
2568
2569 if let Some(visible_rows) = text_layout_details.visible_rows {
2570 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2571 let new_row = (first_visible_line.row().0 + (times as u32))
2572 .min(bottom_row)
2573 .min(map.max_point().row().0);
2574 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2575
2576 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2577 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2578 } else {
2579 let new_row =
2580 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2581 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2582
2583 let new_point = DisplayPoint::new(new_row, new_col);
2584 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2585 }
2586}
2587
2588fn window_middle(
2589 map: &DisplaySnapshot,
2590 point: DisplayPoint,
2591 text_layout_details: &TextLayoutDetails,
2592) -> (DisplayPoint, SelectionGoal) {
2593 if let Some(visible_rows) = text_layout_details.visible_rows {
2594 let first_visible_line = text_layout_details
2595 .scroll_anchor
2596 .anchor
2597 .to_display_point(map);
2598
2599 let max_visible_rows =
2600 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2601
2602 let new_row =
2603 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2604 let new_row = DisplayRow(new_row);
2605 let new_col = point.column().min(map.line_len(new_row));
2606 let new_point = DisplayPoint::new(new_row, new_col);
2607 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2608 } else {
2609 (point, SelectionGoal::None)
2610 }
2611}
2612
2613fn window_bottom(
2614 map: &DisplaySnapshot,
2615 point: DisplayPoint,
2616 text_layout_details: &TextLayoutDetails,
2617 mut times: usize,
2618) -> (DisplayPoint, SelectionGoal) {
2619 if let Some(visible_rows) = text_layout_details.visible_rows {
2620 let first_visible_line = text_layout_details
2621 .scroll_anchor
2622 .anchor
2623 .to_display_point(map);
2624 let bottom_row = first_visible_line.row().0
2625 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2626 if bottom_row < map.max_point().row().0
2627 && text_layout_details.vertical_scroll_margin as usize > times
2628 {
2629 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2630 }
2631 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2632 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2633 {
2634 first_visible_line.row()
2635 } else {
2636 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2637 };
2638 let new_col = point.column().min(map.line_len(new_row));
2639 let new_point = DisplayPoint::new(new_row, new_col);
2640 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2641 } else {
2642 (point, SelectionGoal::None)
2643 }
2644}
2645
2646fn method_motion(
2647 map: &DisplaySnapshot,
2648 mut display_point: DisplayPoint,
2649 times: usize,
2650 direction: Direction,
2651 is_start: bool,
2652) -> DisplayPoint {
2653 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2654 return display_point;
2655 };
2656
2657 for _ in 0..times {
2658 let point = map.display_point_to_point(display_point, Bias::Left);
2659 let offset = point.to_offset(&map.buffer_snapshot);
2660 let range = if direction == Direction::Prev {
2661 0..offset
2662 } else {
2663 offset..buffer.len()
2664 };
2665
2666 let possibilities = buffer
2667 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2668 .filter_map(|(range, object)| {
2669 if !matches!(object, language::TextObject::AroundFunction) {
2670 return None;
2671 }
2672
2673 let relevant = if is_start { range.start } else { range.end };
2674 if direction == Direction::Prev && relevant < offset {
2675 Some(relevant)
2676 } else if direction == Direction::Next && relevant > offset + 1 {
2677 Some(relevant)
2678 } else {
2679 None
2680 }
2681 });
2682
2683 let dest = if direction == Direction::Prev {
2684 possibilities.max().unwrap_or(offset)
2685 } else {
2686 possibilities.min().unwrap_or(offset)
2687 };
2688 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2689 if new_point == display_point {
2690 break;
2691 }
2692 display_point = new_point;
2693 }
2694 display_point
2695}
2696
2697fn comment_motion(
2698 map: &DisplaySnapshot,
2699 mut display_point: DisplayPoint,
2700 times: usize,
2701 direction: Direction,
2702) -> DisplayPoint {
2703 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2704 return display_point;
2705 };
2706
2707 for _ in 0..times {
2708 let point = map.display_point_to_point(display_point, Bias::Left);
2709 let offset = point.to_offset(&map.buffer_snapshot);
2710 let range = if direction == Direction::Prev {
2711 0..offset
2712 } else {
2713 offset..buffer.len()
2714 };
2715
2716 let possibilities = buffer
2717 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2718 .filter_map(|(range, object)| {
2719 if !matches!(object, language::TextObject::AroundComment) {
2720 return None;
2721 }
2722
2723 let relevant = if direction == Direction::Prev {
2724 range.start
2725 } else {
2726 range.end
2727 };
2728 if direction == Direction::Prev && relevant < offset {
2729 Some(relevant)
2730 } else if direction == Direction::Next && relevant > offset + 1 {
2731 Some(relevant)
2732 } else {
2733 None
2734 }
2735 });
2736
2737 let dest = if direction == Direction::Prev {
2738 possibilities.max().unwrap_or(offset)
2739 } else {
2740 possibilities.min().unwrap_or(offset)
2741 };
2742 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2743 if new_point == display_point {
2744 break;
2745 }
2746 display_point = new_point;
2747 }
2748
2749 display_point
2750}
2751
2752fn section_motion(
2753 map: &DisplaySnapshot,
2754 mut display_point: DisplayPoint,
2755 times: usize,
2756 direction: Direction,
2757 is_start: bool,
2758) -> DisplayPoint {
2759 if map.buffer_snapshot.as_singleton().is_some() {
2760 for _ in 0..times {
2761 let offset = map
2762 .display_point_to_point(display_point, Bias::Left)
2763 .to_offset(&map.buffer_snapshot);
2764 let range = if direction == Direction::Prev {
2765 0..offset
2766 } else {
2767 offset..map.buffer_snapshot.len()
2768 };
2769
2770 // we set a max start depth here because we want a section to only be "top level"
2771 // similar to vim's default of '{' in the first column.
2772 // (and without it, ]] at the start of editor.rs is -very- slow)
2773 let mut possibilities = map
2774 .buffer_snapshot
2775 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2776 .filter(|(_, object)| {
2777 matches!(
2778 object,
2779 language::TextObject::AroundClass | language::TextObject::AroundFunction
2780 )
2781 })
2782 .collect::<Vec<_>>();
2783 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2784 let mut prev_end = None;
2785 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2786 if t == language::TextObject::AroundFunction
2787 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2788 {
2789 return None;
2790 }
2791 prev_end = Some(range.end);
2792
2793 let relevant = if is_start { range.start } else { range.end };
2794 if direction == Direction::Prev && relevant < offset {
2795 Some(relevant)
2796 } else if direction == Direction::Next && relevant > offset + 1 {
2797 Some(relevant)
2798 } else {
2799 None
2800 }
2801 });
2802
2803 let offset = if direction == Direction::Prev {
2804 possibilities.max().unwrap_or(0)
2805 } else {
2806 possibilities.min().unwrap_or(map.buffer_snapshot.len())
2807 };
2808
2809 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2810 if new_point == display_point {
2811 break;
2812 }
2813 display_point = new_point;
2814 }
2815 return display_point;
2816 };
2817
2818 for _ in 0..times {
2819 let next_point = if is_start {
2820 movement::start_of_excerpt(map, display_point, direction)
2821 } else {
2822 movement::end_of_excerpt(map, display_point, direction)
2823 };
2824 if next_point == display_point {
2825 break;
2826 }
2827 display_point = next_point;
2828 }
2829
2830 display_point
2831}
2832
2833fn matches_indent_type(
2834 target_indent: &text::LineIndent,
2835 current_indent: &text::LineIndent,
2836 indent_type: IndentType,
2837) -> bool {
2838 match indent_type {
2839 IndentType::Lesser => {
2840 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
2841 }
2842 IndentType::Greater => {
2843 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
2844 }
2845 IndentType::Same => {
2846 target_indent.spaces == current_indent.spaces
2847 && target_indent.tabs == current_indent.tabs
2848 }
2849 }
2850}
2851
2852fn indent_motion(
2853 map: &DisplaySnapshot,
2854 mut display_point: DisplayPoint,
2855 times: usize,
2856 direction: Direction,
2857 indent_type: IndentType,
2858) -> DisplayPoint {
2859 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
2860 let current_row = MultiBufferRow(buffer_point.row);
2861 let current_indent = map.line_indent_for_buffer_row(current_row);
2862 if current_indent.is_line_empty() {
2863 return display_point;
2864 }
2865 let max_row = map.max_point().to_point(map).row;
2866
2867 for _ in 0..times {
2868 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
2869
2870 let target_row = match direction {
2871 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
2872 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
2873 !indent.is_line_empty()
2874 && matches_indent_type(&indent, ¤t_indent, indent_type)
2875 }),
2876 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
2877 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
2878 !indent.is_line_empty()
2879 && matches_indent_type(&indent, ¤t_indent, indent_type)
2880 }),
2881 }
2882 .unwrap_or(current_buffer_row);
2883
2884 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
2885 let new_point = first_non_whitespace(map, false, new_point);
2886 if new_point == display_point {
2887 break;
2888 }
2889 display_point = new_point;
2890 }
2891 display_point
2892}
2893
2894#[cfg(test)]
2895mod test {
2896
2897 use crate::{
2898 state::Mode,
2899 test::{NeovimBackedTestContext, VimTestContext},
2900 };
2901 use editor::display_map::Inlay;
2902 use indoc::indoc;
2903 use language::Point;
2904 use multi_buffer::MultiBufferRow;
2905
2906 #[gpui::test]
2907 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2908 let mut cx = NeovimBackedTestContext::new(cx).await;
2909
2910 let initial_state = indoc! {r"ˇabc
2911 def
2912
2913 paragraph
2914 the second
2915
2916
2917
2918 third and
2919 final"};
2920
2921 // goes down once
2922 cx.set_shared_state(initial_state).await;
2923 cx.simulate_shared_keystrokes("}").await;
2924 cx.shared_state().await.assert_eq(indoc! {r"abc
2925 def
2926 ˇ
2927 paragraph
2928 the second
2929
2930
2931
2932 third and
2933 final"});
2934
2935 // goes up once
2936 cx.simulate_shared_keystrokes("{").await;
2937 cx.shared_state().await.assert_eq(initial_state);
2938
2939 // goes down twice
2940 cx.simulate_shared_keystrokes("2 }").await;
2941 cx.shared_state().await.assert_eq(indoc! {r"abc
2942 def
2943
2944 paragraph
2945 the second
2946 ˇ
2947
2948
2949 third and
2950 final"});
2951
2952 // goes down over multiple blanks
2953 cx.simulate_shared_keystrokes("}").await;
2954 cx.shared_state().await.assert_eq(indoc! {r"abc
2955 def
2956
2957 paragraph
2958 the second
2959
2960
2961
2962 third and
2963 finaˇl"});
2964
2965 // goes up twice
2966 cx.simulate_shared_keystrokes("2 {").await;
2967 cx.shared_state().await.assert_eq(indoc! {r"abc
2968 def
2969 ˇ
2970 paragraph
2971 the second
2972
2973
2974
2975 third and
2976 final"});
2977 }
2978
2979 #[gpui::test]
2980 async fn test_matching(cx: &mut gpui::TestAppContext) {
2981 let mut cx = NeovimBackedTestContext::new(cx).await;
2982
2983 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2984 do(something(with<Types>.and_arrays[0, 2]))
2985 }"})
2986 .await;
2987 cx.simulate_shared_keystrokes("%").await;
2988 cx.shared_state()
2989 .await
2990 .assert_eq(indoc! {r"func (a stringˇ) {
2991 do(something(with<Types>.and_arrays[0, 2]))
2992 }"});
2993
2994 // test it works on the last character of the line
2995 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2996 do(something(with<Types>.and_arrays[0, 2]))
2997 }"})
2998 .await;
2999 cx.simulate_shared_keystrokes("%").await;
3000 cx.shared_state()
3001 .await
3002 .assert_eq(indoc! {r"func (a string) {
3003 do(something(with<Types>.and_arrays[0, 2]))
3004 ˇ}"});
3005
3006 // test it works on immediate nesting
3007 cx.set_shared_state("ˇ{()}").await;
3008 cx.simulate_shared_keystrokes("%").await;
3009 cx.shared_state().await.assert_eq("{()ˇ}");
3010 cx.simulate_shared_keystrokes("%").await;
3011 cx.shared_state().await.assert_eq("ˇ{()}");
3012
3013 // test it works on immediate nesting inside braces
3014 cx.set_shared_state("{\n ˇ{()}\n}").await;
3015 cx.simulate_shared_keystrokes("%").await;
3016 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3017
3018 // test it jumps to the next paren on a line
3019 cx.set_shared_state("func ˇboop() {\n}").await;
3020 cx.simulate_shared_keystrokes("%").await;
3021 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3022 }
3023
3024 #[gpui::test]
3025 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3026 let mut cx = NeovimBackedTestContext::new(cx).await;
3027
3028 // test it works with curly braces
3029 cx.set_shared_state(indoc! {r"func (a string) {
3030 do(something(with<Types>.anˇd_arrays[0, 2]))
3031 }"})
3032 .await;
3033 cx.simulate_shared_keystrokes("] }").await;
3034 cx.shared_state()
3035 .await
3036 .assert_eq(indoc! {r"func (a string) {
3037 do(something(with<Types>.and_arrays[0, 2]))
3038 ˇ}"});
3039
3040 // test it works with brackets
3041 cx.set_shared_state(indoc! {r"func (a string) {
3042 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3043 }"})
3044 .await;
3045 cx.simulate_shared_keystrokes("] )").await;
3046 cx.shared_state()
3047 .await
3048 .assert_eq(indoc! {r"func (a string) {
3049 do(something(with<Types>.and_arrays[0, 2])ˇ)
3050 }"});
3051
3052 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3053 .await;
3054 cx.simulate_shared_keystrokes("] )").await;
3055 cx.shared_state()
3056 .await
3057 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3058
3059 // test it works on immediate nesting
3060 cx.set_shared_state("{ˇ {}{}}").await;
3061 cx.simulate_shared_keystrokes("] }").await;
3062 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3063 cx.set_shared_state("(ˇ ()())").await;
3064 cx.simulate_shared_keystrokes("] )").await;
3065 cx.shared_state().await.assert_eq("( ()()ˇ)");
3066
3067 // test it works on immediate nesting inside braces
3068 cx.set_shared_state("{\n ˇ {()}\n}").await;
3069 cx.simulate_shared_keystrokes("] }").await;
3070 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3071 cx.set_shared_state("(\n ˇ {()}\n)").await;
3072 cx.simulate_shared_keystrokes("] )").await;
3073 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3074 }
3075
3076 #[gpui::test]
3077 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3078 let mut cx = NeovimBackedTestContext::new(cx).await;
3079
3080 // test it works with curly braces
3081 cx.set_shared_state(indoc! {r"func (a string) {
3082 do(something(with<Types>.anˇd_arrays[0, 2]))
3083 }"})
3084 .await;
3085 cx.simulate_shared_keystrokes("[ {").await;
3086 cx.shared_state()
3087 .await
3088 .assert_eq(indoc! {r"func (a string) ˇ{
3089 do(something(with<Types>.and_arrays[0, 2]))
3090 }"});
3091
3092 // test it works with brackets
3093 cx.set_shared_state(indoc! {r"func (a string) {
3094 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3095 }"})
3096 .await;
3097 cx.simulate_shared_keystrokes("[ (").await;
3098 cx.shared_state()
3099 .await
3100 .assert_eq(indoc! {r"func (a string) {
3101 doˇ(something(with<Types>.and_arrays[0, 2]))
3102 }"});
3103
3104 // test it works on immediate nesting
3105 cx.set_shared_state("{{}{} ˇ }").await;
3106 cx.simulate_shared_keystrokes("[ {").await;
3107 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3108 cx.set_shared_state("(()() ˇ )").await;
3109 cx.simulate_shared_keystrokes("[ (").await;
3110 cx.shared_state().await.assert_eq("ˇ(()() )");
3111
3112 // test it works on immediate nesting inside braces
3113 cx.set_shared_state("{\n {()} ˇ\n}").await;
3114 cx.simulate_shared_keystrokes("[ {").await;
3115 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3116 cx.set_shared_state("(\n {()} ˇ\n)").await;
3117 cx.simulate_shared_keystrokes("[ (").await;
3118 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3119 }
3120
3121 #[gpui::test]
3122 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
3123 let mut cx = NeovimBackedTestContext::new_html(cx).await;
3124
3125 cx.neovim.exec("set filetype=html").await;
3126
3127 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
3128 cx.simulate_shared_keystrokes("%").await;
3129 cx.shared_state()
3130 .await
3131 .assert_eq(indoc! {r"<body><ˇ/body>"});
3132 cx.simulate_shared_keystrokes("%").await;
3133
3134 // test jumping backwards
3135 cx.shared_state()
3136 .await
3137 .assert_eq(indoc! {r"<ˇbody></body>"});
3138
3139 // test self-closing tags
3140 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3141 cx.simulate_shared_keystrokes("%").await;
3142 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3143
3144 // test tag with attributes
3145 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3146 </div>
3147 "})
3148 .await;
3149 cx.simulate_shared_keystrokes("%").await;
3150 cx.shared_state()
3151 .await
3152 .assert_eq(indoc! {r"<div class='test' id='main'>
3153 <ˇ/div>
3154 "});
3155
3156 // test multi-line self-closing tag
3157 cx.set_shared_state(indoc! {r#"<a>
3158 <br
3159 test = "test"
3160 /ˇ>
3161 </a>"#})
3162 .await;
3163 cx.simulate_shared_keystrokes("%").await;
3164 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3165 ˇ<br
3166 test = "test"
3167 />
3168 </a>"#});
3169 }
3170
3171 #[gpui::test]
3172 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3173 let mut cx = NeovimBackedTestContext::new(cx).await;
3174
3175 // f and F
3176 cx.set_shared_state("ˇone two three four").await;
3177 cx.simulate_shared_keystrokes("f o").await;
3178 cx.shared_state().await.assert_eq("one twˇo three four");
3179 cx.simulate_shared_keystrokes(",").await;
3180 cx.shared_state().await.assert_eq("ˇone two three four");
3181 cx.simulate_shared_keystrokes("2 ;").await;
3182 cx.shared_state().await.assert_eq("one two three fˇour");
3183 cx.simulate_shared_keystrokes("shift-f e").await;
3184 cx.shared_state().await.assert_eq("one two threˇe four");
3185 cx.simulate_shared_keystrokes("2 ;").await;
3186 cx.shared_state().await.assert_eq("onˇe two three four");
3187 cx.simulate_shared_keystrokes(",").await;
3188 cx.shared_state().await.assert_eq("one two thrˇee four");
3189
3190 // t and T
3191 cx.set_shared_state("ˇone two three four").await;
3192 cx.simulate_shared_keystrokes("t o").await;
3193 cx.shared_state().await.assert_eq("one tˇwo three four");
3194 cx.simulate_shared_keystrokes(",").await;
3195 cx.shared_state().await.assert_eq("oˇne two three four");
3196 cx.simulate_shared_keystrokes("2 ;").await;
3197 cx.shared_state().await.assert_eq("one two three ˇfour");
3198 cx.simulate_shared_keystrokes("shift-t e").await;
3199 cx.shared_state().await.assert_eq("one two threeˇ four");
3200 cx.simulate_shared_keystrokes("3 ;").await;
3201 cx.shared_state().await.assert_eq("oneˇ two three four");
3202 cx.simulate_shared_keystrokes(",").await;
3203 cx.shared_state().await.assert_eq("one two thˇree four");
3204 }
3205
3206 #[gpui::test]
3207 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3208 let mut cx = NeovimBackedTestContext::new(cx).await;
3209 let initial_state = indoc! {r"something(ˇfoo)"};
3210 cx.set_shared_state(initial_state).await;
3211 cx.simulate_shared_keystrokes("}").await;
3212 cx.shared_state().await.assert_eq("something(fooˇ)");
3213 }
3214
3215 #[gpui::test]
3216 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3217 let mut cx = NeovimBackedTestContext::new(cx).await;
3218 cx.set_shared_state("ˇone\n two\nthree").await;
3219 cx.simulate_shared_keystrokes("enter").await;
3220 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3221 }
3222
3223 #[gpui::test]
3224 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3225 let mut cx = NeovimBackedTestContext::new(cx).await;
3226 cx.set_shared_state("ˇ one\n two \nthree").await;
3227 cx.simulate_shared_keystrokes("g _").await;
3228 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3229
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 cx.simulate_shared_keystrokes("2 g _").await;
3234 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3235 }
3236
3237 #[gpui::test]
3238 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3239 let mut cx = NeovimBackedTestContext::new(cx).await;
3240 let initial_state = indoc! {r"abc
3241 def
3242 paragraph
3243 the second
3244 third ˇand
3245 final"};
3246
3247 cx.set_shared_state(initial_state).await;
3248 cx.simulate_shared_keystrokes("shift-h").await;
3249 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3250 def
3251 paragraph
3252 the second
3253 third and
3254 final"});
3255
3256 // clip point
3257 cx.set_shared_state(indoc! {r"
3258 1 2 3
3259 4 5 6
3260 7 8 ˇ9
3261 "})
3262 .await;
3263 cx.simulate_shared_keystrokes("shift-h").await;
3264 cx.shared_state().await.assert_eq(indoc! {"
3265 1 2 ˇ3
3266 4 5 6
3267 7 8 9
3268 "});
3269
3270 cx.set_shared_state(indoc! {r"
3271 1 2 3
3272 4 5 6
3273 ˇ7 8 9
3274 "})
3275 .await;
3276 cx.simulate_shared_keystrokes("shift-h").await;
3277 cx.shared_state().await.assert_eq(indoc! {"
3278 ˇ1 2 3
3279 4 5 6
3280 7 8 9
3281 "});
3282
3283 cx.set_shared_state(indoc! {r"
3284 1 2 3
3285 4 5 ˇ6
3286 7 8 9"})
3287 .await;
3288 cx.simulate_shared_keystrokes("9 shift-h").await;
3289 cx.shared_state().await.assert_eq(indoc! {"
3290 1 2 3
3291 4 5 6
3292 7 8 ˇ9"});
3293 }
3294
3295 #[gpui::test]
3296 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3297 let mut cx = NeovimBackedTestContext::new(cx).await;
3298 let initial_state = indoc! {r"abˇc
3299 def
3300 paragraph
3301 the second
3302 third and
3303 final"};
3304
3305 cx.set_shared_state(initial_state).await;
3306 cx.simulate_shared_keystrokes("shift-m").await;
3307 cx.shared_state().await.assert_eq(indoc! {r"abc
3308 def
3309 paˇragraph
3310 the second
3311 third and
3312 final"});
3313
3314 cx.set_shared_state(indoc! {r"
3315 1 2 3
3316 4 5 6
3317 7 8 ˇ9
3318 "})
3319 .await;
3320 cx.simulate_shared_keystrokes("shift-m").await;
3321 cx.shared_state().await.assert_eq(indoc! {"
3322 1 2 3
3323 4 5 ˇ6
3324 7 8 9
3325 "});
3326 cx.set_shared_state(indoc! {r"
3327 1 2 3
3328 4 5 6
3329 ˇ7 8 9
3330 "})
3331 .await;
3332 cx.simulate_shared_keystrokes("shift-m").await;
3333 cx.shared_state().await.assert_eq(indoc! {"
3334 1 2 3
3335 ˇ4 5 6
3336 7 8 9
3337 "});
3338 cx.set_shared_state(indoc! {r"
3339 ˇ1 2 3
3340 4 5 6
3341 7 8 9
3342 "})
3343 .await;
3344 cx.simulate_shared_keystrokes("shift-m").await;
3345 cx.shared_state().await.assert_eq(indoc! {"
3346 1 2 3
3347 ˇ4 5 6
3348 7 8 9
3349 "});
3350 cx.set_shared_state(indoc! {r"
3351 1 2 3
3352 ˇ4 5 6
3353 7 8 9
3354 "})
3355 .await;
3356 cx.simulate_shared_keystrokes("shift-m").await;
3357 cx.shared_state().await.assert_eq(indoc! {"
3358 1 2 3
3359 ˇ4 5 6
3360 7 8 9
3361 "});
3362 cx.set_shared_state(indoc! {r"
3363 1 2 3
3364 4 5 ˇ6
3365 7 8 9
3366 "})
3367 .await;
3368 cx.simulate_shared_keystrokes("shift-m").await;
3369 cx.shared_state().await.assert_eq(indoc! {"
3370 1 2 3
3371 4 5 ˇ6
3372 7 8 9
3373 "});
3374 }
3375
3376 #[gpui::test]
3377 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3378 let mut cx = NeovimBackedTestContext::new(cx).await;
3379 let initial_state = indoc! {r"abc
3380 deˇf
3381 paragraph
3382 the second
3383 third and
3384 final"};
3385
3386 cx.set_shared_state(initial_state).await;
3387 cx.simulate_shared_keystrokes("shift-l").await;
3388 cx.shared_state().await.assert_eq(indoc! {r"abc
3389 def
3390 paragraph
3391 the second
3392 third and
3393 fiˇnal"});
3394
3395 cx.set_shared_state(indoc! {r"
3396 1 2 3
3397 4 5 ˇ6
3398 7 8 9
3399 "})
3400 .await;
3401 cx.simulate_shared_keystrokes("shift-l").await;
3402 cx.shared_state().await.assert_eq(indoc! {"
3403 1 2 3
3404 4 5 6
3405 7 8 9
3406 ˇ"});
3407
3408 cx.set_shared_state(indoc! {r"
3409 1 2 3
3410 ˇ4 5 6
3411 7 8 9
3412 "})
3413 .await;
3414 cx.simulate_shared_keystrokes("shift-l").await;
3415 cx.shared_state().await.assert_eq(indoc! {"
3416 1 2 3
3417 4 5 6
3418 7 8 9
3419 ˇ"});
3420
3421 cx.set_shared_state(indoc! {r"
3422 1 2 ˇ3
3423 4 5 6
3424 7 8 9
3425 "})
3426 .await;
3427 cx.simulate_shared_keystrokes("shift-l").await;
3428 cx.shared_state().await.assert_eq(indoc! {"
3429 1 2 3
3430 4 5 6
3431 7 8 9
3432 ˇ"});
3433
3434 cx.set_shared_state(indoc! {r"
3435 ˇ1 2 3
3436 4 5 6
3437 7 8 9
3438 "})
3439 .await;
3440 cx.simulate_shared_keystrokes("shift-l").await;
3441 cx.shared_state().await.assert_eq(indoc! {"
3442 1 2 3
3443 4 5 6
3444 7 8 9
3445 ˇ"});
3446
3447 cx.set_shared_state(indoc! {r"
3448 1 2 3
3449 4 5 ˇ6
3450 7 8 9
3451 "})
3452 .await;
3453 cx.simulate_shared_keystrokes("9 shift-l").await;
3454 cx.shared_state().await.assert_eq(indoc! {"
3455 1 2 ˇ3
3456 4 5 6
3457 7 8 9
3458 "});
3459 }
3460
3461 #[gpui::test]
3462 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3463 let mut cx = NeovimBackedTestContext::new(cx).await;
3464 cx.set_shared_state(indoc! {r"
3465 456 5ˇ67 678
3466 "})
3467 .await;
3468 cx.simulate_shared_keystrokes("g e").await;
3469 cx.shared_state().await.assert_eq(indoc! {"
3470 45ˇ6 567 678
3471 "});
3472
3473 // Test times
3474 cx.set_shared_state(indoc! {r"
3475 123 234 345
3476 456 5ˇ67 678
3477 "})
3478 .await;
3479 cx.simulate_shared_keystrokes("4 g e").await;
3480 cx.shared_state().await.assert_eq(indoc! {"
3481 12ˇ3 234 345
3482 456 567 678
3483 "});
3484
3485 // With punctuation
3486 cx.set_shared_state(indoc! {r"
3487 123 234 345
3488 4;5.6 5ˇ67 678
3489 789 890 901
3490 "})
3491 .await;
3492 cx.simulate_shared_keystrokes("g e").await;
3493 cx.shared_state().await.assert_eq(indoc! {"
3494 123 234 345
3495 4;5.ˇ6 567 678
3496 789 890 901
3497 "});
3498
3499 // With punctuation and count
3500 cx.set_shared_state(indoc! {r"
3501 123 234 345
3502 4;5.6 5ˇ67 678
3503 789 890 901
3504 "})
3505 .await;
3506 cx.simulate_shared_keystrokes("5 g e").await;
3507 cx.shared_state().await.assert_eq(indoc! {"
3508 123 234 345
3509 ˇ4;5.6 567 678
3510 789 890 901
3511 "});
3512
3513 // newlines
3514 cx.set_shared_state(indoc! {r"
3515 123 234 345
3516
3517 78ˇ9 890 901
3518 "})
3519 .await;
3520 cx.simulate_shared_keystrokes("g e").await;
3521 cx.shared_state().await.assert_eq(indoc! {"
3522 123 234 345
3523 ˇ
3524 789 890 901
3525 "});
3526 cx.simulate_shared_keystrokes("g e").await;
3527 cx.shared_state().await.assert_eq(indoc! {"
3528 123 234 34ˇ5
3529
3530 789 890 901
3531 "});
3532
3533 // With punctuation
3534 cx.set_shared_state(indoc! {r"
3535 123 234 345
3536 4;5.ˇ6 567 678
3537 789 890 901
3538 "})
3539 .await;
3540 cx.simulate_shared_keystrokes("g shift-e").await;
3541 cx.shared_state().await.assert_eq(indoc! {"
3542 123 234 34ˇ5
3543 4;5.6 567 678
3544 789 890 901
3545 "});
3546 }
3547
3548 #[gpui::test]
3549 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3550 let mut cx = NeovimBackedTestContext::new(cx).await;
3551
3552 cx.set_shared_state(indoc! {"
3553 fn aˇ() {
3554 return
3555 }
3556 "})
3557 .await;
3558 cx.simulate_shared_keystrokes("v $ %").await;
3559 cx.shared_state().await.assert_eq(indoc! {"
3560 fn a«() {
3561 return
3562 }ˇ»
3563 "});
3564 }
3565
3566 #[gpui::test]
3567 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3568 let mut cx = VimTestContext::new(cx, true).await;
3569
3570 cx.set_state(
3571 indoc! {"
3572 struct Foo {
3573 ˇ
3574 }
3575 "},
3576 Mode::Normal,
3577 );
3578
3579 cx.update_editor(|editor, _window, cx| {
3580 let range = editor.selections.newest_anchor().range();
3581 let inlay_text = " field: int,\n field2: string\n field3: float";
3582 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3583 editor.splice_inlays(&[], vec![inlay], cx);
3584 });
3585
3586 cx.simulate_keystrokes("j");
3587 cx.assert_state(
3588 indoc! {"
3589 struct Foo {
3590
3591 ˇ}
3592 "},
3593 Mode::Normal,
3594 );
3595 }
3596
3597 #[gpui::test]
3598 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3599 let mut cx = VimTestContext::new(cx, true).await;
3600
3601 cx.set_state(
3602 indoc! {"
3603 ˇstruct Foo {
3604
3605 }
3606 "},
3607 Mode::Normal,
3608 );
3609 cx.update_editor(|editor, _window, cx| {
3610 let snapshot = editor.buffer().read(cx).snapshot(cx);
3611 let end_of_line =
3612 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3613 let inlay_text = " hint";
3614 let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3615 editor.splice_inlays(&[], vec![inlay], cx);
3616 });
3617 cx.simulate_keystrokes("$");
3618 cx.assert_state(
3619 indoc! {"
3620 struct Foo ˇ{
3621
3622 }
3623 "},
3624 Mode::Normal,
3625 );
3626 }
3627
3628 #[gpui::test]
3629 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3630 let mut cx = NeovimBackedTestContext::new(cx).await;
3631 // Normal mode
3632 cx.set_shared_state(indoc! {"
3633 The ˇquick brown
3634 fox jumps over
3635 the lazy dog
3636 The quick brown
3637 fox jumps over
3638 the lazy dog
3639 The quick brown
3640 fox jumps over
3641 the lazy dog"})
3642 .await;
3643 cx.simulate_shared_keystrokes("2 0 %").await;
3644 cx.shared_state().await.assert_eq(indoc! {"
3645 The quick brown
3646 fox ˇjumps over
3647 the lazy dog
3648 The quick brown
3649 fox jumps over
3650 the lazy dog
3651 The quick brown
3652 fox jumps over
3653 the lazy dog"});
3654
3655 cx.simulate_shared_keystrokes("2 5 %").await;
3656 cx.shared_state().await.assert_eq(indoc! {"
3657 The quick brown
3658 fox jumps over
3659 the ˇlazy dog
3660 The quick brown
3661 fox jumps over
3662 the lazy dog
3663 The quick brown
3664 fox jumps over
3665 the lazy dog"});
3666
3667 cx.simulate_shared_keystrokes("7 5 %").await;
3668 cx.shared_state().await.assert_eq(indoc! {"
3669 The quick brown
3670 fox jumps over
3671 the lazy dog
3672 The quick brown
3673 fox jumps over
3674 the lazy dog
3675 The ˇquick brown
3676 fox jumps over
3677 the lazy dog"});
3678
3679 // Visual mode
3680 cx.set_shared_state(indoc! {"
3681 The ˇquick brown
3682 fox jumps over
3683 the lazy dog
3684 The quick brown
3685 fox jumps over
3686 the lazy dog
3687 The quick brown
3688 fox jumps over
3689 the lazy dog"})
3690 .await;
3691 cx.simulate_shared_keystrokes("v 5 0 %").await;
3692 cx.shared_state().await.assert_eq(indoc! {"
3693 The «quick brown
3694 fox jumps over
3695 the lazy dog
3696 The quick brown
3697 fox jˇ»umps over
3698 the lazy dog
3699 The quick brown
3700 fox jumps over
3701 the lazy dog"});
3702
3703 cx.set_shared_state(indoc! {"
3704 The ˇquick brown
3705 fox jumps over
3706 the lazy dog
3707 The quick brown
3708 fox jumps over
3709 the lazy dog
3710 The quick brown
3711 fox jumps over
3712 the lazy dog"})
3713 .await;
3714 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3715 cx.shared_state().await.assert_eq(indoc! {"
3716 The «quick brown
3717 fox jumps over
3718 the lazy dog
3719 The quick brown
3720 fox jumps over
3721 the lazy dog
3722 The quick brown
3723 fox jumps over
3724 the lˇ»azy dog"});
3725 }
3726
3727 #[gpui::test]
3728 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
3729 let mut cx = NeovimBackedTestContext::new(cx).await;
3730
3731 cx.set_shared_state("ˇπππππ").await;
3732 cx.simulate_shared_keystrokes("3 space").await;
3733 cx.shared_state().await.assert_eq("πππˇππ");
3734 }
3735
3736 #[gpui::test]
3737 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
3738 let mut cx = NeovimBackedTestContext::new(cx).await;
3739
3740 cx.set_shared_state(indoc! {"
3741 ππππˇπ
3742 πanotherline"})
3743 .await;
3744 cx.simulate_shared_keystrokes("4 space").await;
3745 cx.shared_state().await.assert_eq(indoc! {"
3746 πππππ
3747 πanˇotherline"});
3748 }
3749
3750 #[gpui::test]
3751 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
3752 let mut cx = NeovimBackedTestContext::new(cx).await;
3753
3754 cx.set_shared_state(indoc! {"
3755 ππππ
3756 πanˇotherline"})
3757 .await;
3758 cx.simulate_shared_keystrokes("4 backspace").await;
3759 cx.shared_state().await.assert_eq(indoc! {"
3760 πππˇπ
3761 πanotherline"});
3762 }
3763
3764 #[gpui::test]
3765 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
3766 let mut cx = VimTestContext::new(cx, true).await;
3767 cx.set_state(
3768 indoc! {
3769 "func empty(a string) bool {
3770 ˇif a == \"\" {
3771 return true
3772 }
3773 return false
3774 }"
3775 },
3776 Mode::Normal,
3777 );
3778 cx.simulate_keystrokes("[ -");
3779 cx.assert_state(
3780 indoc! {
3781 "ˇfunc empty(a string) bool {
3782 if a == \"\" {
3783 return true
3784 }
3785 return false
3786 }"
3787 },
3788 Mode::Normal,
3789 );
3790 cx.simulate_keystrokes("] =");
3791 cx.assert_state(
3792 indoc! {
3793 "func empty(a string) bool {
3794 if a == \"\" {
3795 return true
3796 }
3797 return false
3798 ˇ}"
3799 },
3800 Mode::Normal,
3801 );
3802 cx.simulate_keystrokes("[ +");
3803 cx.assert_state(
3804 indoc! {
3805 "func empty(a string) bool {
3806 if a == \"\" {
3807 return true
3808 }
3809 ˇreturn false
3810 }"
3811 },
3812 Mode::Normal,
3813 );
3814 cx.simulate_keystrokes("2 [ =");
3815 cx.assert_state(
3816 indoc! {
3817 "func empty(a string) bool {
3818 ˇif a == \"\" {
3819 return true
3820 }
3821 return false
3822 }"
3823 },
3824 Mode::Normal,
3825 );
3826 cx.simulate_keystrokes("] +");
3827 cx.assert_state(
3828 indoc! {
3829 "func empty(a string) bool {
3830 if a == \"\" {
3831 ˇreturn true
3832 }
3833 return false
3834 }"
3835 },
3836 Mode::Normal,
3837 );
3838 cx.simulate_keystrokes("] -");
3839 cx.assert_state(
3840 indoc! {
3841 "func empty(a string) bool {
3842 if a == \"\" {
3843 return true
3844 ˇ}
3845 return false
3846 }"
3847 },
3848 Mode::Normal,
3849 );
3850 }
3851
3852 #[gpui::test]
3853 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
3854 let mut cx = NeovimBackedTestContext::new(cx).await;
3855 cx.set_shared_state("abˇc").await;
3856 cx.simulate_shared_keystrokes("delete").await;
3857 cx.shared_state().await.assert_eq("aˇb");
3858 }
3859
3860 #[gpui::test]
3861 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
3862 let mut cx = NeovimBackedTestContext::new(cx).await;
3863
3864 cx.set_shared_state(indoc! {"
3865 ˇthe quick brown fox
3866 jumped over the lazy dog"})
3867 .await;
3868 cx.simulate_shared_keystrokes("d v 0").await;
3869 cx.shared_state().await.assert_eq(indoc! {"
3870 ˇhe quick brown fox
3871 jumped over the lazy dog"});
3872 assert_eq!(cx.cx.forced_motion(), false);
3873
3874 cx.set_shared_state(indoc! {"
3875 the quick bˇrown fox
3876 jumped over the lazy dog"})
3877 .await;
3878 cx.simulate_shared_keystrokes("d v 0").await;
3879 cx.shared_state().await.assert_eq(indoc! {"
3880 ˇown fox
3881 jumped over the lazy dog"});
3882 assert_eq!(cx.cx.forced_motion(), false);
3883
3884 cx.set_shared_state(indoc! {"
3885 the quick brown foˇx
3886 jumped over the lazy dog"})
3887 .await;
3888 cx.simulate_shared_keystrokes("d v 0").await;
3889 cx.shared_state().await.assert_eq(indoc! {"
3890 ˇ
3891 jumped over the lazy dog"});
3892 assert_eq!(cx.cx.forced_motion(), false);
3893 }
3894
3895 #[gpui::test]
3896 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
3897 let mut cx = NeovimBackedTestContext::new(cx).await;
3898
3899 cx.set_shared_state(indoc! {"
3900 the quick brown foˇx
3901 jumped over the lazy dog"})
3902 .await;
3903 cx.simulate_shared_keystrokes("d v $").await;
3904 cx.shared_state().await.assert_eq(indoc! {"
3905 the quick brown foˇx
3906 jumped over the lazy dog"});
3907 assert_eq!(cx.cx.forced_motion(), false);
3908
3909 cx.set_shared_state(indoc! {"
3910 ˇthe quick brown fox
3911 jumped over the lazy dog"})
3912 .await;
3913 cx.simulate_shared_keystrokes("d v $").await;
3914 cx.shared_state().await.assert_eq(indoc! {"
3915 ˇx
3916 jumped over the lazy dog"});
3917 assert_eq!(cx.cx.forced_motion(), false);
3918 }
3919
3920 #[gpui::test]
3921 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
3922 let mut cx = NeovimBackedTestContext::new(cx).await;
3923
3924 cx.set_shared_state(indoc! {"
3925 ˇthe quick brown fox
3926 jumped over the lazy dog"})
3927 .await;
3928 cx.simulate_shared_keystrokes("y v j p").await;
3929 cx.shared_state().await.assert_eq(indoc! {"
3930 the quick brown fox
3931 ˇthe quick brown fox
3932 jumped over the lazy dog"});
3933 assert_eq!(cx.cx.forced_motion(), false);
3934
3935 cx.set_shared_state(indoc! {"
3936 the quick bˇrown fox
3937 jumped over the lazy dog"})
3938 .await;
3939 cx.simulate_shared_keystrokes("y v j p").await;
3940 cx.shared_state().await.assert_eq(indoc! {"
3941 the quick brˇrown fox
3942 jumped overown fox
3943 jumped over the lazy dog"});
3944 assert_eq!(cx.cx.forced_motion(), false);
3945
3946 cx.set_shared_state(indoc! {"
3947 the quick brown foˇx
3948 jumped over the lazy dog"})
3949 .await;
3950 cx.simulate_shared_keystrokes("y v j p").await;
3951 cx.shared_state().await.assert_eq(indoc! {"
3952 the quick brown foxˇx
3953 jumped over the la
3954 jumped over the lazy dog"});
3955 assert_eq!(cx.cx.forced_motion(), false);
3956
3957 cx.set_shared_state(indoc! {"
3958 the quick brown fox
3959 jˇumped over the lazy dog"})
3960 .await;
3961 cx.simulate_shared_keystrokes("y v k p").await;
3962 cx.shared_state().await.assert_eq(indoc! {"
3963 thˇhe quick brown fox
3964 je quick brown fox
3965 jumped over the lazy dog"});
3966 assert_eq!(cx.cx.forced_motion(), false);
3967 }
3968
3969 #[gpui::test]
3970 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
3971 let mut cx = NeovimBackedTestContext::new(cx).await;
3972
3973 cx.set_shared_state(indoc! {"
3974 ˇthe quick brown fox
3975 jumped over the lazy dog"})
3976 .await;
3977 cx.simulate_shared_keystrokes("d v e").await;
3978 cx.shared_state().await.assert_eq(indoc! {"
3979 ˇe quick brown fox
3980 jumped over the lazy dog"});
3981 assert_eq!(cx.cx.forced_motion(), false);
3982
3983 cx.set_shared_state(indoc! {"
3984 the quick bˇrown fox
3985 jumped over the lazy dog"})
3986 .await;
3987 cx.simulate_shared_keystrokes("d v e").await;
3988 cx.shared_state().await.assert_eq(indoc! {"
3989 the quick bˇn fox
3990 jumped over the lazy dog"});
3991 assert_eq!(cx.cx.forced_motion(), false);
3992
3993 cx.set_shared_state(indoc! {"
3994 the quick brown foˇx
3995 jumped over the lazy dog"})
3996 .await;
3997 cx.simulate_shared_keystrokes("d v e").await;
3998 cx.shared_state().await.assert_eq(indoc! {"
3999 the quick brown foˇd over the lazy dog"});
4000 assert_eq!(cx.cx.forced_motion(), false);
4001 }
4002}