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