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