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