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