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