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 } else if point.row() < map.max_point().row() {
1326 *point.row_mut() += 1;
1327 *point.column_mut() = 0;
1328 }
1329 point
1330}
1331
1332pub(crate) fn start_of_relative_buffer_row(
1333 map: &DisplaySnapshot,
1334 point: DisplayPoint,
1335 times: isize,
1336) -> DisplayPoint {
1337 let start = map.display_point_to_fold_point(point, Bias::Left);
1338 let target = start.row() as isize + times;
1339 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1340
1341 map.clip_point(
1342 map.fold_point_to_display_point(
1343 map.fold_snapshot
1344 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
1345 ),
1346 Bias::Right,
1347 )
1348}
1349
1350fn up_down_buffer_rows(
1351 map: &DisplaySnapshot,
1352 mut point: DisplayPoint,
1353 mut goal: SelectionGoal,
1354 mut times: isize,
1355 text_layout_details: &TextLayoutDetails,
1356) -> (DisplayPoint, SelectionGoal) {
1357 let bias = if times < 0 { Bias::Left } else { Bias::Right };
1358
1359 while map.is_folded_buffer_header(point.row()) {
1360 if times < 0 {
1361 (point, _) = movement::up(map, point, goal, true, text_layout_details);
1362 times += 1;
1363 } else if times > 0 {
1364 (point, _) = movement::down(map, point, goal, true, text_layout_details);
1365 times -= 1;
1366 } else {
1367 break;
1368 }
1369 }
1370
1371 let start = map.display_point_to_fold_point(point, Bias::Left);
1372 let begin_folded_line = map.fold_point_to_display_point(
1373 map.fold_snapshot
1374 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1375 );
1376 let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1377
1378 let (goal_wrap, goal_x) = match goal {
1379 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1380 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
1381 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
1382 _ => {
1383 let x = map.x_for_display_point(point, text_layout_details);
1384 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
1385 (select_nth_wrapped_row, x.0)
1386 }
1387 };
1388
1389 let target = start.row() as isize + times;
1390 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
1391
1392 let mut begin_folded_line = map.fold_point_to_display_point(
1393 map.fold_snapshot
1394 .clip_point(FoldPoint::new(new_row, 0), bias),
1395 );
1396
1397 let mut i = 0;
1398 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1399 let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1400 if map
1401 .display_point_to_fold_point(next_folded_line, bias)
1402 .row()
1403 == new_row
1404 {
1405 i += 1;
1406 begin_folded_line = next_folded_line;
1407 } else {
1408 break;
1409 }
1410 }
1411
1412 let new_col = if i == goal_wrap {
1413 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1414 } else {
1415 map.line_len(begin_folded_line.row())
1416 };
1417
1418 (
1419 map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
1420 goal,
1421 )
1422}
1423
1424fn down_display(
1425 map: &DisplaySnapshot,
1426 mut point: DisplayPoint,
1427 mut goal: SelectionGoal,
1428 times: usize,
1429 text_layout_details: &TextLayoutDetails,
1430) -> (DisplayPoint, SelectionGoal) {
1431 for _ in 0..times {
1432 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1433 }
1434
1435 (point, goal)
1436}
1437
1438fn up_display(
1439 map: &DisplaySnapshot,
1440 mut point: DisplayPoint,
1441 mut goal: SelectionGoal,
1442 times: usize,
1443 text_layout_details: &TextLayoutDetails,
1444) -> (DisplayPoint, SelectionGoal) {
1445 for _ in 0..times {
1446 (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1447 }
1448
1449 (point, goal)
1450}
1451
1452pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1453 for _ in 0..times {
1454 let new_point = movement::saturating_right(map, point);
1455 if point == new_point {
1456 break;
1457 }
1458 point = new_point;
1459 }
1460 point
1461}
1462
1463pub(crate) fn next_char(
1464 map: &DisplaySnapshot,
1465 point: DisplayPoint,
1466 allow_cross_newline: bool,
1467) -> DisplayPoint {
1468 let mut new_point = point;
1469 let mut max_column = map.line_len(new_point.row());
1470 if !allow_cross_newline {
1471 max_column -= 1;
1472 }
1473 if new_point.column() < max_column {
1474 *new_point.column_mut() += 1;
1475 } else if new_point < map.max_point() && allow_cross_newline {
1476 *new_point.row_mut() += 1;
1477 *new_point.column_mut() = 0;
1478 }
1479 map.clip_ignoring_line_ends(new_point, Bias::Right)
1480}
1481
1482pub(crate) fn next_word_start(
1483 map: &DisplaySnapshot,
1484 mut point: DisplayPoint,
1485 ignore_punctuation: bool,
1486 times: usize,
1487) -> DisplayPoint {
1488 let classifier = map
1489 .buffer_snapshot
1490 .char_classifier_at(point.to_point(map))
1491 .ignore_punctuation(ignore_punctuation);
1492 for _ in 0..times {
1493 let mut crossed_newline = false;
1494 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1495 let left_kind = classifier.kind(left);
1496 let right_kind = classifier.kind(right);
1497 let at_newline = right == '\n';
1498
1499 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1500 || at_newline && crossed_newline
1501 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1502
1503 crossed_newline |= at_newline;
1504 found
1505 });
1506 if point == new_point {
1507 break;
1508 }
1509 point = new_point;
1510 }
1511 point
1512}
1513
1514pub(crate) fn next_word_end(
1515 map: &DisplaySnapshot,
1516 mut point: DisplayPoint,
1517 ignore_punctuation: bool,
1518 times: usize,
1519 allow_cross_newline: bool,
1520) -> DisplayPoint {
1521 let classifier = map
1522 .buffer_snapshot
1523 .char_classifier_at(point.to_point(map))
1524 .ignore_punctuation(ignore_punctuation);
1525 for _ in 0..times {
1526 let new_point = next_char(map, point, allow_cross_newline);
1527 let mut need_next_char = false;
1528 let new_point = movement::find_boundary_exclusive(
1529 map,
1530 new_point,
1531 FindRange::MultiLine,
1532 |left, right| {
1533 let left_kind = classifier.kind(left);
1534 let right_kind = classifier.kind(right);
1535 let at_newline = right == '\n';
1536
1537 if !allow_cross_newline && at_newline {
1538 need_next_char = true;
1539 return true;
1540 }
1541
1542 left_kind != right_kind && left_kind != CharKind::Whitespace
1543 },
1544 );
1545 let new_point = if need_next_char {
1546 next_char(map, new_point, true)
1547 } else {
1548 new_point
1549 };
1550 let new_point = map.clip_point(new_point, Bias::Left);
1551 if point == new_point {
1552 break;
1553 }
1554 point = new_point;
1555 }
1556 point
1557}
1558
1559fn previous_word_start(
1560 map: &DisplaySnapshot,
1561 mut point: DisplayPoint,
1562 ignore_punctuation: bool,
1563 times: usize,
1564) -> DisplayPoint {
1565 let classifier = map
1566 .buffer_snapshot
1567 .char_classifier_at(point.to_point(map))
1568 .ignore_punctuation(ignore_punctuation);
1569 for _ in 0..times {
1570 // This works even though find_preceding_boundary is called for every character in the line containing
1571 // cursor because the newline is checked only once.
1572 let new_point = movement::find_preceding_boundary_display_point(
1573 map,
1574 point,
1575 FindRange::MultiLine,
1576 |left, right| {
1577 let left_kind = classifier.kind(left);
1578 let right_kind = classifier.kind(right);
1579
1580 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1581 },
1582 );
1583 if point == new_point {
1584 break;
1585 }
1586 point = new_point;
1587 }
1588 point
1589}
1590
1591fn previous_word_end(
1592 map: &DisplaySnapshot,
1593 point: DisplayPoint,
1594 ignore_punctuation: bool,
1595 times: usize,
1596) -> DisplayPoint {
1597 let classifier = map
1598 .buffer_snapshot
1599 .char_classifier_at(point.to_point(map))
1600 .ignore_punctuation(ignore_punctuation);
1601 let mut point = point.to_point(map);
1602
1603 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1604 point.column += 1;
1605 }
1606 for _ in 0..times {
1607 let new_point = movement::find_preceding_boundary_point(
1608 &map.buffer_snapshot,
1609 point,
1610 FindRange::MultiLine,
1611 |left, right| {
1612 let left_kind = classifier.kind(left);
1613 let right_kind = classifier.kind(right);
1614 match (left_kind, right_kind) {
1615 (CharKind::Punctuation, CharKind::Whitespace)
1616 | (CharKind::Punctuation, CharKind::Word)
1617 | (CharKind::Word, CharKind::Whitespace)
1618 | (CharKind::Word, CharKind::Punctuation) => true,
1619 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1620 _ => false,
1621 }
1622 },
1623 );
1624 if new_point == point {
1625 break;
1626 }
1627 point = new_point;
1628 }
1629 movement::saturating_left(map, point.to_display_point(map))
1630}
1631
1632fn next_subword_start(
1633 map: &DisplaySnapshot,
1634 mut point: DisplayPoint,
1635 ignore_punctuation: bool,
1636 times: usize,
1637) -> DisplayPoint {
1638 let classifier = map
1639 .buffer_snapshot
1640 .char_classifier_at(point.to_point(map))
1641 .ignore_punctuation(ignore_punctuation);
1642 for _ in 0..times {
1643 let mut crossed_newline = false;
1644 let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
1645 let left_kind = classifier.kind(left);
1646 let right_kind = classifier.kind(right);
1647 let at_newline = right == '\n';
1648
1649 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1650 let is_subword_start =
1651 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1652
1653 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1654 || at_newline && crossed_newline
1655 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1656
1657 crossed_newline |= at_newline;
1658 found
1659 });
1660 if point == new_point {
1661 break;
1662 }
1663 point = new_point;
1664 }
1665 point
1666}
1667
1668pub(crate) fn next_subword_end(
1669 map: &DisplaySnapshot,
1670 mut point: DisplayPoint,
1671 ignore_punctuation: bool,
1672 times: usize,
1673 allow_cross_newline: bool,
1674) -> DisplayPoint {
1675 let classifier = map
1676 .buffer_snapshot
1677 .char_classifier_at(point.to_point(map))
1678 .ignore_punctuation(ignore_punctuation);
1679 for _ in 0..times {
1680 let new_point = next_char(map, point, allow_cross_newline);
1681
1682 let mut crossed_newline = false;
1683 let mut need_backtrack = false;
1684 let new_point =
1685 movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| {
1686 let left_kind = classifier.kind(left);
1687 let right_kind = classifier.kind(right);
1688 let at_newline = right == '\n';
1689
1690 if !allow_cross_newline && at_newline {
1691 return true;
1692 }
1693
1694 let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric();
1695 let is_subword_end =
1696 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1697
1698 let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end);
1699
1700 if found && (is_word_end || is_subword_end) {
1701 need_backtrack = true;
1702 }
1703
1704 crossed_newline |= at_newline;
1705 found
1706 });
1707 let mut new_point = map.clip_point(new_point, Bias::Left);
1708 if need_backtrack {
1709 *new_point.column_mut() -= 1;
1710 }
1711 let new_point = map.clip_point(new_point, Bias::Left);
1712 if point == new_point {
1713 break;
1714 }
1715 point = new_point;
1716 }
1717 point
1718}
1719
1720fn previous_subword_start(
1721 map: &DisplaySnapshot,
1722 mut point: DisplayPoint,
1723 ignore_punctuation: bool,
1724 times: usize,
1725) -> DisplayPoint {
1726 let classifier = map
1727 .buffer_snapshot
1728 .char_classifier_at(point.to_point(map))
1729 .ignore_punctuation(ignore_punctuation);
1730 for _ in 0..times {
1731 let mut crossed_newline = false;
1732 // This works even though find_preceding_boundary is called for every character in the line containing
1733 // cursor because the newline is checked only once.
1734 let new_point = movement::find_preceding_boundary_display_point(
1735 map,
1736 point,
1737 FindRange::MultiLine,
1738 |left, right| {
1739 let left_kind = classifier.kind(left);
1740 let right_kind = classifier.kind(right);
1741 let at_newline = right == '\n';
1742
1743 let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric();
1744 let is_subword_start =
1745 left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
1746
1747 let found = (!right.is_whitespace() && (is_word_start || is_subword_start))
1748 || at_newline && crossed_newline
1749 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1750
1751 crossed_newline |= at_newline;
1752
1753 found
1754 },
1755 );
1756 if point == new_point {
1757 break;
1758 }
1759 point = new_point;
1760 }
1761 point
1762}
1763
1764fn previous_subword_end(
1765 map: &DisplaySnapshot,
1766 point: DisplayPoint,
1767 ignore_punctuation: bool,
1768 times: usize,
1769) -> DisplayPoint {
1770 let classifier = map
1771 .buffer_snapshot
1772 .char_classifier_at(point.to_point(map))
1773 .ignore_punctuation(ignore_punctuation);
1774 let mut point = point.to_point(map);
1775
1776 if point.column < map.buffer_snapshot.line_len(MultiBufferRow(point.row)) {
1777 point.column += 1;
1778 }
1779 for _ in 0..times {
1780 let new_point = movement::find_preceding_boundary_point(
1781 &map.buffer_snapshot,
1782 point,
1783 FindRange::MultiLine,
1784 |left, right| {
1785 let left_kind = classifier.kind(left);
1786 let right_kind = classifier.kind(right);
1787
1788 let is_subword_end =
1789 left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
1790
1791 if is_subword_end {
1792 return true;
1793 }
1794
1795 match (left_kind, right_kind) {
1796 (CharKind::Word, CharKind::Whitespace)
1797 | (CharKind::Word, CharKind::Punctuation) => true,
1798 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1799 _ => false,
1800 }
1801 },
1802 );
1803 if new_point == point {
1804 break;
1805 }
1806 point = new_point;
1807 }
1808 movement::saturating_left(map, point.to_display_point(map))
1809}
1810
1811pub(crate) fn first_non_whitespace(
1812 map: &DisplaySnapshot,
1813 display_lines: bool,
1814 from: DisplayPoint,
1815) -> DisplayPoint {
1816 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
1817 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1818 for (ch, offset) in map.buffer_chars_at(start_offset) {
1819 if ch == '\n' {
1820 return from;
1821 }
1822
1823 start_offset = offset;
1824
1825 if classifier.kind(ch) != CharKind::Whitespace {
1826 break;
1827 }
1828 }
1829
1830 start_offset.to_display_point(map)
1831}
1832
1833pub(crate) fn last_non_whitespace(
1834 map: &DisplaySnapshot,
1835 from: DisplayPoint,
1836 count: usize,
1837) -> DisplayPoint {
1838 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
1839 let classifier = map.buffer_snapshot.char_classifier_at(from.to_point(map));
1840
1841 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
1842 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() {
1843 if classifier.kind(ch) != CharKind::Whitespace {
1844 return end_of_line.to_display_point(map);
1845 }
1846 }
1847
1848 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
1849 if ch == '\n' {
1850 break;
1851 }
1852 end_of_line = offset;
1853 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
1854 break;
1855 }
1856 }
1857
1858 end_of_line.to_display_point(map)
1859}
1860
1861pub(crate) fn start_of_line(
1862 map: &DisplaySnapshot,
1863 display_lines: bool,
1864 point: DisplayPoint,
1865) -> DisplayPoint {
1866 if display_lines {
1867 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
1868 } else {
1869 map.prev_line_boundary(point.to_point(map)).1
1870 }
1871}
1872
1873pub(crate) fn end_of_line(
1874 map: &DisplaySnapshot,
1875 display_lines: bool,
1876 mut point: DisplayPoint,
1877 times: usize,
1878) -> DisplayPoint {
1879 if times > 1 {
1880 point = start_of_relative_buffer_row(map, point, times as isize - 1);
1881 }
1882 if display_lines {
1883 map.clip_point(
1884 DisplayPoint::new(point.row(), map.line_len(point.row())),
1885 Bias::Left,
1886 )
1887 } else {
1888 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
1889 }
1890}
1891
1892fn sentence_backwards(
1893 map: &DisplaySnapshot,
1894 point: DisplayPoint,
1895 mut times: usize,
1896) -> DisplayPoint {
1897 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot);
1898 let mut chars = map.reverse_buffer_chars_at(start).peekable();
1899
1900 let mut was_newline = map
1901 .buffer_chars_at(start)
1902 .next()
1903 .is_some_and(|(c, _)| c == '\n');
1904
1905 while let Some((ch, offset)) = chars.next() {
1906 let start_of_next_sentence = if was_newline && ch == '\n' {
1907 Some(offset + ch.len_utf8())
1908 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1909 Some(next_non_blank(map, offset + ch.len_utf8()))
1910 } else if ch == '.' || ch == '?' || ch == '!' {
1911 start_of_next_sentence(map, offset + ch.len_utf8())
1912 } else {
1913 None
1914 };
1915
1916 if let Some(start_of_next_sentence) = start_of_next_sentence {
1917 if start_of_next_sentence < start {
1918 times = times.saturating_sub(1);
1919 }
1920 if times == 0 || offset == 0 {
1921 return map.clip_point(
1922 start_of_next_sentence
1923 .to_offset(&map.buffer_snapshot)
1924 .to_display_point(map),
1925 Bias::Left,
1926 );
1927 }
1928 }
1929 if was_newline {
1930 start = offset;
1931 }
1932 was_newline = ch == '\n';
1933 }
1934
1935 DisplayPoint::zero()
1936}
1937
1938fn sentence_forwards(map: &DisplaySnapshot, point: DisplayPoint, mut times: usize) -> DisplayPoint {
1939 let start = point.to_point(map).to_offset(&map.buffer_snapshot);
1940 let mut chars = map.buffer_chars_at(start).peekable();
1941
1942 let mut was_newline = map
1943 .reverse_buffer_chars_at(start)
1944 .next()
1945 .is_some_and(|(c, _)| c == '\n')
1946 && chars.peek().is_some_and(|(c, _)| *c == '\n');
1947
1948 while let Some((ch, offset)) = chars.next() {
1949 if was_newline && ch == '\n' {
1950 continue;
1951 }
1952 let start_of_next_sentence = if was_newline {
1953 Some(next_non_blank(map, offset))
1954 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
1955 Some(next_non_blank(map, offset + ch.len_utf8()))
1956 } else if ch == '.' || ch == '?' || ch == '!' {
1957 start_of_next_sentence(map, offset + ch.len_utf8())
1958 } else {
1959 None
1960 };
1961
1962 if let Some(start_of_next_sentence) = start_of_next_sentence {
1963 times = times.saturating_sub(1);
1964 if times == 0 {
1965 return map.clip_point(
1966 start_of_next_sentence
1967 .to_offset(&map.buffer_snapshot)
1968 .to_display_point(map),
1969 Bias::Right,
1970 );
1971 }
1972 }
1973
1974 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
1975 }
1976
1977 map.max_point()
1978}
1979
1980fn next_non_blank(map: &DisplaySnapshot, start: usize) -> usize {
1981 for (c, o) in map.buffer_chars_at(start) {
1982 if c == '\n' || !c.is_whitespace() {
1983 return o;
1984 }
1985 }
1986
1987 map.buffer_snapshot.len()
1988}
1989
1990// given the offset after a ., !, or ? find the start of the next sentence.
1991// if this is not a sentence boundary, returns None.
1992fn start_of_next_sentence(map: &DisplaySnapshot, end_of_sentence: usize) -> Option<usize> {
1993 let chars = map.buffer_chars_at(end_of_sentence);
1994 let mut seen_space = false;
1995
1996 for (char, offset) in chars {
1997 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
1998 continue;
1999 }
2000
2001 if char == '\n' && seen_space {
2002 return Some(offset);
2003 } else if char.is_whitespace() {
2004 seen_space = true;
2005 } else if seen_space {
2006 return Some(offset);
2007 } else {
2008 return None;
2009 }
2010 }
2011
2012 Some(map.buffer_snapshot.len())
2013}
2014
2015fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2016 let point = map.display_point_to_point(display_point, Bias::Left);
2017 let Some(mut excerpt) = map.buffer_snapshot.excerpt_containing(point..point) else {
2018 return display_point;
2019 };
2020 let offset = excerpt.buffer().point_to_offset(
2021 excerpt
2022 .buffer()
2023 .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2024 );
2025 let buffer_range = excerpt.buffer_range();
2026 if offset >= buffer_range.start && offset <= buffer_range.end {
2027 let point = map
2028 .buffer_snapshot
2029 .offset_to_point(excerpt.map_offset_from_buffer(offset));
2030 return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left);
2031 }
2032 let mut last_position = None;
2033 for (excerpt, buffer, range) in map.buffer_snapshot.excerpts() {
2034 let excerpt_range = language::ToOffset::to_offset(&range.context.start, &buffer)
2035 ..language::ToOffset::to_offset(&range.context.end, &buffer);
2036 if offset >= excerpt_range.start && offset <= excerpt_range.end {
2037 let text_anchor = buffer.anchor_after(offset);
2038 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), text_anchor);
2039 return anchor.to_display_point(map);
2040 } else if offset <= excerpt_range.start {
2041 let anchor = Anchor::in_buffer(excerpt, buffer.remote_id(), range.context.start);
2042 return anchor.to_display_point(map);
2043 } else {
2044 last_position = Some(Anchor::in_buffer(
2045 excerpt,
2046 buffer.remote_id(),
2047 range.context.end,
2048 ));
2049 }
2050 }
2051
2052 let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot);
2053 last_point.column = point.column;
2054
2055 map.clip_point(
2056 map.point_to_display_point(
2057 map.buffer_snapshot.clip_point(point, Bias::Left),
2058 Bias::Left,
2059 ),
2060 Bias::Left,
2061 )
2062}
2063
2064fn start_of_document(
2065 map: &DisplaySnapshot,
2066 display_point: DisplayPoint,
2067 maybe_times: Option<usize>,
2068) -> DisplayPoint {
2069 if let Some(times) = maybe_times {
2070 return go_to_line(map, display_point, times);
2071 }
2072
2073 let point = map.display_point_to_point(display_point, Bias::Left);
2074 let mut first_point = Point::zero();
2075 first_point.column = point.column;
2076
2077 map.clip_point(
2078 map.point_to_display_point(
2079 map.buffer_snapshot.clip_point(first_point, Bias::Left),
2080 Bias::Left,
2081 ),
2082 Bias::Left,
2083 )
2084}
2085
2086fn end_of_document(
2087 map: &DisplaySnapshot,
2088 display_point: DisplayPoint,
2089 maybe_times: Option<usize>,
2090) -> DisplayPoint {
2091 if let Some(times) = maybe_times {
2092 return go_to_line(map, display_point, times);
2093 };
2094 let point = map.display_point_to_point(display_point, Bias::Left);
2095 let mut last_point = map.buffer_snapshot.max_point();
2096 last_point.column = point.column;
2097
2098 map.clip_point(
2099 map.point_to_display_point(
2100 map.buffer_snapshot.clip_point(last_point, Bias::Left),
2101 Bias::Left,
2102 ),
2103 Bias::Left,
2104 )
2105}
2106
2107fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2108 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2109 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2110
2111 if head > outer.start && head < inner.start {
2112 let mut offset = inner.end.to_offset(map, Bias::Left);
2113 for c in map.buffer_snapshot.chars_at(offset) {
2114 if c == '/' || c == '\n' || c == '>' {
2115 return Some(offset.to_display_point(map));
2116 }
2117 offset += c.len_utf8();
2118 }
2119 } else {
2120 let mut offset = outer.start.to_offset(map, Bias::Left);
2121 for c in map.buffer_snapshot.chars_at(offset) {
2122 offset += c.len_utf8();
2123 if c == '<' || c == '\n' {
2124 return Some(offset.to_display_point(map));
2125 }
2126 }
2127 }
2128
2129 return None;
2130}
2131
2132fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
2133 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2134 let display_point = map.clip_at_line_end(display_point);
2135 let point = display_point.to_point(map);
2136 let offset = point.to_offset(&map.buffer_snapshot);
2137
2138 // Ensure the range is contained by the current line.
2139 let mut line_end = map.next_line_boundary(point).0;
2140 if line_end == point {
2141 line_end = map.max_point().to_point(map);
2142 }
2143
2144 let line_range = map.prev_line_boundary(point).0..line_end;
2145 let visible_line_range =
2146 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2147 let ranges = map
2148 .buffer_snapshot
2149 .bracket_ranges(visible_line_range.clone());
2150 if let Some(ranges) = ranges {
2151 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
2152 ..line_range.end.to_offset(&map.buffer_snapshot);
2153 let mut closest_pair_destination = None;
2154 let mut closest_distance = usize::MAX;
2155
2156 for (open_range, close_range) in ranges {
2157 if map.buffer_snapshot.chars_at(open_range.start).next() == Some('<') {
2158 if offset > open_range.start && offset < close_range.start {
2159 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2160 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2161 return display_point;
2162 }
2163 if let Some(tag) = matching_tag(map, display_point) {
2164 return tag;
2165 }
2166 } else if close_range.contains(&offset) {
2167 return open_range.start.to_display_point(map);
2168 } else if open_range.contains(&offset) {
2169 return (close_range.end - 1).to_display_point(map);
2170 }
2171 }
2172
2173 if (open_range.contains(&offset) || open_range.start >= offset)
2174 && line_range.contains(&open_range.start)
2175 {
2176 let distance = open_range.start.saturating_sub(offset);
2177 if distance < closest_distance {
2178 closest_pair_destination = Some(close_range.start);
2179 closest_distance = distance;
2180 continue;
2181 }
2182 }
2183
2184 if (close_range.contains(&offset) || close_range.start >= offset)
2185 && line_range.contains(&close_range.start)
2186 {
2187 let distance = close_range.start.saturating_sub(offset);
2188 if distance < closest_distance {
2189 closest_pair_destination = Some(open_range.start);
2190 closest_distance = distance;
2191 continue;
2192 }
2193 }
2194
2195 continue;
2196 }
2197
2198 closest_pair_destination
2199 .map(|destination| destination.to_display_point(map))
2200 .unwrap_or(display_point)
2201 } else {
2202 display_point
2203 }
2204}
2205
2206// Go to {count} percentage in the file, on the first
2207// non-blank in the line linewise. To compute the new
2208// line number this formula is used:
2209// ({count} * number-of-lines + 99) / 100
2210//
2211// https://neovim.io/doc/user/motion.html#N%25
2212fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2213 let total_lines = map.buffer_snapshot.max_point().row + 1;
2214 let target_line = (count * total_lines as usize + 99) / 100;
2215 let target_point = DisplayPoint::new(
2216 DisplayRow(target_line.saturating_sub(1) as u32),
2217 point.column(),
2218 );
2219 map.clip_point(target_point, Bias::Left)
2220}
2221
2222fn unmatched_forward(
2223 map: &DisplaySnapshot,
2224 mut display_point: DisplayPoint,
2225 char: char,
2226 times: usize,
2227) -> DisplayPoint {
2228 for _ in 0..times {
2229 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2230 let point = display_point.to_point(map);
2231 let offset = point.to_offset(&map.buffer_snapshot);
2232
2233 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2234 let Some(ranges) = ranges else { break };
2235 let mut closest_closing_destination = None;
2236 let mut closest_distance = usize::MAX;
2237
2238 for (_, close_range) in ranges {
2239 if close_range.start > offset {
2240 let mut chars = map.buffer_snapshot.chars_at(close_range.start);
2241 if Some(char) == chars.next() {
2242 let distance = close_range.start - offset;
2243 if distance < closest_distance {
2244 closest_closing_destination = Some(close_range.start);
2245 closest_distance = distance;
2246 continue;
2247 }
2248 }
2249 }
2250 }
2251
2252 let new_point = closest_closing_destination
2253 .map(|destination| destination.to_display_point(map))
2254 .unwrap_or(display_point);
2255 if new_point == display_point {
2256 break;
2257 }
2258 display_point = new_point;
2259 }
2260 return display_point;
2261}
2262
2263fn unmatched_backward(
2264 map: &DisplaySnapshot,
2265 mut display_point: DisplayPoint,
2266 char: char,
2267 times: usize,
2268) -> DisplayPoint {
2269 for _ in 0..times {
2270 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2271 let point = display_point.to_point(map);
2272 let offset = point.to_offset(&map.buffer_snapshot);
2273
2274 let ranges = map.buffer_snapshot.enclosing_bracket_ranges(point..point);
2275 let Some(ranges) = ranges else {
2276 break;
2277 };
2278
2279 let mut closest_starting_destination = None;
2280 let mut closest_distance = usize::MAX;
2281
2282 for (start_range, _) in ranges {
2283 if start_range.start < offset {
2284 let mut chars = map.buffer_snapshot.chars_at(start_range.start);
2285 if Some(char) == chars.next() {
2286 let distance = offset - start_range.start;
2287 if distance < closest_distance {
2288 closest_starting_destination = Some(start_range.start);
2289 closest_distance = distance;
2290 continue;
2291 }
2292 }
2293 }
2294 }
2295
2296 let new_point = closest_starting_destination
2297 .map(|destination| destination.to_display_point(map))
2298 .unwrap_or(display_point);
2299 if new_point == display_point {
2300 break;
2301 } else {
2302 display_point = new_point;
2303 }
2304 }
2305 display_point
2306}
2307
2308fn find_forward(
2309 map: &DisplaySnapshot,
2310 from: DisplayPoint,
2311 before: bool,
2312 target: char,
2313 times: usize,
2314 mode: FindRange,
2315 smartcase: bool,
2316) -> Option<DisplayPoint> {
2317 let mut to = from;
2318 let mut found = false;
2319
2320 for _ in 0..times {
2321 found = false;
2322 let new_to = find_boundary(map, to, mode, |_, right| {
2323 found = is_character_match(target, right, smartcase);
2324 found
2325 });
2326 if to == new_to {
2327 break;
2328 }
2329 to = new_to;
2330 }
2331
2332 if found {
2333 if before && to.column() > 0 {
2334 *to.column_mut() -= 1;
2335 Some(map.clip_point(to, Bias::Left))
2336 } else {
2337 Some(to)
2338 }
2339 } else {
2340 None
2341 }
2342}
2343
2344fn find_backward(
2345 map: &DisplaySnapshot,
2346 from: DisplayPoint,
2347 after: bool,
2348 target: char,
2349 times: usize,
2350 mode: FindRange,
2351 smartcase: bool,
2352) -> DisplayPoint {
2353 let mut to = from;
2354
2355 for _ in 0..times {
2356 let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| {
2357 is_character_match(target, right, smartcase)
2358 });
2359 if to == new_to {
2360 break;
2361 }
2362 to = new_to;
2363 }
2364
2365 let next = map.buffer_snapshot.chars_at(to.to_point(map)).next();
2366 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2367 if after {
2368 *to.column_mut() += 1;
2369 map.clip_point(to, Bias::Right)
2370 } else {
2371 to
2372 }
2373 } else {
2374 from
2375 }
2376}
2377
2378fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2379 if smartcase {
2380 if target.is_uppercase() {
2381 target == other
2382 } else {
2383 target == other.to_ascii_lowercase()
2384 }
2385 } else {
2386 target == other
2387 }
2388}
2389
2390fn sneak(
2391 map: &DisplaySnapshot,
2392 from: DisplayPoint,
2393 first_target: char,
2394 second_target: char,
2395 times: usize,
2396 smartcase: bool,
2397) -> Option<DisplayPoint> {
2398 let mut to = from;
2399 let mut found = false;
2400
2401 for _ in 0..times {
2402 found = false;
2403 let new_to = find_boundary(
2404 map,
2405 movement::right(map, to),
2406 FindRange::MultiLine,
2407 |left, right| {
2408 found = is_character_match(first_target, left, smartcase)
2409 && is_character_match(second_target, right, smartcase);
2410 found
2411 },
2412 );
2413 if to == new_to {
2414 break;
2415 }
2416 to = new_to;
2417 }
2418
2419 if found {
2420 Some(movement::left(map, to))
2421 } else {
2422 None
2423 }
2424}
2425
2426fn sneak_backward(
2427 map: &DisplaySnapshot,
2428 from: DisplayPoint,
2429 first_target: char,
2430 second_target: char,
2431 times: usize,
2432 smartcase: bool,
2433) -> Option<DisplayPoint> {
2434 let mut to = from;
2435 let mut found = false;
2436
2437 for _ in 0..times {
2438 found = false;
2439 let new_to =
2440 find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| {
2441 found = is_character_match(first_target, left, smartcase)
2442 && is_character_match(second_target, right, smartcase);
2443 found
2444 });
2445 if to == new_to {
2446 break;
2447 }
2448 to = new_to;
2449 }
2450
2451 if found {
2452 Some(movement::left(map, to))
2453 } else {
2454 None
2455 }
2456}
2457
2458fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2459 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
2460 first_non_whitespace(map, false, correct_line)
2461}
2462
2463fn previous_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 go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
2469 let correct_line = start_of_relative_buffer_row(map, point, 0);
2470 right(map, correct_line, times.saturating_sub(1))
2471}
2472
2473pub(crate) fn next_line_end(
2474 map: &DisplaySnapshot,
2475 mut point: DisplayPoint,
2476 times: usize,
2477) -> DisplayPoint {
2478 if times > 1 {
2479 point = start_of_relative_buffer_row(map, point, times as isize - 1);
2480 }
2481 end_of_line(map, false, point, 1)
2482}
2483
2484fn window_top(
2485 map: &DisplaySnapshot,
2486 point: DisplayPoint,
2487 text_layout_details: &TextLayoutDetails,
2488 mut times: usize,
2489) -> (DisplayPoint, SelectionGoal) {
2490 let first_visible_line = text_layout_details
2491 .scroll_anchor
2492 .anchor
2493 .to_display_point(map);
2494
2495 if first_visible_line.row() != DisplayRow(0)
2496 && text_layout_details.vertical_scroll_margin as usize > times
2497 {
2498 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2499 }
2500
2501 if let Some(visible_rows) = text_layout_details.visible_rows {
2502 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
2503 let new_row = (first_visible_line.row().0 + (times as u32))
2504 .min(bottom_row)
2505 .min(map.max_point().row().0);
2506 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2507
2508 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
2509 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2510 } else {
2511 let new_row =
2512 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
2513 let new_col = point.column().min(map.line_len(first_visible_line.row()));
2514
2515 let new_point = DisplayPoint::new(new_row, new_col);
2516 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2517 }
2518}
2519
2520fn window_middle(
2521 map: &DisplaySnapshot,
2522 point: DisplayPoint,
2523 text_layout_details: &TextLayoutDetails,
2524) -> (DisplayPoint, SelectionGoal) {
2525 if let Some(visible_rows) = text_layout_details.visible_rows {
2526 let first_visible_line = text_layout_details
2527 .scroll_anchor
2528 .anchor
2529 .to_display_point(map);
2530
2531 let max_visible_rows =
2532 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
2533
2534 let new_row =
2535 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
2536 let new_row = DisplayRow(new_row);
2537 let new_col = point.column().min(map.line_len(new_row));
2538 let new_point = DisplayPoint::new(new_row, new_col);
2539 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2540 } else {
2541 (point, SelectionGoal::None)
2542 }
2543}
2544
2545fn window_bottom(
2546 map: &DisplaySnapshot,
2547 point: DisplayPoint,
2548 text_layout_details: &TextLayoutDetails,
2549 mut times: usize,
2550) -> (DisplayPoint, SelectionGoal) {
2551 if let Some(visible_rows) = text_layout_details.visible_rows {
2552 let first_visible_line = text_layout_details
2553 .scroll_anchor
2554 .anchor
2555 .to_display_point(map);
2556 let bottom_row = first_visible_line.row().0
2557 + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32;
2558 if bottom_row < map.max_point().row().0
2559 && text_layout_details.vertical_scroll_margin as usize > times
2560 {
2561 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
2562 }
2563 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
2564 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
2565 {
2566 first_visible_line.row()
2567 } else {
2568 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
2569 };
2570 let new_col = point.column().min(map.line_len(new_row));
2571 let new_point = DisplayPoint::new(new_row, new_col);
2572 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
2573 } else {
2574 (point, SelectionGoal::None)
2575 }
2576}
2577
2578fn method_motion(
2579 map: &DisplaySnapshot,
2580 mut display_point: DisplayPoint,
2581 times: usize,
2582 direction: Direction,
2583 is_start: bool,
2584) -> DisplayPoint {
2585 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2586 return display_point;
2587 };
2588
2589 for _ in 0..times {
2590 let point = map.display_point_to_point(display_point, Bias::Left);
2591 let offset = point.to_offset(&map.buffer_snapshot);
2592 let range = if direction == Direction::Prev {
2593 0..offset
2594 } else {
2595 offset..buffer.len()
2596 };
2597
2598 let possibilities = buffer
2599 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
2600 .filter_map(|(range, object)| {
2601 if !matches!(object, language::TextObject::AroundFunction) {
2602 return None;
2603 }
2604
2605 let relevant = if is_start { range.start } else { range.end };
2606 if direction == Direction::Prev && relevant < offset {
2607 Some(relevant)
2608 } else if direction == Direction::Next && relevant > offset + 1 {
2609 Some(relevant)
2610 } else {
2611 None
2612 }
2613 });
2614
2615 let dest = if direction == Direction::Prev {
2616 possibilities.max().unwrap_or(offset)
2617 } else {
2618 possibilities.min().unwrap_or(offset)
2619 };
2620 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2621 if new_point == display_point {
2622 break;
2623 }
2624 display_point = new_point;
2625 }
2626 display_point
2627}
2628
2629fn comment_motion(
2630 map: &DisplaySnapshot,
2631 mut display_point: DisplayPoint,
2632 times: usize,
2633 direction: Direction,
2634) -> DisplayPoint {
2635 let Some((_, _, buffer)) = map.buffer_snapshot.as_singleton() else {
2636 return display_point;
2637 };
2638
2639 for _ in 0..times {
2640 let point = map.display_point_to_point(display_point, Bias::Left);
2641 let offset = point.to_offset(&map.buffer_snapshot);
2642 let range = if direction == Direction::Prev {
2643 0..offset
2644 } else {
2645 offset..buffer.len()
2646 };
2647
2648 let possibilities = buffer
2649 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
2650 .filter_map(|(range, object)| {
2651 if !matches!(object, language::TextObject::AroundComment) {
2652 return None;
2653 }
2654
2655 let relevant = if direction == Direction::Prev {
2656 range.start
2657 } else {
2658 range.end
2659 };
2660 if direction == Direction::Prev && relevant < offset {
2661 Some(relevant)
2662 } else if direction == Direction::Next && relevant > offset + 1 {
2663 Some(relevant)
2664 } else {
2665 None
2666 }
2667 });
2668
2669 let dest = if direction == Direction::Prev {
2670 possibilities.max().unwrap_or(offset)
2671 } else {
2672 possibilities.min().unwrap_or(offset)
2673 };
2674 let new_point = map.clip_point(dest.to_display_point(&map), Bias::Left);
2675 if new_point == display_point {
2676 break;
2677 }
2678 display_point = new_point;
2679 }
2680
2681 display_point
2682}
2683
2684fn section_motion(
2685 map: &DisplaySnapshot,
2686 mut display_point: DisplayPoint,
2687 times: usize,
2688 direction: Direction,
2689 is_start: bool,
2690) -> DisplayPoint {
2691 if map.buffer_snapshot.as_singleton().is_some() {
2692 for _ in 0..times {
2693 let offset = map
2694 .display_point_to_point(display_point, Bias::Left)
2695 .to_offset(&map.buffer_snapshot);
2696 let range = if direction == Direction::Prev {
2697 0..offset
2698 } else {
2699 offset..map.buffer_snapshot.len()
2700 };
2701
2702 // we set a max start depth here because we want a section to only be "top level"
2703 // similar to vim's default of '{' in the first column.
2704 // (and without it, ]] at the start of editor.rs is -very- slow)
2705 let mut possibilities = map
2706 .buffer_snapshot
2707 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
2708 .filter(|(_, object)| {
2709 matches!(
2710 object,
2711 language::TextObject::AroundClass | language::TextObject::AroundFunction
2712 )
2713 })
2714 .collect::<Vec<_>>();
2715 possibilities.sort_by_key(|(range_a, _)| range_a.start);
2716 let mut prev_end = None;
2717 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
2718 if t == language::TextObject::AroundFunction
2719 && prev_end.is_some_and(|prev_end| prev_end > range.start)
2720 {
2721 return None;
2722 }
2723 prev_end = Some(range.end);
2724
2725 let relevant = if is_start { range.start } else { range.end };
2726 if direction == Direction::Prev && relevant < offset {
2727 Some(relevant)
2728 } else if direction == Direction::Next && relevant > offset + 1 {
2729 Some(relevant)
2730 } else {
2731 None
2732 }
2733 });
2734
2735 let offset = if direction == Direction::Prev {
2736 possibilities.max().unwrap_or(0)
2737 } else {
2738 possibilities.min().unwrap_or(map.buffer_snapshot.len())
2739 };
2740
2741 let new_point = map.clip_point(offset.to_display_point(&map), Bias::Left);
2742 if new_point == display_point {
2743 break;
2744 }
2745 display_point = new_point;
2746 }
2747 return display_point;
2748 };
2749
2750 for _ in 0..times {
2751 let next_point = if is_start {
2752 movement::start_of_excerpt(map, display_point, direction)
2753 } else {
2754 movement::end_of_excerpt(map, display_point, direction)
2755 };
2756 if next_point == display_point {
2757 break;
2758 }
2759 display_point = next_point;
2760 }
2761
2762 display_point
2763}
2764
2765#[cfg(test)]
2766mod test {
2767
2768 use crate::{
2769 state::Mode,
2770 test::{NeovimBackedTestContext, VimTestContext},
2771 };
2772 use editor::display_map::Inlay;
2773 use indoc::indoc;
2774 use language::Point;
2775 use multi_buffer::MultiBufferRow;
2776
2777 #[gpui::test]
2778 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
2779 let mut cx = NeovimBackedTestContext::new(cx).await;
2780
2781 let initial_state = indoc! {r"ˇabc
2782 def
2783
2784 paragraph
2785 the second
2786
2787
2788
2789 third and
2790 final"};
2791
2792 // goes down once
2793 cx.set_shared_state(initial_state).await;
2794 cx.simulate_shared_keystrokes("}").await;
2795 cx.shared_state().await.assert_eq(indoc! {r"abc
2796 def
2797 ˇ
2798 paragraph
2799 the second
2800
2801
2802
2803 third and
2804 final"});
2805
2806 // goes up once
2807 cx.simulate_shared_keystrokes("{").await;
2808 cx.shared_state().await.assert_eq(initial_state);
2809
2810 // goes down twice
2811 cx.simulate_shared_keystrokes("2 }").await;
2812 cx.shared_state().await.assert_eq(indoc! {r"abc
2813 def
2814
2815 paragraph
2816 the second
2817 ˇ
2818
2819
2820 third and
2821 final"});
2822
2823 // goes down over multiple blanks
2824 cx.simulate_shared_keystrokes("}").await;
2825 cx.shared_state().await.assert_eq(indoc! {r"abc
2826 def
2827
2828 paragraph
2829 the second
2830
2831
2832
2833 third and
2834 finaˇl"});
2835
2836 // goes up twice
2837 cx.simulate_shared_keystrokes("2 {").await;
2838 cx.shared_state().await.assert_eq(indoc! {r"abc
2839 def
2840 ˇ
2841 paragraph
2842 the second
2843
2844
2845
2846 third and
2847 final"});
2848 }
2849
2850 #[gpui::test]
2851 async fn test_matching(cx: &mut gpui::TestAppContext) {
2852 let mut cx = NeovimBackedTestContext::new(cx).await;
2853
2854 cx.set_shared_state(indoc! {r"func ˇ(a string) {
2855 do(something(with<Types>.and_arrays[0, 2]))
2856 }"})
2857 .await;
2858 cx.simulate_shared_keystrokes("%").await;
2859 cx.shared_state()
2860 .await
2861 .assert_eq(indoc! {r"func (a stringˇ) {
2862 do(something(with<Types>.and_arrays[0, 2]))
2863 }"});
2864
2865 // test it works on the last character of the line
2866 cx.set_shared_state(indoc! {r"func (a string) ˇ{
2867 do(something(with<Types>.and_arrays[0, 2]))
2868 }"})
2869 .await;
2870 cx.simulate_shared_keystrokes("%").await;
2871 cx.shared_state()
2872 .await
2873 .assert_eq(indoc! {r"func (a string) {
2874 do(something(with<Types>.and_arrays[0, 2]))
2875 ˇ}"});
2876
2877 // test it works on immediate nesting
2878 cx.set_shared_state("ˇ{()}").await;
2879 cx.simulate_shared_keystrokes("%").await;
2880 cx.shared_state().await.assert_eq("{()ˇ}");
2881 cx.simulate_shared_keystrokes("%").await;
2882 cx.shared_state().await.assert_eq("ˇ{()}");
2883
2884 // test it works on immediate nesting inside braces
2885 cx.set_shared_state("{\n ˇ{()}\n}").await;
2886 cx.simulate_shared_keystrokes("%").await;
2887 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
2888
2889 // test it jumps to the next paren on a line
2890 cx.set_shared_state("func ˇboop() {\n}").await;
2891 cx.simulate_shared_keystrokes("%").await;
2892 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
2893 }
2894
2895 #[gpui::test]
2896 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
2897 let mut cx = NeovimBackedTestContext::new(cx).await;
2898
2899 // test it works with curly braces
2900 cx.set_shared_state(indoc! {r"func (a string) {
2901 do(something(with<Types>.anˇd_arrays[0, 2]))
2902 }"})
2903 .await;
2904 cx.simulate_shared_keystrokes("] }").await;
2905 cx.shared_state()
2906 .await
2907 .assert_eq(indoc! {r"func (a string) {
2908 do(something(with<Types>.and_arrays[0, 2]))
2909 ˇ}"});
2910
2911 // test it works with brackets
2912 cx.set_shared_state(indoc! {r"func (a string) {
2913 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2914 }"})
2915 .await;
2916 cx.simulate_shared_keystrokes("] )").await;
2917 cx.shared_state()
2918 .await
2919 .assert_eq(indoc! {r"func (a string) {
2920 do(something(with<Types>.and_arrays[0, 2])ˇ)
2921 }"});
2922
2923 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
2924 .await;
2925 cx.simulate_shared_keystrokes("] )").await;
2926 cx.shared_state()
2927 .await
2928 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
2929
2930 // test it works on immediate nesting
2931 cx.set_shared_state("{ˇ {}{}}").await;
2932 cx.simulate_shared_keystrokes("] }").await;
2933 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
2934 cx.set_shared_state("(ˇ ()())").await;
2935 cx.simulate_shared_keystrokes("] )").await;
2936 cx.shared_state().await.assert_eq("( ()()ˇ)");
2937
2938 // test it works on immediate nesting inside braces
2939 cx.set_shared_state("{\n ˇ {()}\n}").await;
2940 cx.simulate_shared_keystrokes("] }").await;
2941 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
2942 cx.set_shared_state("(\n ˇ {()}\n)").await;
2943 cx.simulate_shared_keystrokes("] )").await;
2944 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
2945 }
2946
2947 #[gpui::test]
2948 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
2949 let mut cx = NeovimBackedTestContext::new(cx).await;
2950
2951 // test it works with curly braces
2952 cx.set_shared_state(indoc! {r"func (a string) {
2953 do(something(with<Types>.anˇd_arrays[0, 2]))
2954 }"})
2955 .await;
2956 cx.simulate_shared_keystrokes("[ {").await;
2957 cx.shared_state()
2958 .await
2959 .assert_eq(indoc! {r"func (a string) ˇ{
2960 do(something(with<Types>.and_arrays[0, 2]))
2961 }"});
2962
2963 // test it works with brackets
2964 cx.set_shared_state(indoc! {r"func (a string) {
2965 do(somethiˇng(with<Types>.and_arrays[0, 2]))
2966 }"})
2967 .await;
2968 cx.simulate_shared_keystrokes("[ (").await;
2969 cx.shared_state()
2970 .await
2971 .assert_eq(indoc! {r"func (a string) {
2972 doˇ(something(with<Types>.and_arrays[0, 2]))
2973 }"});
2974
2975 // test it works on immediate nesting
2976 cx.set_shared_state("{{}{} ˇ }").await;
2977 cx.simulate_shared_keystrokes("[ {").await;
2978 cx.shared_state().await.assert_eq("ˇ{{}{} }");
2979 cx.set_shared_state("(()() ˇ )").await;
2980 cx.simulate_shared_keystrokes("[ (").await;
2981 cx.shared_state().await.assert_eq("ˇ(()() )");
2982
2983 // test it works on immediate nesting inside braces
2984 cx.set_shared_state("{\n {()} ˇ\n}").await;
2985 cx.simulate_shared_keystrokes("[ {").await;
2986 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
2987 cx.set_shared_state("(\n {()} ˇ\n)").await;
2988 cx.simulate_shared_keystrokes("[ (").await;
2989 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
2990 }
2991
2992 #[gpui::test]
2993 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
2994 let mut cx = NeovimBackedTestContext::new_html(cx).await;
2995
2996 cx.neovim.exec("set filetype=html").await;
2997
2998 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
2999 cx.simulate_shared_keystrokes("%").await;
3000 cx.shared_state()
3001 .await
3002 .assert_eq(indoc! {r"<body><ˇ/body>"});
3003 cx.simulate_shared_keystrokes("%").await;
3004
3005 // test jumping backwards
3006 cx.shared_state()
3007 .await
3008 .assert_eq(indoc! {r"<ˇbody></body>"});
3009
3010 // test self-closing tags
3011 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
3012 cx.simulate_shared_keystrokes("%").await;
3013 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
3014
3015 // test tag with attributes
3016 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
3017 </div>
3018 "})
3019 .await;
3020 cx.simulate_shared_keystrokes("%").await;
3021 cx.shared_state()
3022 .await
3023 .assert_eq(indoc! {r"<div class='test' id='main'>
3024 <ˇ/div>
3025 "});
3026
3027 // test multi-line self-closing tag
3028 cx.set_shared_state(indoc! {r#"<a>
3029 <br
3030 test = "test"
3031 /ˇ>
3032 </a>"#})
3033 .await;
3034 cx.simulate_shared_keystrokes("%").await;
3035 cx.shared_state().await.assert_eq(indoc! {r#"<a>
3036 ˇ<br
3037 test = "test"
3038 />
3039 </a>"#});
3040 }
3041
3042 #[gpui::test]
3043 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
3044 let mut cx = NeovimBackedTestContext::new(cx).await;
3045
3046 // f and F
3047 cx.set_shared_state("ˇone two three four").await;
3048 cx.simulate_shared_keystrokes("f o").await;
3049 cx.shared_state().await.assert_eq("one twˇo three four");
3050 cx.simulate_shared_keystrokes(",").await;
3051 cx.shared_state().await.assert_eq("ˇone two three four");
3052 cx.simulate_shared_keystrokes("2 ;").await;
3053 cx.shared_state().await.assert_eq("one two three fˇour");
3054 cx.simulate_shared_keystrokes("shift-f e").await;
3055 cx.shared_state().await.assert_eq("one two threˇe four");
3056 cx.simulate_shared_keystrokes("2 ;").await;
3057 cx.shared_state().await.assert_eq("onˇe two three four");
3058 cx.simulate_shared_keystrokes(",").await;
3059 cx.shared_state().await.assert_eq("one two thrˇee four");
3060
3061 // t and T
3062 cx.set_shared_state("ˇone two three four").await;
3063 cx.simulate_shared_keystrokes("t o").await;
3064 cx.shared_state().await.assert_eq("one tˇwo three four");
3065 cx.simulate_shared_keystrokes(",").await;
3066 cx.shared_state().await.assert_eq("oˇne two three four");
3067 cx.simulate_shared_keystrokes("2 ;").await;
3068 cx.shared_state().await.assert_eq("one two three ˇfour");
3069 cx.simulate_shared_keystrokes("shift-t e").await;
3070 cx.shared_state().await.assert_eq("one two threeˇ four");
3071 cx.simulate_shared_keystrokes("3 ;").await;
3072 cx.shared_state().await.assert_eq("oneˇ two three four");
3073 cx.simulate_shared_keystrokes(",").await;
3074 cx.shared_state().await.assert_eq("one two thˇree four");
3075 }
3076
3077 #[gpui::test]
3078 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
3079 let mut cx = NeovimBackedTestContext::new(cx).await;
3080 let initial_state = indoc! {r"something(ˇfoo)"};
3081 cx.set_shared_state(initial_state).await;
3082 cx.simulate_shared_keystrokes("}").await;
3083 cx.shared_state().await.assert_eq("something(fooˇ)");
3084 }
3085
3086 #[gpui::test]
3087 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
3088 let mut cx = NeovimBackedTestContext::new(cx).await;
3089 cx.set_shared_state("ˇone\n two\nthree").await;
3090 cx.simulate_shared_keystrokes("enter").await;
3091 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
3092 }
3093
3094 #[gpui::test]
3095 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
3096 let mut cx = NeovimBackedTestContext::new(cx).await;
3097 cx.set_shared_state("ˇ one\n two \nthree").await;
3098 cx.simulate_shared_keystrokes("g _").await;
3099 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
3100
3101 cx.set_shared_state("ˇ one \n two \nthree").await;
3102 cx.simulate_shared_keystrokes("g _").await;
3103 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
3104 cx.simulate_shared_keystrokes("2 g _").await;
3105 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
3106 }
3107
3108 #[gpui::test]
3109 async fn test_window_top(cx: &mut gpui::TestAppContext) {
3110 let mut cx = NeovimBackedTestContext::new(cx).await;
3111 let initial_state = indoc! {r"abc
3112 def
3113 paragraph
3114 the second
3115 third ˇand
3116 final"};
3117
3118 cx.set_shared_state(initial_state).await;
3119 cx.simulate_shared_keystrokes("shift-h").await;
3120 cx.shared_state().await.assert_eq(indoc! {r"abˇc
3121 def
3122 paragraph
3123 the second
3124 third and
3125 final"});
3126
3127 // clip point
3128 cx.set_shared_state(indoc! {r"
3129 1 2 3
3130 4 5 6
3131 7 8 ˇ9
3132 "})
3133 .await;
3134 cx.simulate_shared_keystrokes("shift-h").await;
3135 cx.shared_state().await.assert_eq(indoc! {"
3136 1 2 ˇ3
3137 4 5 6
3138 7 8 9
3139 "});
3140
3141 cx.set_shared_state(indoc! {r"
3142 1 2 3
3143 4 5 6
3144 ˇ7 8 9
3145 "})
3146 .await;
3147 cx.simulate_shared_keystrokes("shift-h").await;
3148 cx.shared_state().await.assert_eq(indoc! {"
3149 ˇ1 2 3
3150 4 5 6
3151 7 8 9
3152 "});
3153
3154 cx.set_shared_state(indoc! {r"
3155 1 2 3
3156 4 5 ˇ6
3157 7 8 9"})
3158 .await;
3159 cx.simulate_shared_keystrokes("9 shift-h").await;
3160 cx.shared_state().await.assert_eq(indoc! {"
3161 1 2 3
3162 4 5 6
3163 7 8 ˇ9"});
3164 }
3165
3166 #[gpui::test]
3167 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
3168 let mut cx = NeovimBackedTestContext::new(cx).await;
3169 let initial_state = indoc! {r"abˇc
3170 def
3171 paragraph
3172 the second
3173 third and
3174 final"};
3175
3176 cx.set_shared_state(initial_state).await;
3177 cx.simulate_shared_keystrokes("shift-m").await;
3178 cx.shared_state().await.assert_eq(indoc! {r"abc
3179 def
3180 paˇragraph
3181 the second
3182 third and
3183 final"});
3184
3185 cx.set_shared_state(indoc! {r"
3186 1 2 3
3187 4 5 6
3188 7 8 ˇ9
3189 "})
3190 .await;
3191 cx.simulate_shared_keystrokes("shift-m").await;
3192 cx.shared_state().await.assert_eq(indoc! {"
3193 1 2 3
3194 4 5 ˇ6
3195 7 8 9
3196 "});
3197 cx.set_shared_state(indoc! {r"
3198 1 2 3
3199 4 5 6
3200 ˇ7 8 9
3201 "})
3202 .await;
3203 cx.simulate_shared_keystrokes("shift-m").await;
3204 cx.shared_state().await.assert_eq(indoc! {"
3205 1 2 3
3206 ˇ4 5 6
3207 7 8 9
3208 "});
3209 cx.set_shared_state(indoc! {r"
3210 ˇ1 2 3
3211 4 5 6
3212 7 8 9
3213 "})
3214 .await;
3215 cx.simulate_shared_keystrokes("shift-m").await;
3216 cx.shared_state().await.assert_eq(indoc! {"
3217 1 2 3
3218 ˇ4 5 6
3219 7 8 9
3220 "});
3221 cx.set_shared_state(indoc! {r"
3222 1 2 3
3223 ˇ4 5 6
3224 7 8 9
3225 "})
3226 .await;
3227 cx.simulate_shared_keystrokes("shift-m").await;
3228 cx.shared_state().await.assert_eq(indoc! {"
3229 1 2 3
3230 ˇ4 5 6
3231 7 8 9
3232 "});
3233 cx.set_shared_state(indoc! {r"
3234 1 2 3
3235 4 5 ˇ6
3236 7 8 9
3237 "})
3238 .await;
3239 cx.simulate_shared_keystrokes("shift-m").await;
3240 cx.shared_state().await.assert_eq(indoc! {"
3241 1 2 3
3242 4 5 ˇ6
3243 7 8 9
3244 "});
3245 }
3246
3247 #[gpui::test]
3248 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
3249 let mut cx = NeovimBackedTestContext::new(cx).await;
3250 let initial_state = indoc! {r"abc
3251 deˇf
3252 paragraph
3253 the second
3254 third and
3255 final"};
3256
3257 cx.set_shared_state(initial_state).await;
3258 cx.simulate_shared_keystrokes("shift-l").await;
3259 cx.shared_state().await.assert_eq(indoc! {r"abc
3260 def
3261 paragraph
3262 the second
3263 third and
3264 fiˇnal"});
3265
3266 cx.set_shared_state(indoc! {r"
3267 1 2 3
3268 4 5 ˇ6
3269 7 8 9
3270 "})
3271 .await;
3272 cx.simulate_shared_keystrokes("shift-l").await;
3273 cx.shared_state().await.assert_eq(indoc! {"
3274 1 2 3
3275 4 5 6
3276 7 8 9
3277 ˇ"});
3278
3279 cx.set_shared_state(indoc! {r"
3280 1 2 3
3281 ˇ4 5 6
3282 7 8 9
3283 "})
3284 .await;
3285 cx.simulate_shared_keystrokes("shift-l").await;
3286 cx.shared_state().await.assert_eq(indoc! {"
3287 1 2 3
3288 4 5 6
3289 7 8 9
3290 ˇ"});
3291
3292 cx.set_shared_state(indoc! {r"
3293 1 2 ˇ3
3294 4 5 6
3295 7 8 9
3296 "})
3297 .await;
3298 cx.simulate_shared_keystrokes("shift-l").await;
3299 cx.shared_state().await.assert_eq(indoc! {"
3300 1 2 3
3301 4 5 6
3302 7 8 9
3303 ˇ"});
3304
3305 cx.set_shared_state(indoc! {r"
3306 ˇ1 2 3
3307 4 5 6
3308 7 8 9
3309 "})
3310 .await;
3311 cx.simulate_shared_keystrokes("shift-l").await;
3312 cx.shared_state().await.assert_eq(indoc! {"
3313 1 2 3
3314 4 5 6
3315 7 8 9
3316 ˇ"});
3317
3318 cx.set_shared_state(indoc! {r"
3319 1 2 3
3320 4 5 ˇ6
3321 7 8 9
3322 "})
3323 .await;
3324 cx.simulate_shared_keystrokes("9 shift-l").await;
3325 cx.shared_state().await.assert_eq(indoc! {"
3326 1 2 ˇ3
3327 4 5 6
3328 7 8 9
3329 "});
3330 }
3331
3332 #[gpui::test]
3333 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
3334 let mut cx = NeovimBackedTestContext::new(cx).await;
3335 cx.set_shared_state(indoc! {r"
3336 456 5ˇ67 678
3337 "})
3338 .await;
3339 cx.simulate_shared_keystrokes("g e").await;
3340 cx.shared_state().await.assert_eq(indoc! {"
3341 45ˇ6 567 678
3342 "});
3343
3344 // Test times
3345 cx.set_shared_state(indoc! {r"
3346 123 234 345
3347 456 5ˇ67 678
3348 "})
3349 .await;
3350 cx.simulate_shared_keystrokes("4 g e").await;
3351 cx.shared_state().await.assert_eq(indoc! {"
3352 12ˇ3 234 345
3353 456 567 678
3354 "});
3355
3356 // With punctuation
3357 cx.set_shared_state(indoc! {r"
3358 123 234 345
3359 4;5.6 5ˇ67 678
3360 789 890 901
3361 "})
3362 .await;
3363 cx.simulate_shared_keystrokes("g e").await;
3364 cx.shared_state().await.assert_eq(indoc! {"
3365 123 234 345
3366 4;5.ˇ6 567 678
3367 789 890 901
3368 "});
3369
3370 // With punctuation and count
3371 cx.set_shared_state(indoc! {r"
3372 123 234 345
3373 4;5.6 5ˇ67 678
3374 789 890 901
3375 "})
3376 .await;
3377 cx.simulate_shared_keystrokes("5 g e").await;
3378 cx.shared_state().await.assert_eq(indoc! {"
3379 123 234 345
3380 ˇ4;5.6 567 678
3381 789 890 901
3382 "});
3383
3384 // newlines
3385 cx.set_shared_state(indoc! {r"
3386 123 234 345
3387
3388 78ˇ9 890 901
3389 "})
3390 .await;
3391 cx.simulate_shared_keystrokes("g e").await;
3392 cx.shared_state().await.assert_eq(indoc! {"
3393 123 234 345
3394 ˇ
3395 789 890 901
3396 "});
3397 cx.simulate_shared_keystrokes("g e").await;
3398 cx.shared_state().await.assert_eq(indoc! {"
3399 123 234 34ˇ5
3400
3401 789 890 901
3402 "});
3403
3404 // With punctuation
3405 cx.set_shared_state(indoc! {r"
3406 123 234 345
3407 4;5.ˇ6 567 678
3408 789 890 901
3409 "})
3410 .await;
3411 cx.simulate_shared_keystrokes("g shift-e").await;
3412 cx.shared_state().await.assert_eq(indoc! {"
3413 123 234 34ˇ5
3414 4;5.6 567 678
3415 789 890 901
3416 "});
3417 }
3418
3419 #[gpui::test]
3420 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
3421 let mut cx = NeovimBackedTestContext::new(cx).await;
3422
3423 cx.set_shared_state(indoc! {"
3424 fn aˇ() {
3425 return
3426 }
3427 "})
3428 .await;
3429 cx.simulate_shared_keystrokes("v $ %").await;
3430 cx.shared_state().await.assert_eq(indoc! {"
3431 fn a«() {
3432 return
3433 }ˇ»
3434 "});
3435 }
3436
3437 #[gpui::test]
3438 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
3439 let mut cx = VimTestContext::new(cx, true).await;
3440
3441 cx.set_state(
3442 indoc! {"
3443 struct Foo {
3444 ˇ
3445 }
3446 "},
3447 Mode::Normal,
3448 );
3449
3450 cx.update_editor(|editor, _window, cx| {
3451 let range = editor.selections.newest_anchor().range();
3452 let inlay_text = " field: int,\n field2: string\n field3: float";
3453 let inlay = Inlay::inline_completion(1, range.start, inlay_text);
3454 editor.splice_inlays(&[], vec![inlay], cx);
3455 });
3456
3457 cx.simulate_keystrokes("j");
3458 cx.assert_state(
3459 indoc! {"
3460 struct Foo {
3461
3462 ˇ}
3463 "},
3464 Mode::Normal,
3465 );
3466 }
3467
3468 #[gpui::test]
3469 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
3470 let mut cx = VimTestContext::new(cx, true).await;
3471
3472 cx.set_state(
3473 indoc! {"
3474 ˇstruct Foo {
3475
3476 }
3477 "},
3478 Mode::Normal,
3479 );
3480 cx.update_editor(|editor, _window, cx| {
3481 let snapshot = editor.buffer().read(cx).snapshot(cx);
3482 let end_of_line =
3483 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
3484 let inlay_text = " hint";
3485 let inlay = Inlay::inline_completion(1, end_of_line, inlay_text);
3486 editor.splice_inlays(&[], vec![inlay], cx);
3487 });
3488 cx.simulate_keystrokes("$");
3489 cx.assert_state(
3490 indoc! {"
3491 struct Foo ˇ{
3492
3493 }
3494 "},
3495 Mode::Normal,
3496 );
3497 }
3498
3499 #[gpui::test]
3500 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
3501 let mut cx = NeovimBackedTestContext::new(cx).await;
3502 // Normal mode
3503 cx.set_shared_state(indoc! {"
3504 The ˇquick brown
3505 fox jumps over
3506 the lazy dog
3507 The quick brown
3508 fox jumps over
3509 the lazy dog
3510 The quick brown
3511 fox jumps over
3512 the lazy dog"})
3513 .await;
3514 cx.simulate_shared_keystrokes("2 0 %").await;
3515 cx.shared_state().await.assert_eq(indoc! {"
3516 The quick brown
3517 fox ˇjumps over
3518 the lazy dog
3519 The quick brown
3520 fox jumps over
3521 the lazy dog
3522 The quick brown
3523 fox jumps over
3524 the lazy dog"});
3525
3526 cx.simulate_shared_keystrokes("2 5 %").await;
3527 cx.shared_state().await.assert_eq(indoc! {"
3528 The quick brown
3529 fox jumps over
3530 the ˇlazy dog
3531 The quick brown
3532 fox jumps over
3533 the lazy dog
3534 The quick brown
3535 fox jumps over
3536 the lazy dog"});
3537
3538 cx.simulate_shared_keystrokes("7 5 %").await;
3539 cx.shared_state().await.assert_eq(indoc! {"
3540 The quick brown
3541 fox jumps over
3542 the lazy dog
3543 The quick brown
3544 fox jumps over
3545 the lazy dog
3546 The ˇquick brown
3547 fox jumps over
3548 the lazy dog"});
3549
3550 // Visual mode
3551 cx.set_shared_state(indoc! {"
3552 The ˇquick brown
3553 fox jumps over
3554 the lazy dog
3555 The quick brown
3556 fox jumps over
3557 the lazy dog
3558 The quick brown
3559 fox jumps over
3560 the lazy dog"})
3561 .await;
3562 cx.simulate_shared_keystrokes("v 5 0 %").await;
3563 cx.shared_state().await.assert_eq(indoc! {"
3564 The «quick brown
3565 fox jumps over
3566 the lazy dog
3567 The quick brown
3568 fox jˇ»umps over
3569 the lazy dog
3570 The quick brown
3571 fox jumps over
3572 the lazy dog"});
3573
3574 cx.set_shared_state(indoc! {"
3575 The ˇquick brown
3576 fox jumps over
3577 the lazy dog
3578 The quick brown
3579 fox jumps over
3580 the lazy dog
3581 The quick brown
3582 fox jumps over
3583 the lazy dog"})
3584 .await;
3585 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
3586 cx.shared_state().await.assert_eq(indoc! {"
3587 The «quick brown
3588 fox jumps over
3589 the lazy dog
3590 The quick brown
3591 fox jumps over
3592 the lazy dog
3593 The quick brown
3594 fox jumps over
3595 the lˇ»azy dog"});
3596 }
3597}