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