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