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 if point == new_point {
1679 break;
1680 }
1681 point = new_point;
1682 }
1683 point
1684}
1685
1686fn previous_subword_start(
1687 map: &DisplaySnapshot,
1688 mut point: DisplayPoint,
1689 ignore_punctuation: bool,
1690 times: usize,
1691) -> DisplayPoint {
1692 let classifier = map
1693 .buffer_snapshot
1694 .char_classifier_at(point.to_point(map))
1695 .ignore_punctuation(ignore_punctuation);
1696 for _ in 0..times {
1697 let mut crossed_newline = false;
1698 // This works even though find_preceding_boundary is called for every character in the line containing
1699 // cursor because the newline is checked only once.
1700 let new_point = movement::find_preceding_boundary_display_point(
1701 map,
1702 point,
1703 FindRange::MultiLine,
1704 |left, right| {
1705 let left_kind = classifier.kind(left);
1706 let right_kind = classifier.kind(right);
1707 let at_newline = right == '\n';
1708
1709 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1710 let is_subword_start =
1711 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1712
1713 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1714 || at_newline && crossed_newline
1715 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1716
1717 crossed_newline |= at_newline;
1718
1719 found
1720 },
1721 );
1722 if point == new_point {
1723 break;
1724 }
1725 point = new_point;
1726 }
1727 point
1728}
1729
1730fn previous_subword_end(
1731 map: &DisplaySnapshot,
1732 point: DisplayPoint,
1733 ignore_punctuation: bool,
1734 times: usize,
1735) -> DisplayPoint {
1736 let classifier = map
1737 .buffer_snapshot
1738 .char_classifier_at(point.to_point(map))
1739 .ignore_punctuation(ignore_punctuation);
1740 let mut point = point.to_point(map);
1741
1742 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1743 point.column += 1;
1744 }
1745 for _ in 0..times {
1746 let new_point = movement::find_preceding_boundary_point(
1747 &map.buffer_snapshot,
1748 point,
1749 FindRange::MultiLine,
1750 |left, right| {
1751 let left_kind = classifier.kind(left);
1752 let right_kind = classifier.kind(right);
1753
1754 let is_subword_end =
1755 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1756
1757 if is_subword_end {
1758 return true;
1759 }
1760
1761 match (left_kind, right_kind) {
1762 (CharKind::Word, CharKind::Whitespace)
1763 | (CharKind::Word, CharKind::Punctuation) => true,
1764 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1765 _ => false,
1766 }
1767 },
1768 );
1769 if new_point == point {
1770 break;
1771 }
1772 point = new_point;
1773 }
1774 movement::saturating_left(map, point.to_display_point(map))
1775}
1776
1777pub(crate) fn first_non_whitespace(
1778 map: &DisplaySnapshot,
1779 display_lines: bool,
1780 from: DisplayPoint,
1781) -> DisplayPoint {
1782 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1783 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1784 for (ch, offset) in map.buffer_chars_at(start_offset) {
1785 if ch == '\n' {
1786 return from;
1787 }
1788
1789 start_offset = offset;
1790
1791 if classifier.kind(ch) != CharKind::Whitespace {
1792 break;
1793 }
1794 }
1795
1796 start_offset.to_display_point(map)
1797}
1798
1799pub(crate) fn last_non_whitespace(
1800 map: &DisplaySnapshot,
1801 from: DisplayPoint,
1802 count: usize,
1803) -> DisplayPoint {
1804 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1805 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1806
1807 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1808 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1809 if classifier.kind(ch) != CharKind::Whitespace {
1810 return end_of_line.to_display_point(map);
1811 }
1812 }
1813
1814 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1815 if ch == '\n' {
1816 break;
1817 }
1818 end_of_line = offset;
1819 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1820 break;
1821 }
1822 }
1823
1824 end_of_line.to_display_point(map)
1825}
1826
1827pub(crate) fn start_of_line(
1828 map: &DisplaySnapshot,
1829 display_lines: bool,
1830 point: DisplayPoint,
1831) -> DisplayPoint {
1832 if display_lines {
1833 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1834 } else {
1835 map.prev_line_boundary(point.to_point(map)).1
1836 }
1837}
1838
1839pub(crate) fn end_of_line(
1840 map: &DisplaySnapshot,
1841 display_lines: bool,
1842 mut point: DisplayPoint,
1843 times: usize,
1844) -> DisplayPoint {
1845 if times > 1 {
1846 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1847 }
1848 if display_lines {
1849 map.clip_point(
1850 DisplayPoint::new(point.row(), map.line_len(point.row())),
1851 Bias::Left,
1852 )
1853 } else {
1854 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1855 }
1856}
1857
1858fn sentence_backwards(
1859 map: &DisplaySnapshot,
1860 point: DisplayPoint,
1861 mut times: usize,
1862) -> DisplayPoint {
1863 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1864 let mut chars = map.reverse_buffer_chars_at(start).peekable();
1865
1866 let mut was_newline = map
1867 .buffer_chars_at(start)
1868 .next()
1869 .is_some_and(|(c, _)| c == '\n');
1870
1871 while let Some((ch, offset)) = chars.next() {
1872 let start_of_next_sentence = if was_newline && ch == '\n' {
1873 Some(offset + ch.len_utf8())
1874 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1875 Some(next_non_blank(map, offset + ch.len_utf8()))
1876 } else if ch == '.' || ch == '?' || ch == '!' {
1877 start_of_next_sentence(map, offset + ch.len_utf8())
1878 } else {
1879 None
1880 };
1881
1882 if let Some(start_of_next_sentence) = start_of_next_sentence {
1883 if start_of_next_sentence < start {
1884 times = times.saturating_sub(1);
1885 }
1886 if times == 0 || offset == 0 {
1887 return map.clip_point(
1888 start_of_next_sentence
1889 .to_offset(&map.buffer_snapshot)
1890 .to_display_point(map),
1891 Bias::Left,
1892 );
1893 }
1894 }
1895 if was_newline {
1896 start = offset;
1897 }
1898 was_newline = ch == '\n';
1899 }
1900
1901 DisplayPoint::zero()
1902}
1903
1904fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1905 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1906 let mut chars = map.buffer_chars_at(start).peekable();
1907
1908 let mut was_newline = map
1909 .reverse_buffer_chars_at(start)
1910 .next()
1911 .is_some_and(|(c, _)| c == '\n')
1912 && chars.peek().is_some_and(|(c, _)| *c == '\n');
1913
1914 while let Some((ch, offset)) = chars.next() {
1915 if was_newline && ch == '\n' {
1916 continue;
1917 }
1918 let start_of_next_sentence = if was_newline {
1919 Some(next_non_blank(map, offset))
1920 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1921 Some(next_non_blank(map, offset + ch.len_utf8()))
1922 } else if ch == '.' || ch == '?' || ch == '!' {
1923 start_of_next_sentence(map, offset + ch.len_utf8())
1924 } else {
1925 None
1926 };
1927
1928 if let Some(start_of_next_sentence) = start_of_next_sentence {
1929 times = times.saturating_sub(1);
1930 if times == 0 {
1931 return map.clip_point(
1932 start_of_next_sentence
1933 .to_offset(&map.buffer_snapshot)
1934 .to_display_point(map),
1935 Bias::Right,
1936 );
1937 }
1938 }
1939
1940 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1941 }
1942
1943 map.max_point()
1944}
1945
1946fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1947 for (c, o) in map.buffer_chars_at(start) {
1948 if c == '\n' || !c.is_whitespace() {
1949 return o;
1950 }
1951 }
1952
1953 map.buffer_snapshot.len()
1954}
1955
1956// given the offset after a ., !, or ? find the start of the next sentence.
1957// if this is not a sentence boundary, returns None.
1958fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1959 let chars = map.buffer_chars_at(end_of_sentence);
1960 let mut seen_space = false;
1961
1962 for (char, offset) in chars {
1963 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1964 continue;
1965 }
1966
1967 if char == '\n' && seen_space {
1968 return Some(offset);
1969 } else if char.is_whitespace() {
1970 seen_space = true;
1971 } else if seen_space {
1972 return Some(offset);
1973 } else {
1974 return None;
1975 }
1976 }
1977
1978 Some(map.buffer_snapshot.len())
1979}
1980
1981fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
1982 let point = map.display_point_to_point(display_point, Bias::Left);
1983 let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
1984 return display_point;
1985 };
1986 let offset = excerpt.buffer().point_to_offset(
1987 excerpt
1988 .buffer()
1989 .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
1990 );
1991 let buffer_range = excerpt.buffer_range();
1992 if offset >= buffer_range.start && offset <= buffer_range.end {
1993 let point = map
1994 .buffer_snapshot
1995 .offset_to_point(excerpt.map_offset_from_buffer(offset));
1996 return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
1997 }
1998 let mut last_position = None;
1999 for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2000 let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
2001 ..language::ToOffset::to_offset(&range.context.end, &buffer);
2002 if offset >= excerpt_range.start && offset <= excerpt_range.end {
2003 let text_anchor = buffer.anchor_after(offset);
2004 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2005 return anchor.to_display_point(map);
2006 } else if offset <= excerpt_range.start {
2007 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2008 return anchor.to_display_point(map);
2009 } else {
2010 last_position = Some(Anchor::in_buffer(
2011 excerpt,
2012 buffer.remote_id(),
2013 range.context.end,
2014 ));
2015 }
2016 }
2017
2018 let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2019 last_point.column = point.column;
2020
2021 map.clip_point(
2022 map.point_to_display_point(
2023 map.buffer_snapshot.clip_point(point, Bias::Left),
2024 Bias::Left,
2025 ),
2026 Bias::Left,
2027 )
2028}
2029
2030fn start_of_document(
2031 map: &DisplaySnapshot,
2032 display_point: DisplayPoint,
2033 maybe_times: Option<usize>,
2034) -> DisplayPoint {
2035 if let Some(times) = maybe_times {
2036 return go_to_line(map, display_point, times);
2037 }
2038
2039 let point = map.display_point_to_point(display_point, Bias::Left);
2040 let mut first_point = Point::zero();
2041 first_point.column = point.column;
2042
2043 map.clip_point(
2044 map.point_to_display_point(
2045 map.buffer_snapshot.clip_point(first_point, Bias::Left),
2046 Bias::Left,
2047 ),
2048 Bias::Left,
2049 )
2050}
2051
2052fn end_of_document(
2053 map: &DisplaySnapshot,
2054 display_point: DisplayPoint,
2055 maybe_times: Option<usize>,
2056) -> DisplayPoint {
2057 if let Some(times) = maybe_times {
2058 return go_to_line(map, display_point, times);
2059 };
2060 let point = map.display_point_to_point(display_point, Bias::Left);
2061 let mut last_point = map.buffer_snapshot.max_point();
2062 last_point.column = point.column;
2063
2064 map.clip_point(
2065 map.point_to_display_point(
2066 map.buffer_snapshot.clip_point(last_point, Bias::Left),
2067 Bias::Left,
2068 ),
2069 Bias::Left,
2070 )
2071}
2072
2073fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2074 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2075 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2076
2077 if head > outer.start && head < inner.start {
2078 let mut offset = inner.end.to_offset(map, Bias::Left);
2079 for c in map.buffer_snapshot.chars_at(offset) {
2080 if c == '/' || c == '\n' || c == '>' {
2081 return Some(offset.to_display_point(map));
2082 }
2083 offset += c.len_utf8();
2084 }
2085 } else {
2086 let mut offset = outer.start.to_offset(map, Bias::Left);
2087 for c in map.buffer_snapshot.chars_at(offset) {
2088 offset += c.len_utf8();
2089 if c == '<' || c == '\n' {
2090 return Some(offset.to_display_point(map));
2091 }
2092 }
2093 }
2094
2095 return None;
2096}
2097
2098fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2099 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2100 let display_point = map.clip_at_line_end(display_point);
2101 let point = display_point.to_point(map);
2102 let offset = point.to_offset(&map.buffer_snapshot);
2103
2104 // Ensure the range is contained by the current line.
2105 let mut line_end = map.next_line_boundary(point).0;
2106 if line_end == point {
2107 line_end = map.max_point().to_point(map);
2108 }
2109
2110 let line_range = map.prev_line_boundary(point).0..line_end;
2111 let visible_line_range =
2112 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2113 let ranges = map
2114 .buffer_snapshot
2115 .bracket_ranges(visible_line_range.clone());
2116 if let Some(ranges) = ranges {
2117 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2118 ..line_range.end.to_offset(&map.buffer_snapshot);
2119 let mut closest_pair_destination = None;
2120 let mut closest_distance = usize::MAX;
2121
2122 for (open_range, close_range) in ranges {
2123 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2124 if offset > open_range.start && offset < close_range.start {
2125 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2126 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2127 return display_point;
2128 }
2129 if let Some(tag) = matching_tag(map, display_point) {
2130 return tag;
2131 }
2132 } else if close_range.contains(&offset) {
2133 return open_range.start.to_display_point(map);
2134 } else if open_range.contains(&offset) {
2135 return (close_range.end - 1).to_display_point(map);
2136 }
2137 }
2138
2139 if (open_range.contains(&offset) || open_range.start >= offset)
2140 && line_range.contains(&open_range.start)
2141 {
2142 let distance = open_range.start.saturating_sub(offset);
2143 if distance < closest_distance {
2144 closest_pair_destination = Some(close_range.start);
2145 closest_distance = distance;
2146 continue;
2147 }
2148 }
2149
2150 if (close_range.contains(&offset) || close_range.start >= offset)
2151 && line_range.contains(&close_range.start)
2152 {
2153 let distance = close_range.start.saturating_sub(offset);
2154 if distance < closest_distance {
2155 closest_pair_destination = Some(open_range.start);
2156 closest_distance = distance;
2157 continue;
2158 }
2159 }
2160
2161 continue;
2162 }
2163
2164 closest_pair_destination
2165 .map(|destination| destination.to_display_point(map))
2166 .unwrap_or(display_point)
2167 } else {
2168 display_point
2169 }
2170}
2171
2172fn unmatched_forward(
2173 map: &DisplaySnapshot,
2174 mut display_point: DisplayPoint,
2175 char: char,
2176 times: usize,
2177) -> DisplayPoint {
2178 for _ in 0..times {
2179 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2180 let point = display_point.to_point(map);
2181 let offset = point.to_offset(&map.buffer_snapshot);
2182
2183 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2184 let Some(ranges) = ranges else { break };
2185 let mut closest_closing_destination = None;
2186 let mut closest_distance = usize::MAX;
2187
2188 for (_, close_range) in ranges {
2189 if close_range.start > offset {
2190 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2191 if Some(char) == chars.next() {
2192 let distance = close_range.start - offset;
2193 if distance < closest_distance {
2194 closest_closing_destination = Some(close_range.start);
2195 closest_distance = distance;
2196 continue;
2197 }
2198 }
2199 }
2200 }
2201
2202 let new_point = closest_closing_destination
2203 .map(|destination| destination.to_display_point(map))
2204 .unwrap_or(display_point);
2205 if new_point == display_point {
2206 break;
2207 }
2208 display_point = new_point;
2209 }
2210 return display_point;
2211}
2212
2213fn unmatched_backward(
2214 map: &DisplaySnapshot,
2215 mut display_point: DisplayPoint,
2216 char: char,
2217 times: usize,
2218) -> DisplayPoint {
2219 for _ in 0..times {
2220 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2221 let point = display_point.to_point(map);
2222 let offset = point.to_offset(&map.buffer_snapshot);
2223
2224 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2225 let Some(ranges) = ranges else {
2226 break;
2227 };
2228
2229 let mut closest_starting_destination = None;
2230 let mut closest_distance = usize::MAX;
2231
2232 for (start_range, _) in ranges {
2233 if start_range.start < offset {
2234 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2235 if Some(char) == chars.next() {
2236 let distance = offset - start_range.start;
2237 if distance < closest_distance {
2238 closest_starting_destination = Some(start_range.start);
2239 closest_distance = distance;
2240 continue;
2241 }
2242 }
2243 }
2244 }
2245
2246 let new_point = closest_starting_destination
2247 .map(|destination| destination.to_display_point(map))
2248 .unwrap_or(display_point);
2249 if new_point == display_point {
2250 break;
2251 } else {
2252 display_point = new_point;
2253 }
2254 }
2255 display_point
2256}
2257
2258fn find_forward(
2259 map: &DisplaySnapshot,
2260 from: DisplayPoint,
2261 before: bool,
2262 target: char,
2263 times: usize,
2264 mode: FindRange,
2265 smartcase: bool,
2266) -> Option<DisplayPoint> {
2267 let mut to = from;
2268 let mut found = false;
2269
2270 for _ in 0..times {
2271 found = false;
2272 let new_to = find_boundary(map, to, mode, |_, right| {
2273 found = is_character_match(target, right, smartcase);
2274 found
2275 });
2276 if to == new_to {
2277 break;
2278 }
2279 to = new_to;
2280 }
2281
2282 if found {
2283 if before && to.column() > 0 {
2284 *to.column_mut() -= 1;
2285 Some(map.clip_point(to, Bias::Left))
2286 } else {
2287 Some(to)
2288 }
2289 } else {
2290 None
2291 }
2292}
2293
2294fn find_backward(
2295 map: &DisplaySnapshot,
2296 from: DisplayPoint,
2297 after: bool,
2298 target: char,
2299 times: usize,
2300 mode: FindRange,
2301 smartcase: bool,
2302) -> DisplayPoint {
2303 let mut to = from;
2304
2305 for _ in 0..times {
2306 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2307 is_character_match(target, right, smartcase)
2308 });
2309 if to == new_to {
2310 break;
2311 }
2312 to = new_to;
2313 }
2314
2315 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2316 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2317 if after {
2318 *to.column_mut() += 1;
2319 map.clip_point(to, Bias::Right)
2320 } else {
2321 to
2322 }
2323 } else {
2324 from
2325 }
2326}
2327
2328fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2329 if smartcase {
2330 if target.is_uppercase() {
2331 target == other
2332 } else {
2333 target == other.to_ascii_lowercase()
2334 }
2335 } else {
2336 target == other
2337 }
2338}
2339
2340fn sneak(
2341 map: &DisplaySnapshot,
2342 from: DisplayPoint,
2343 first_target: char,
2344 second_target: char,
2345 times: usize,
2346 smartcase: bool,
2347) -> Option<DisplayPoint> {
2348 let mut to = from;
2349 let mut found = false;
2350
2351 for _ in 0..times {
2352 found = false;
2353 let new_to = find_boundary(
2354 map,
2355 movement::right(map, to),
2356 FindRange::MultiLine,
2357 |left, right| {
2358 found = is_character_match(first_target, left, smartcase)
2359 && is_character_match(second_target, right, smartcase);
2360 found
2361 },
2362 );
2363 if to == new_to {
2364 break;
2365 }
2366 to = new_to;
2367 }
2368
2369 if found {
2370 Some(movement::left(map, to))
2371 } else {
2372 None
2373 }
2374}
2375
2376fn sneak_backward(
2377 map: &DisplaySnapshot,
2378 from: DisplayPoint,
2379 first_target: char,
2380 second_target: char,
2381 times: usize,
2382 smartcase: bool,
2383) -> Option<DisplayPoint> {
2384 let mut to = from;
2385 let mut found = false;
2386
2387 for _ in 0..times {
2388 found = false;
2389 let new_to =
2390 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2391 found = is_character_match(first_target, left, smartcase)
2392 && is_character_match(second_target, right, smartcase);
2393 found
2394 });
2395 if to == new_to {
2396 break;
2397 }
2398 to = new_to;
2399 }
2400
2401 if found {
2402 Some(movement::left(map, to))
2403 } else {
2404 None
2405 }
2406}
2407
2408fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2409 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2410 first_non_whitespace(map, false, correct_line)
2411}
2412
2413fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2414 let correct_line = start_of_relative_buffer_row(map, point, -(times as isize));
2415 first_non_whitespace(map, false, correct_line)
2416}
2417
2418fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2419 let correct_line = start_of_relative_buffer_row(map, point, 0);
2420 right(map, correct_line, times.saturating_sub(1))
2421}
2422
2423pub(crate) fn next_line_end(
2424 map: &DisplaySnapshot,
2425 mut point: DisplayPoint,
2426 times: usize,
2427) -> DisplayPoint {
2428 if times > 1 {
2429 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2430 }
2431 end_of_line(map, false, point, 1)
2432}
2433
2434fn window_top(
2435 map: &DisplaySnapshot,
2436 point: DisplayPoint,
2437 text_layout_details: &TextLayoutDetails,
2438 mut times: usize,
2439) -> (DisplayPoint, SelectionGoal) {
2440 let first_visible_line = text_layout_details
2441 .scroll_anchor
2442 .anchor
2443 .to_display_point(map);
2444
2445 if first_visible_line.row() != DisplayRow(0)
2446 && text_layout_details.vertical_scroll_margin as usize > times
2447 {
2448 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2449 }
2450
2451 if let Some(visible_rows) = text_layout_details.visible_rows {
2452 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2453 let new_row = (first_visible_line.row().0 + (times as u32))
2454 .min(bottom_row)
2455 .min(map.max_point().row().0);
2456 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2457
2458 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2459 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2460 } else {
2461 let new_row =
2462 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2463 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2464
2465 let new_point = DisplayPoint::new(new_row, new_col);
2466 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2467 }
2468}
2469
2470fn window_middle(
2471 map: &DisplaySnapshot,
2472 point: DisplayPoint,
2473 text_layout_details: &TextLayoutDetails,
2474) -> (DisplayPoint, SelectionGoal) {
2475 if let Some(visible_rows) = text_layout_details.visible_rows {
2476 let first_visible_line = text_layout_details
2477 .scroll_anchor
2478 .anchor
2479 .to_display_point(map);
2480
2481 let max_visible_rows =
2482 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2483
2484 let new_row =
2485 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2486 let new_row = DisplayRow(new_row);
2487 let new_col = point.column().min(map.line_len(new_row));
2488 let new_point = DisplayPoint::new(new_row, new_col);
2489 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2490 } else {
2491 (point, SelectionGoal::None)
2492 }
2493}
2494
2495fn window_bottom(
2496 map: &DisplaySnapshot,
2497 point: DisplayPoint,
2498 text_layout_details: &TextLayoutDetails,
2499 mut times: usize,
2500) -> (DisplayPoint, SelectionGoal) {
2501 if let Some(visible_rows) = text_layout_details.visible_rows {
2502 let first_visible_line = text_layout_details
2503 .scroll_anchor
2504 .anchor
2505 .to_display_point(map);
2506 let bottom_row = first_visible_line.row().0
2507 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2508 if bottom_row < map.max_point().row().0
2509 && text_layout_details.vertical_scroll_margin as usize > times
2510 {
2511 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2512 }
2513 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2514 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2515 {
2516 first_visible_line.row()
2517 } else {
2518 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2519 };
2520 let new_col = point.column().min(map.line_len(new_row));
2521 let new_point = DisplayPoint::new(new_row, new_col);
2522 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2523 } else {
2524 (point, SelectionGoal::None)
2525 }
2526}
2527
2528fn method_motion(
2529 map: &DisplaySnapshot,
2530 mut display_point: DisplayPoint,
2531 times: usize,
2532 direction: Direction,
2533 is_start: bool,
2534) -> DisplayPoint {
2535 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2536 return display_point;
2537 };
2538
2539 for _ in 0..times {
2540 let point = map.display_point_to_point(display_point, Bias::Left);
2541 let offset = point.to_offset(&map.buffer_snapshot);
2542 let range = if direction == Direction::Prev {
2543 0..offset
2544 } else {
2545 offset..buffer.len()
2546 };
2547
2548 let possibilities = buffer
2549 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2550 .filter_map(|(range, object)| {
2551 if !matches!(object, language::TextObject::AroundFunction) {
2552 return None;
2553 }
2554
2555 let relevant = if is_start { range.start } else { range.end };
2556 if direction == Direction::Prev && relevant < offset {
2557 Some(relevant)
2558 } else if direction == Direction::Next && relevant > offset + 1 {
2559 Some(relevant)
2560 } else {
2561 None
2562 }
2563 });
2564
2565 let dest = if direction == Direction::Prev {
2566 possibilities.max().unwrap_or(offset)
2567 } else {
2568 possibilities.min().unwrap_or(offset)
2569 };
2570 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2571 if new_point == display_point {
2572 break;
2573 }
2574 display_point = new_point;
2575 }
2576 display_point
2577}
2578
2579fn comment_motion(
2580 map: &DisplaySnapshot,
2581 mut display_point: DisplayPoint,
2582 times: usize,
2583 direction: Direction,
2584) -> DisplayPoint {
2585 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2586 return display_point;
2587 };
2588
2589 for _ in 0..times {
2590 let point = map.display_point_to_point(display_point, Bias::Left);
2591 let offset = point.to_offset(&map.buffer_snapshot);
2592 let range = if direction == Direction::Prev {
2593 0..offset
2594 } else {
2595 offset..buffer.len()
2596 };
2597
2598 let possibilities = buffer
2599 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2600 .filter_map(|(range, object)| {
2601 if !matches!(object, language::TextObject::AroundComment) {
2602 return None;
2603 }
2604
2605 let relevant = if direction == Direction::Prev {
2606 range.start
2607 } else {
2608 range.end
2609 };
2610 if direction == Direction::Prev && relevant < offset {
2611 Some(relevant)
2612 } else if direction == Direction::Next && relevant > offset + 1 {
2613 Some(relevant)
2614 } else {
2615 None
2616 }
2617 });
2618
2619 let dest = if direction == Direction::Prev {
2620 possibilities.max().unwrap_or(offset)
2621 } else {
2622 possibilities.min().unwrap_or(offset)
2623 };
2624 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2625 if new_point == display_point {
2626 break;
2627 }
2628 display_point = new_point;
2629 }
2630
2631 display_point
2632}
2633
2634fn section_motion(
2635 map: &DisplaySnapshot,
2636 mut display_point: DisplayPoint,
2637 times: usize,
2638 direction: Direction,
2639 is_start: bool,
2640) -> DisplayPoint {
2641 if map.buffer_snapshot.as_singleton().is_some() {
2642 for _ in 0..times {
2643 let offset = map
2644 .display_point_to_point(display_point, Bias::Left)
2645 .to_offset(&map.buffer_snapshot);
2646 let range = if direction == Direction::Prev {
2647 0..offset
2648 } else {
2649 offset..map.buffer_snapshot.len()
2650 };
2651
2652 // we set a max start depth here because we want a section to only be "top level"
2653 // similar to vim's default of '{' in the first column.
2654 // (and without it, ]] at the start of editor.rs is -very- slow)
2655 let mut possibilities = map
2656 .buffer_snapshot
2657 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2658 .filter(|(_, object)| {
2659 matches!(
2660 object,
2661 language::TextObject::AroundClass | language::TextObject::AroundFunction
2662 )
2663 })
2664 .collect::<Vec<_>>();
2665 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2666 let mut prev_end = None;
2667 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2668 if t == language::TextObject::AroundFunction
2669 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2670 {
2671 return None;
2672 }
2673 prev_end = Some(range.end);
2674
2675 let relevant = if is_start { range.start } else { range.end };
2676 if direction == Direction::Prev && relevant < offset {
2677 Some(relevant)
2678 } else if direction == Direction::Next && relevant > offset + 1 {
2679 Some(relevant)
2680 } else {
2681 None
2682 }
2683 });
2684
2685 let offset = if direction == Direction::Prev {
2686 possibilities.max().unwrap_or(0)
2687 } else {
2688 possibilities.min().unwrap_or(map.buffer_snapshot.len())
2689 };
2690
2691 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2692 if new_point == display_point {
2693 break;
2694 }
2695 display_point = new_point;
2696 }
2697 return display_point;
2698 };
2699
2700 for _ in 0..times {
2701 let point = map.display_point_to_point(display_point, Bias::Left);
2702 let Some(excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2703 return display_point;
2704 };
2705 let next_point = match (direction, is_start) {
2706 (Direction::Prev, true) => {
2707 let mut start = excerpt.start_anchor().to_display_point(&map);
2708 if start >= display_point && start.row() > DisplayRow(0) {
2709 let Some(excerpt) = map.buffer_snapshot.excerpt_before(excerpt.id()) else {
2710 return display_point;
2711 };
2712 start = excerpt.start_anchor().to_display_point(&map);
2713 }
2714 start
2715 }
2716 (Direction::Prev, false) => {
2717 let mut start = excerpt.start_anchor().to_display_point(&map);
2718 if start.row() > DisplayRow(0) {
2719 *start.row_mut() -= 1;
2720 }
2721 map.clip_point(start, Bias::Left)
2722 }
2723 (Direction::Next, true) => {
2724 let mut end = excerpt.end_anchor().to_display_point(&map);
2725 *end.row_mut() += 1;
2726 map.clip_point(end, Bias::Right)
2727 }
2728 (Direction::Next, false) => {
2729 let mut end = excerpt.end_anchor().to_display_point(&map);
2730 *end.column_mut() = 0;
2731 if end <= display_point {
2732 *end.row_mut() += 1;
2733 let point_end = map.display_point_to_point(end, Bias::Right);
2734 let Some(excerpt) =
2735 map.buffer_snapshot.excerpt_containing(point_end..point_end)
2736 else {
2737 return display_point;
2738 };
2739 end = excerpt.end_anchor().to_display_point(&map);
2740 *end.column_mut() = 0;
2741 }
2742 end
2743 }
2744 };
2745 if next_point == display_point {
2746 break;
2747 }
2748 display_point = next_point;
2749 }
2750
2751 display_point
2752}
2753
2754#[cfg(test)]
2755mod test {
2756
2757 use crate::{
2758 state::Mode,
2759 test::{NeovimBackedTestContext, VimTestContext},
2760 };
2761 use editor::display_map::Inlay;
2762 use indoc::indoc;
2763
2764 #[gpui::test]
2765 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2766 let mut cx = NeovimBackedTestContext::new(cx).await;
2767
2768 let initial_state = indoc! {r"ˇabc
2769 def
2770
2771 paragraph
2772 the second
2773
2774
2775
2776 third and
2777 final"};
2778
2779 // goes down once
2780 cx.set_shared_state(initial_state).await;
2781 cx.simulate_shared_keystrokes("}").await;
2782 cx.shared_state().await.assert_eq(indoc! {r"abc
2783 def
2784 ˇ
2785 paragraph
2786 the second
2787
2788
2789
2790 third and
2791 final"});
2792
2793 // goes up once
2794 cx.simulate_shared_keystrokes("{").await;
2795 cx.shared_state().await.assert_eq(initial_state);
2796
2797 // goes down twice
2798 cx.simulate_shared_keystrokes("2 }").await;
2799 cx.shared_state().await.assert_eq(indoc! {r"abc
2800 def
2801
2802 paragraph
2803 the second
2804 ˇ
2805
2806
2807 third and
2808 final"});
2809
2810 // goes down over multiple blanks
2811 cx.simulate_shared_keystrokes("}").await;
2812 cx.shared_state().await.assert_eq(indoc! {r"abc
2813 def
2814
2815 paragraph
2816 the second
2817
2818
2819
2820 third and
2821 finaˇl"});
2822
2823 // goes up twice
2824 cx.simulate_shared_keystrokes("2 {").await;
2825 cx.shared_state().await.assert_eq(indoc! {r"abc
2826 def
2827 ˇ
2828 paragraph
2829 the second
2830
2831
2832
2833 third and
2834 final"});
2835 }
2836
2837 #[gpui::test]
2838 async fn test_matching(cx: &mut gpui::TestAppContext) {
2839 let mut cx = NeovimBackedTestContext::new(cx).await;
2840
2841 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2842 do(something(with<Types>.and_arrays[0, 2]))
2843 }"})
2844 .await;
2845 cx.simulate_shared_keystrokes("%").await;
2846 cx.shared_state()
2847 .await
2848 .assert_eq(indoc! {r"func (a stringˇ) {
2849 do(something(with<Types>.and_arrays[0, 2]))
2850 }"});
2851
2852 // test it works on the last character of the line
2853 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2854 do(something(with<Types>.and_arrays[0, 2]))
2855 }"})
2856 .await;
2857 cx.simulate_shared_keystrokes("%").await;
2858 cx.shared_state()
2859 .await
2860 .assert_eq(indoc! {r"func (a string) {
2861 do(something(with<Types>.and_arrays[0, 2]))
2862 ˇ}"});
2863
2864 // test it works on immediate nesting
2865 cx.set_shared_state("ˇ{()}").await;
2866 cx.simulate_shared_keystrokes("%").await;
2867 cx.shared_state().await.assert_eq("{()ˇ}");
2868 cx.simulate_shared_keystrokes("%").await;
2869 cx.shared_state().await.assert_eq("ˇ{()}");
2870
2871 // test it works on immediate nesting inside braces
2872 cx.set_shared_state("{\n ˇ{()}\n}").await;
2873 cx.simulate_shared_keystrokes("%").await;
2874 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
2875
2876 // test it jumps to the next paren on a line
2877 cx.set_shared_state("func ˇboop() {\n}").await;
2878 cx.simulate_shared_keystrokes("%").await;
2879 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2880 }
2881
2882 #[gpui::test]
2883 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2884 let mut cx = NeovimBackedTestContext::new(cx).await;
2885
2886 // test it works with curly braces
2887 cx.set_shared_state(indoc! {r"func (a string) {
2888 do(something(with<Types>.anˇd_arrays[0, 2]))
2889 }"})
2890 .await;
2891 cx.simulate_shared_keystrokes("] }").await;
2892 cx.shared_state()
2893 .await
2894 .assert_eq(indoc! {r"func (a string) {
2895 do(something(with<Types>.and_arrays[0, 2]))
2896 ˇ}"});
2897
2898 // test it works with brackets
2899 cx.set_shared_state(indoc! {r"func (a string) {
2900 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2901 }"})
2902 .await;
2903 cx.simulate_shared_keystrokes("] )").await;
2904 cx.shared_state()
2905 .await
2906 .assert_eq(indoc! {r"func (a string) {
2907 do(something(with<Types>.and_arrays[0, 2])ˇ)
2908 }"});
2909
2910 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2911 .await;
2912 cx.simulate_shared_keystrokes("] )").await;
2913 cx.shared_state()
2914 .await
2915 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2916
2917 // test it works on immediate nesting
2918 cx.set_shared_state("{ˇ {}{}}").await;
2919 cx.simulate_shared_keystrokes("] }").await;
2920 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2921 cx.set_shared_state("(ˇ ()())").await;
2922 cx.simulate_shared_keystrokes("] )").await;
2923 cx.shared_state().await.assert_eq("( ()()ˇ)");
2924
2925 // test it works on immediate nesting inside braces
2926 cx.set_shared_state("{\n ˇ {()}\n}").await;
2927 cx.simulate_shared_keystrokes("] }").await;
2928 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
2929 cx.set_shared_state("(\n ˇ {()}\n)").await;
2930 cx.simulate_shared_keystrokes("] )").await;
2931 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
2932 }
2933
2934 #[gpui::test]
2935 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2936 let mut cx = NeovimBackedTestContext::new(cx).await;
2937
2938 // test it works with curly braces
2939 cx.set_shared_state(indoc! {r"func (a string) {
2940 do(something(with<Types>.anˇd_arrays[0, 2]))
2941 }"})
2942 .await;
2943 cx.simulate_shared_keystrokes("[ {").await;
2944 cx.shared_state()
2945 .await
2946 .assert_eq(indoc! {r"func (a string) ˇ{
2947 do(something(with<Types>.and_arrays[0, 2]))
2948 }"});
2949
2950 // test it works with brackets
2951 cx.set_shared_state(indoc! {r"func (a string) {
2952 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2953 }"})
2954 .await;
2955 cx.simulate_shared_keystrokes("[ (").await;
2956 cx.shared_state()
2957 .await
2958 .assert_eq(indoc! {r"func (a string) {
2959 doˇ(something(with<Types>.and_arrays[0, 2]))
2960 }"});
2961
2962 // test it works on immediate nesting
2963 cx.set_shared_state("{{}{} ˇ }").await;
2964 cx.simulate_shared_keystrokes("[ {").await;
2965 cx.shared_state().await.assert_eq("ˇ{{}{} }");
2966 cx.set_shared_state("(()() ˇ )").await;
2967 cx.simulate_shared_keystrokes("[ (").await;
2968 cx.shared_state().await.assert_eq("ˇ(()() )");
2969
2970 // test it works on immediate nesting inside braces
2971 cx.set_shared_state("{\n {()} ˇ\n}").await;
2972 cx.simulate_shared_keystrokes("[ {").await;
2973 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
2974 cx.set_shared_state("(\n {()} ˇ\n)").await;
2975 cx.simulate_shared_keystrokes("[ (").await;
2976 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
2977 }
2978
2979 #[gpui::test]
2980 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2981 let mut cx = NeovimBackedTestContext::new_html(cx).await;
2982
2983 cx.neovim.exec("set filetype=html").await;
2984
2985 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2986 cx.simulate_shared_keystrokes("%").await;
2987 cx.shared_state()
2988 .await
2989 .assert_eq(indoc! {r"<body><ˇ/body>"});
2990 cx.simulate_shared_keystrokes("%").await;
2991
2992 // test jumping backwards
2993 cx.shared_state()
2994 .await
2995 .assert_eq(indoc! {r"<ˇbody></body>"});
2996
2997 // test self-closing tags
2998 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
2999 cx.simulate_shared_keystrokes("%").await;
3000 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3001
3002 // test tag with attributes
3003 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3004 </div>
3005 "})
3006 .await;
3007 cx.simulate_shared_keystrokes("%").await;
3008 cx.shared_state()
3009 .await
3010 .assert_eq(indoc! {r"<div class='test' id='main'>
3011 <ˇ/div>
3012 "});
3013
3014 // test multi-line self-closing tag
3015 cx.set_shared_state(indoc! {r#"<a>
3016 <br
3017 test = "test"
3018 /ˇ>
3019 </a>"#})
3020 .await;
3021 cx.simulate_shared_keystrokes("%").await;
3022 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3023 ˇ<br
3024 test = "test"
3025 />
3026 </a>"#});
3027 }
3028
3029 #[gpui::test]
3030 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3031 let mut cx = NeovimBackedTestContext::new(cx).await;
3032
3033 // f and F
3034 cx.set_shared_state("ˇone two three four").await;
3035 cx.simulate_shared_keystrokes("f o").await;
3036 cx.shared_state().await.assert_eq("one twˇo three four");
3037 cx.simulate_shared_keystrokes(",").await;
3038 cx.shared_state().await.assert_eq("ˇone two three four");
3039 cx.simulate_shared_keystrokes("2 ;").await;
3040 cx.shared_state().await.assert_eq("one two three fˇour");
3041 cx.simulate_shared_keystrokes("shift-f e").await;
3042 cx.shared_state().await.assert_eq("one two threˇe four");
3043 cx.simulate_shared_keystrokes("2 ;").await;
3044 cx.shared_state().await.assert_eq("onˇe two three four");
3045 cx.simulate_shared_keystrokes(",").await;
3046 cx.shared_state().await.assert_eq("one two thrˇee four");
3047
3048 // t and T
3049 cx.set_shared_state("ˇone two three four").await;
3050 cx.simulate_shared_keystrokes("t o").await;
3051 cx.shared_state().await.assert_eq("one tˇwo three four");
3052 cx.simulate_shared_keystrokes(",").await;
3053 cx.shared_state().await.assert_eq("oˇne two three four");
3054 cx.simulate_shared_keystrokes("2 ;").await;
3055 cx.shared_state().await.assert_eq("one two three ˇfour");
3056 cx.simulate_shared_keystrokes("shift-t e").await;
3057 cx.shared_state().await.assert_eq("one two threeˇ four");
3058 cx.simulate_shared_keystrokes("3 ;").await;
3059 cx.shared_state().await.assert_eq("oneˇ two three four");
3060 cx.simulate_shared_keystrokes(",").await;
3061 cx.shared_state().await.assert_eq("one two thˇree four");
3062 }
3063
3064 #[gpui::test]
3065 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3066 let mut cx = NeovimBackedTestContext::new(cx).await;
3067 let initial_state = indoc! {r"something(ˇfoo)"};
3068 cx.set_shared_state(initial_state).await;
3069 cx.simulate_shared_keystrokes("}").await;
3070 cx.shared_state().await.assert_eq("something(fooˇ)");
3071 }
3072
3073 #[gpui::test]
3074 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3075 let mut cx = NeovimBackedTestContext::new(cx).await;
3076 cx.set_shared_state("ˇone\n two\nthree").await;
3077 cx.simulate_shared_keystrokes("enter").await;
3078 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3079 }
3080
3081 #[gpui::test]
3082 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3083 let mut cx = NeovimBackedTestContext::new(cx).await;
3084 cx.set_shared_state("ˇ one\n two \nthree").await;
3085 cx.simulate_shared_keystrokes("g _").await;
3086 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3087
3088 cx.set_shared_state("ˇ one \n two \nthree").await;
3089 cx.simulate_shared_keystrokes("g _").await;
3090 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3091 cx.simulate_shared_keystrokes("2 g _").await;
3092 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3093 }
3094
3095 #[gpui::test]
3096 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3097 let mut cx = NeovimBackedTestContext::new(cx).await;
3098 let initial_state = indoc! {r"abc
3099 def
3100 paragraph
3101 the second
3102 third ˇand
3103 final"};
3104
3105 cx.set_shared_state(initial_state).await;
3106 cx.simulate_shared_keystrokes("shift-h").await;
3107 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3108 def
3109 paragraph
3110 the second
3111 third and
3112 final"});
3113
3114 // clip point
3115 cx.set_shared_state(indoc! {r"
3116 1 2 3
3117 4 5 6
3118 7 8 ˇ9
3119 "})
3120 .await;
3121 cx.simulate_shared_keystrokes("shift-h").await;
3122 cx.shared_state().await.assert_eq(indoc! {"
3123 1 2 ˇ3
3124 4 5 6
3125 7 8 9
3126 "});
3127
3128 cx.set_shared_state(indoc! {r"
3129 1 2 3
3130 4 5 6
3131 ˇ7 8 9
3132 "})
3133 .await;
3134 cx.simulate_shared_keystrokes("shift-h").await;
3135 cx.shared_state().await.assert_eq(indoc! {"
3136 ˇ1 2 3
3137 4 5 6
3138 7 8 9
3139 "});
3140
3141 cx.set_shared_state(indoc! {r"
3142 1 2 3
3143 4 5 ˇ6
3144 7 8 9"})
3145 .await;
3146 cx.simulate_shared_keystrokes("9 shift-h").await;
3147 cx.shared_state().await.assert_eq(indoc! {"
3148 1 2 3
3149 4 5 6
3150 7 8 ˇ9"});
3151 }
3152
3153 #[gpui::test]
3154 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3155 let mut cx = NeovimBackedTestContext::new(cx).await;
3156 let initial_state = indoc! {r"abˇc
3157 def
3158 paragraph
3159 the second
3160 third and
3161 final"};
3162
3163 cx.set_shared_state(initial_state).await;
3164 cx.simulate_shared_keystrokes("shift-m").await;
3165 cx.shared_state().await.assert_eq(indoc! {r"abc
3166 def
3167 paˇragraph
3168 the second
3169 third and
3170 final"});
3171
3172 cx.set_shared_state(indoc! {r"
3173 1 2 3
3174 4 5 6
3175 7 8 ˇ9
3176 "})
3177 .await;
3178 cx.simulate_shared_keystrokes("shift-m").await;
3179 cx.shared_state().await.assert_eq(indoc! {"
3180 1 2 3
3181 4 5 ˇ6
3182 7 8 9
3183 "});
3184 cx.set_shared_state(indoc! {r"
3185 1 2 3
3186 4 5 6
3187 ˇ7 8 9
3188 "})
3189 .await;
3190 cx.simulate_shared_keystrokes("shift-m").await;
3191 cx.shared_state().await.assert_eq(indoc! {"
3192 1 2 3
3193 ˇ4 5 6
3194 7 8 9
3195 "});
3196 cx.set_shared_state(indoc! {r"
3197 ˇ1 2 3
3198 4 5 6
3199 7 8 9
3200 "})
3201 .await;
3202 cx.simulate_shared_keystrokes("shift-m").await;
3203 cx.shared_state().await.assert_eq(indoc! {"
3204 1 2 3
3205 ˇ4 5 6
3206 7 8 9
3207 "});
3208 cx.set_shared_state(indoc! {r"
3209 1 2 3
3210 ˇ4 5 6
3211 7 8 9
3212 "})
3213 .await;
3214 cx.simulate_shared_keystrokes("shift-m").await;
3215 cx.shared_state().await.assert_eq(indoc! {"
3216 1 2 3
3217 ˇ4 5 6
3218 7 8 9
3219 "});
3220 cx.set_shared_state(indoc! {r"
3221 1 2 3
3222 4 5 ˇ6
3223 7 8 9
3224 "})
3225 .await;
3226 cx.simulate_shared_keystrokes("shift-m").await;
3227 cx.shared_state().await.assert_eq(indoc! {"
3228 1 2 3
3229 4 5 ˇ6
3230 7 8 9
3231 "});
3232 }
3233
3234 #[gpui::test]
3235 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3236 let mut cx = NeovimBackedTestContext::new(cx).await;
3237 let initial_state = indoc! {r"abc
3238 deˇf
3239 paragraph
3240 the second
3241 third and
3242 final"};
3243
3244 cx.set_shared_state(initial_state).await;
3245 cx.simulate_shared_keystrokes("shift-l").await;
3246 cx.shared_state().await.assert_eq(indoc! {r"abc
3247 def
3248 paragraph
3249 the second
3250 third and
3251 fiˇnal"});
3252
3253 cx.set_shared_state(indoc! {r"
3254 1 2 3
3255 4 5 ˇ6
3256 7 8 9
3257 "})
3258 .await;
3259 cx.simulate_shared_keystrokes("shift-l").await;
3260 cx.shared_state().await.assert_eq(indoc! {"
3261 1 2 3
3262 4 5 6
3263 7 8 9
3264 ˇ"});
3265
3266 cx.set_shared_state(indoc! {r"
3267 1 2 3
3268 ˇ4 5 6
3269 7 8 9
3270 "})
3271 .await;
3272 cx.simulate_shared_keystrokes("shift-l").await;
3273 cx.shared_state().await.assert_eq(indoc! {"
3274 1 2 3
3275 4 5 6
3276 7 8 9
3277 ˇ"});
3278
3279 cx.set_shared_state(indoc! {r"
3280 1 2 ˇ3
3281 4 5 6
3282 7 8 9
3283 "})
3284 .await;
3285 cx.simulate_shared_keystrokes("shift-l").await;
3286 cx.shared_state().await.assert_eq(indoc! {"
3287 1 2 3
3288 4 5 6
3289 7 8 9
3290 ˇ"});
3291
3292 cx.set_shared_state(indoc! {r"
3293 ˇ1 2 3
3294 4 5 6
3295 7 8 9
3296 "})
3297 .await;
3298 cx.simulate_shared_keystrokes("shift-l").await;
3299 cx.shared_state().await.assert_eq(indoc! {"
3300 1 2 3
3301 4 5 6
3302 7 8 9
3303 ˇ"});
3304
3305 cx.set_shared_state(indoc! {r"
3306 1 2 3
3307 4 5 ˇ6
3308 7 8 9
3309 "})
3310 .await;
3311 cx.simulate_shared_keystrokes("9 shift-l").await;
3312 cx.shared_state().await.assert_eq(indoc! {"
3313 1 2 ˇ3
3314 4 5 6
3315 7 8 9
3316 "});
3317 }
3318
3319 #[gpui::test]
3320 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3321 let mut cx = NeovimBackedTestContext::new(cx).await;
3322 cx.set_shared_state(indoc! {r"
3323 456 5ˇ67 678
3324 "})
3325 .await;
3326 cx.simulate_shared_keystrokes("g e").await;
3327 cx.shared_state().await.assert_eq(indoc! {"
3328 45ˇ6 567 678
3329 "});
3330
3331 // Test times
3332 cx.set_shared_state(indoc! {r"
3333 123 234 345
3334 456 5ˇ67 678
3335 "})
3336 .await;
3337 cx.simulate_shared_keystrokes("4 g e").await;
3338 cx.shared_state().await.assert_eq(indoc! {"
3339 12ˇ3 234 345
3340 456 567 678
3341 "});
3342
3343 // With punctuation
3344 cx.set_shared_state(indoc! {r"
3345 123 234 345
3346 4;5.6 5ˇ67 678
3347 789 890 901
3348 "})
3349 .await;
3350 cx.simulate_shared_keystrokes("g e").await;
3351 cx.shared_state().await.assert_eq(indoc! {"
3352 123 234 345
3353 4;5.ˇ6 567 678
3354 789 890 901
3355 "});
3356
3357 // With punctuation and count
3358 cx.set_shared_state(indoc! {r"
3359 123 234 345
3360 4;5.6 5ˇ67 678
3361 789 890 901
3362 "})
3363 .await;
3364 cx.simulate_shared_keystrokes("5 g e").await;
3365 cx.shared_state().await.assert_eq(indoc! {"
3366 123 234 345
3367 ˇ4;5.6 567 678
3368 789 890 901
3369 "});
3370
3371 // newlines
3372 cx.set_shared_state(indoc! {r"
3373 123 234 345
3374
3375 78ˇ9 890 901
3376 "})
3377 .await;
3378 cx.simulate_shared_keystrokes("g e").await;
3379 cx.shared_state().await.assert_eq(indoc! {"
3380 123 234 345
3381 ˇ
3382 789 890 901
3383 "});
3384 cx.simulate_shared_keystrokes("g e").await;
3385 cx.shared_state().await.assert_eq(indoc! {"
3386 123 234 34ˇ5
3387
3388 789 890 901
3389 "});
3390
3391 // With punctuation
3392 cx.set_shared_state(indoc! {r"
3393 123 234 345
3394 4;5.ˇ6 567 678
3395 789 890 901
3396 "})
3397 .await;
3398 cx.simulate_shared_keystrokes("g shift-e").await;
3399 cx.shared_state().await.assert_eq(indoc! {"
3400 123 234 34ˇ5
3401 4;5.6 567 678
3402 789 890 901
3403 "});
3404 }
3405
3406 #[gpui::test]
3407 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3408 let mut cx = NeovimBackedTestContext::new(cx).await;
3409
3410 cx.set_shared_state(indoc! {"
3411 fn aˇ() {
3412 return
3413 }
3414 "})
3415 .await;
3416 cx.simulate_shared_keystrokes("v $ %").await;
3417 cx.shared_state().await.assert_eq(indoc! {"
3418 fn a«() {
3419 return
3420 }ˇ»
3421 "});
3422 }
3423
3424 #[gpui::test]
3425 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3426 let mut cx = VimTestContext::new(cx, true).await;
3427
3428 cx.set_state(
3429 indoc! {"
3430 struct Foo {
3431 ˇ
3432 }
3433 "},
3434 Mode::Normal,
3435 );
3436
3437 cx.update_editor(|editor, _window, cx| {
3438 let range = editor.selections.newest_anchor().range();
3439 let inlay_text = " field: int,\n field2: string\n field3: float";
3440 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3441 editor.splice_inlays(vec![], vec![inlay], cx);
3442 });
3443
3444 cx.simulate_keystrokes("j");
3445 cx.assert_state(
3446 indoc! {"
3447 struct Foo {
3448
3449 ˇ}
3450 "},
3451 Mode::Normal,
3452 );
3453 }
3454}