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