Skip to repository content5695 lines · 186.9 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:42:09.549Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
motion.rs
1use editor::{
2 Anchor, Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, RowExt, ToOffset,
3 ToPoint as _,
4 display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint},
5 movement::{
6 self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
7 },
8};
9use gpui::{Action, Context, Window, actions, px};
10use language::{CharKind, Point, Selection, SelectionGoal, TextObject, TreeSitterOptions};
11use multi_buffer::MultiBufferRow;
12use schemars::JsonSchema;
13use serde::Deserialize;
14use std::{f64, ops::Range};
15
16use workspace::searchable::Direction;
17
18use crate::{
19 Vim,
20 normal::mark,
21 state::{Mode, Operator},
22 surrounds::SurroundsType,
23};
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub(crate) enum MotionKind {
27 Linewise,
28 Exclusive,
29 Inclusive,
30}
31
32impl MotionKind {
33 pub(crate) fn for_mode(mode: Mode) -> Self {
34 match mode {
35 Mode::VisualLine => MotionKind::Linewise,
36 _ => MotionKind::Exclusive,
37 }
38 }
39
40 pub(crate) fn linewise(&self) -> bool {
41 matches!(self, MotionKind::Linewise)
42 }
43}
44
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub enum Motion {
47 Left,
48 WrappingLeft,
49 Down {
50 display_lines: bool,
51 },
52 Up {
53 display_lines: bool,
54 },
55 Right,
56 WrappingRight,
57 NextWordStart {
58 ignore_punctuation: bool,
59 },
60 NextWordEnd {
61 ignore_punctuation: bool,
62 },
63 PreviousWordStart {
64 ignore_punctuation: bool,
65 },
66 PreviousWordEnd {
67 ignore_punctuation: bool,
68 },
69 NextSubwordStart {
70 ignore_punctuation: bool,
71 },
72 NextSubwordEnd {
73 ignore_punctuation: bool,
74 },
75 PreviousSubwordStart {
76 ignore_punctuation: bool,
77 },
78 PreviousSubwordEnd {
79 ignore_punctuation: bool,
80 },
81 FirstNonWhitespace {
82 display_lines: bool,
83 },
84 CurrentLine,
85 StartOfLine {
86 display_lines: bool,
87 },
88 MiddleOfLine {
89 display_lines: bool,
90 },
91 EndOfLine {
92 display_lines: bool,
93 },
94 SentenceBackward,
95 SentenceForward,
96 StartOfParagraph,
97 EndOfParagraph,
98 StartOfDocument,
99 EndOfDocument,
100 Matching {
101 match_quotes: bool,
102 },
103 GoToPercentage,
104 UnmatchedForward {
105 char: char,
106 },
107 UnmatchedBackward {
108 char: char,
109 },
110 FindForward {
111 before: bool,
112 char: char,
113 mode: FindRange,
114 smartcase: bool,
115 },
116 FindBackward {
117 after: bool,
118 char: char,
119 mode: FindRange,
120 smartcase: bool,
121 },
122 Sneak {
123 first_char: char,
124 second_char: char,
125 smartcase: bool,
126 },
127 SneakBackward {
128 first_char: char,
129 second_char: char,
130 smartcase: bool,
131 },
132 RepeatFind {
133 last_find: Box<Motion>,
134 },
135 RepeatFindReversed {
136 last_find: Box<Motion>,
137 },
138 NextLineStart,
139 PreviousLineStart,
140 StartOfLineDownward,
141 EndOfLineDownward,
142 GoToColumn,
143 WindowTop,
144 WindowMiddle,
145 WindowBottom,
146 NextSectionStart,
147 NextSectionEnd,
148 PreviousSectionStart,
149 PreviousSectionEnd,
150 NextMethodStart,
151 NextMethodEnd,
152 PreviousMethodStart,
153 PreviousMethodEnd,
154 NextComment,
155 PreviousComment,
156 PreviousLesserIndent,
157 PreviousGreaterIndent,
158 PreviousSameIndent,
159 NextLesserIndent,
160 NextGreaterIndent,
161 NextSameIndent,
162
163 // we don't have a good way to run a search synchronously, so
164 // we handle search motions by running the search async and then
165 // calling back into motion with this
166 ZedSearchResult {
167 prior_selections: Vec<Range<Anchor>>,
168 new_selections: Vec<Range<Anchor>>,
169 },
170 Jump {
171 anchor: Anchor,
172 line: bool,
173 },
174}
175
176#[derive(Clone, Copy)]
177enum IndentType {
178 Lesser,
179 Greater,
180 Same,
181}
182
183/// Moves to the start of the next word.
184#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
185#[action(namespace = vim)]
186#[serde(deny_unknown_fields)]
187struct NextWordStart {
188 #[serde(default)]
189 ignore_punctuation: bool,
190}
191
192/// Moves to the end of the next word.
193#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
194#[action(namespace = vim)]
195#[serde(deny_unknown_fields)]
196struct NextWordEnd {
197 #[serde(default)]
198 ignore_punctuation: bool,
199}
200
201/// Moves to the start of the previous word.
202#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
203#[action(namespace = vim)]
204#[serde(deny_unknown_fields)]
205struct PreviousWordStart {
206 #[serde(default)]
207 ignore_punctuation: bool,
208}
209
210/// Moves to the end of the previous word.
211#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
212#[action(namespace = vim)]
213#[serde(deny_unknown_fields)]
214struct PreviousWordEnd {
215 #[serde(default)]
216 ignore_punctuation: bool,
217}
218
219/// Moves to the start of the next subword.
220#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
221#[action(namespace = vim)]
222#[serde(deny_unknown_fields)]
223pub(crate) struct NextSubwordStart {
224 #[serde(default)]
225 pub(crate) ignore_punctuation: bool,
226}
227
228/// Moves to the end of the next subword.
229#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
230#[action(namespace = vim)]
231#[serde(deny_unknown_fields)]
232pub(crate) struct NextSubwordEnd {
233 #[serde(default)]
234 pub(crate) ignore_punctuation: bool,
235}
236
237/// Moves to the start of the previous subword.
238#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
239#[action(namespace = vim)]
240#[serde(deny_unknown_fields)]
241pub(crate) struct PreviousSubwordStart {
242 #[serde(default)]
243 pub(crate) ignore_punctuation: bool,
244}
245
246/// Moves to the end of the previous subword.
247#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
248#[action(namespace = vim)]
249#[serde(deny_unknown_fields)]
250pub(crate) struct PreviousSubwordEnd {
251 #[serde(default)]
252 pub(crate) ignore_punctuation: bool,
253}
254
255/// Moves cursor up by the specified number of lines.
256#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
257#[action(namespace = vim)]
258#[serde(deny_unknown_fields)]
259pub(crate) struct Up {
260 #[serde(default)]
261 pub(crate) display_lines: bool,
262}
263
264/// Moves cursor down by the specified number of lines.
265#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
266#[action(namespace = vim)]
267#[serde(deny_unknown_fields)]
268pub(crate) struct Down {
269 #[serde(default)]
270 pub(crate) display_lines: bool,
271}
272
273/// Moves to the first non-whitespace character on the current line.
274#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
275#[action(namespace = vim)]
276#[serde(deny_unknown_fields)]
277struct FirstNonWhitespace {
278 #[serde(default)]
279 display_lines: bool,
280}
281
282/// Moves to the matching bracket or delimiter.
283#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
284#[action(namespace = vim)]
285#[serde(deny_unknown_fields)]
286struct Matching {
287 #[serde(default)]
288 /// Whether to include quote characters (`'`, `"`, `` ` ``) when searching
289 /// for matching pairs. When `false`, only brackets and parentheses are
290 /// matched, which aligns with Neovim's default `%` behavior.
291 match_quotes: bool,
292}
293
294/// Moves to the end of the current line.
295#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
296#[action(namespace = vim)]
297#[serde(deny_unknown_fields)]
298struct EndOfLine {
299 #[serde(default)]
300 display_lines: bool,
301}
302
303/// Moves to the start of the current line.
304#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
305#[action(namespace = vim)]
306#[serde(deny_unknown_fields)]
307pub struct StartOfLine {
308 #[serde(default)]
309 pub(crate) display_lines: bool,
310}
311
312/// Moves to the middle of the current line.
313#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
314#[action(namespace = vim)]
315#[serde(deny_unknown_fields)]
316struct MiddleOfLine {
317 #[serde(default)]
318 display_lines: bool,
319}
320
321/// Finds the next unmatched bracket or delimiter.
322#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
323#[action(namespace = vim)]
324#[serde(deny_unknown_fields)]
325struct UnmatchedForward {
326 #[serde(default)]
327 char: char,
328}
329
330/// Finds the previous unmatched bracket or delimiter.
331#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
332#[action(namespace = vim)]
333#[serde(deny_unknown_fields)]
334struct UnmatchedBackward {
335 #[serde(default)]
336 char: char,
337}
338
339actions!(
340 vim,
341 [
342 /// Moves cursor left one character.
343 Left,
344 /// Moves cursor left one character, wrapping to previous line.
345 #[action(deprecated_aliases = ["vim::Backspace"])]
346 WrappingLeft,
347 /// Moves cursor right one character.
348 Right,
349 /// Moves cursor right one character, wrapping to next line.
350 #[action(deprecated_aliases = ["vim::Space"])]
351 WrappingRight,
352 /// Selects the current line.
353 CurrentLine,
354 /// Moves to the start of the next sentence.
355 SentenceForward,
356 /// Moves to the start of the previous sentence.
357 SentenceBackward,
358 /// Moves to the start of the paragraph.
359 StartOfParagraph,
360 /// Moves to the end of the paragraph.
361 EndOfParagraph,
362 /// Moves to the start of the document.
363 StartOfDocument,
364 /// Moves to the end of the document.
365 EndOfDocument,
366 /// Goes to a percentage position in the file.
367 GoToPercentage,
368 /// Moves to the start of the next line.
369 NextLineStart,
370 /// Moves to the start of the previous line.
371 PreviousLineStart,
372 /// Moves to the start of a line downward.
373 StartOfLineDownward,
374 /// Moves to the end of a line downward.
375 EndOfLineDownward,
376 /// Goes to a specific column number.
377 GoToColumn,
378 /// Repeats the last character find.
379 RepeatFind,
380 /// Repeats the last character find in reverse.
381 RepeatFindReversed,
382 /// Moves to the top of the window.
383 WindowTop,
384 /// Moves to the middle of the window.
385 WindowMiddle,
386 /// Moves to the bottom of the window.
387 WindowBottom,
388 /// Moves to the start of the next section.
389 NextSectionStart,
390 /// Moves to the end of the next section.
391 NextSectionEnd,
392 /// Moves to the start of the previous section.
393 PreviousSectionStart,
394 /// Moves to the end of the previous section.
395 PreviousSectionEnd,
396 /// Moves to the start of the next method.
397 NextMethodStart,
398 /// Moves to the end of the next method.
399 NextMethodEnd,
400 /// Moves to the start of the previous method.
401 PreviousMethodStart,
402 /// Moves to the end of the previous method.
403 PreviousMethodEnd,
404 /// Moves to the next comment.
405 NextComment,
406 /// Moves to the previous comment.
407 PreviousComment,
408 /// Moves to the previous line with lesser indentation.
409 PreviousLesserIndent,
410 /// Moves to the previous line with greater indentation.
411 PreviousGreaterIndent,
412 /// Moves to the previous line with the same indentation.
413 PreviousSameIndent,
414 /// Moves to the next line with lesser indentation.
415 NextLesserIndent,
416 /// Moves to the next line with greater indentation.
417 NextGreaterIndent,
418 /// Moves to the next line with the same indentation.
419 NextSameIndent,
420 ]
421);
422
423pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
424 Vim::action(editor, cx, |vim, _: &Left, window, cx| {
425 vim.motion(Motion::Left, window, cx)
426 });
427 Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| {
428 vim.motion(Motion::WrappingLeft, window, cx)
429 });
430 Vim::action(editor, cx, |vim, action: &Down, window, cx| {
431 vim.motion(
432 Motion::Down {
433 display_lines: action.display_lines,
434 },
435 window,
436 cx,
437 )
438 });
439 Vim::action(editor, cx, |vim, action: &Up, window, cx| {
440 vim.motion(
441 Motion::Up {
442 display_lines: action.display_lines,
443 },
444 window,
445 cx,
446 )
447 });
448 Vim::action(editor, cx, |vim, _: &Right, window, cx| {
449 vim.motion(Motion::Right, window, cx)
450 });
451 Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| {
452 vim.motion(Motion::WrappingRight, window, cx)
453 });
454 Vim::action(
455 editor,
456 cx,
457 |vim, action: &FirstNonWhitespace, window, cx| {
458 vim.motion(
459 Motion::FirstNonWhitespace {
460 display_lines: action.display_lines,
461 },
462 window,
463 cx,
464 )
465 },
466 );
467 Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| {
468 vim.motion(
469 Motion::StartOfLine {
470 display_lines: action.display_lines,
471 },
472 window,
473 cx,
474 )
475 });
476 Vim::action(editor, cx, |vim, action: &MiddleOfLine, window, cx| {
477 vim.motion(
478 Motion::MiddleOfLine {
479 display_lines: action.display_lines,
480 },
481 window,
482 cx,
483 )
484 });
485 Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| {
486 vim.motion(
487 Motion::EndOfLine {
488 display_lines: action.display_lines,
489 },
490 window,
491 cx,
492 )
493 });
494 Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| {
495 vim.motion(Motion::CurrentLine, window, cx)
496 });
497 Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| {
498 vim.motion(Motion::StartOfParagraph, window, cx)
499 });
500 Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| {
501 vim.motion(Motion::EndOfParagraph, window, cx)
502 });
503
504 Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| {
505 vim.motion(Motion::SentenceForward, window, cx)
506 });
507 Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| {
508 vim.motion(Motion::SentenceBackward, window, cx)
509 });
510 Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| {
511 vim.motion(Motion::StartOfDocument, window, cx)
512 });
513 Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| {
514 vim.motion(Motion::EndOfDocument, window, cx)
515 });
516 Vim::action(
517 editor,
518 cx,
519 |vim, &Matching { match_quotes }: &Matching, window, cx| {
520 vim.motion(Motion::Matching { match_quotes }, window, cx)
521 },
522 );
523
524 Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| {
525 vim.motion(Motion::GoToPercentage, window, cx)
526 });
527 Vim::action(
528 editor,
529 cx,
530 |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| {
531 vim.motion(Motion::UnmatchedForward { char }, window, cx)
532 },
533 );
534 Vim::action(
535 editor,
536 cx,
537 |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| {
538 vim.motion(Motion::UnmatchedBackward { char }, window, cx)
539 },
540 );
541 Vim::action(
542 editor,
543 cx,
544 |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| {
545 vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx)
546 },
547 );
548 Vim::action(
549 editor,
550 cx,
551 |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| {
552 vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx)
553 },
554 );
555 Vim::action(
556 editor,
557 cx,
558 |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| {
559 vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx)
560 },
561 );
562 Vim::action(
563 editor,
564 cx,
565 |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| {
566 vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx)
567 },
568 );
569 Vim::action(
570 editor,
571 cx,
572 |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| {
573 vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx)
574 },
575 );
576 Vim::action(
577 editor,
578 cx,
579 |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| {
580 vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx)
581 },
582 );
583 Vim::action(
584 editor,
585 cx,
586 |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| {
587 vim.motion(
588 Motion::PreviousSubwordStart { ignore_punctuation },
589 window,
590 cx,
591 )
592 },
593 );
594 Vim::action(
595 editor,
596 cx,
597 |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| {
598 vim.motion(
599 Motion::PreviousSubwordEnd { ignore_punctuation },
600 window,
601 cx,
602 )
603 },
604 );
605 Vim::action(editor, cx, |vim, &NextLineStart, window, cx| {
606 vim.motion(Motion::NextLineStart, window, cx)
607 });
608 Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| {
609 vim.motion(Motion::PreviousLineStart, window, cx)
610 });
611 Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| {
612 vim.motion(Motion::StartOfLineDownward, window, cx)
613 });
614 Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| {
615 vim.motion(Motion::EndOfLineDownward, window, cx)
616 });
617 Vim::action(editor, cx, |vim, &GoToColumn, window, cx| {
618 vim.motion(Motion::GoToColumn, window, cx)
619 });
620
621 Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| {
622 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
623 vim.motion(Motion::RepeatFind { last_find }, window, cx);
624 }
625 });
626
627 Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| {
628 if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) {
629 vim.motion(Motion::RepeatFindReversed { last_find }, window, cx);
630 }
631 });
632 Vim::action(editor, cx, |vim, &WindowTop, window, cx| {
633 vim.motion(Motion::WindowTop, window, cx)
634 });
635 Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| {
636 vim.motion(Motion::WindowMiddle, window, cx)
637 });
638 Vim::action(editor, cx, |vim, &WindowBottom, window, cx| {
639 vim.motion(Motion::WindowBottom, window, cx)
640 });
641
642 Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| {
643 vim.motion(Motion::PreviousSectionStart, window, cx)
644 });
645 Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| {
646 vim.motion(Motion::NextSectionStart, window, cx)
647 });
648 Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| {
649 vim.motion(Motion::PreviousSectionEnd, window, cx)
650 });
651 Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| {
652 vim.motion(Motion::NextSectionEnd, window, cx)
653 });
654 Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| {
655 vim.motion(Motion::PreviousMethodStart, window, cx)
656 });
657 Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| {
658 vim.motion(Motion::NextMethodStart, window, cx)
659 });
660 Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| {
661 vim.motion(Motion::PreviousMethodEnd, window, cx)
662 });
663 Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| {
664 vim.motion(Motion::NextMethodEnd, window, cx)
665 });
666 Vim::action(editor, cx, |vim, &NextComment, window, cx| {
667 vim.motion(Motion::NextComment, window, cx)
668 });
669 Vim::action(editor, cx, |vim, &PreviousComment, window, cx| {
670 vim.motion(Motion::PreviousComment, window, cx)
671 });
672 Vim::action(editor, cx, |vim, &PreviousLesserIndent, window, cx| {
673 vim.motion(Motion::PreviousLesserIndent, window, cx)
674 });
675 Vim::action(editor, cx, |vim, &PreviousGreaterIndent, window, cx| {
676 vim.motion(Motion::PreviousGreaterIndent, window, cx)
677 });
678 Vim::action(editor, cx, |vim, &PreviousSameIndent, window, cx| {
679 vim.motion(Motion::PreviousSameIndent, window, cx)
680 });
681 Vim::action(editor, cx, |vim, &NextLesserIndent, window, cx| {
682 vim.motion(Motion::NextLesserIndent, window, cx)
683 });
684 Vim::action(editor, cx, |vim, &NextGreaterIndent, window, cx| {
685 vim.motion(Motion::NextGreaterIndent, window, cx)
686 });
687 Vim::action(editor, cx, |vim, &NextSameIndent, window, cx| {
688 vim.motion(Motion::NextSameIndent, window, cx)
689 });
690}
691
692impl Vim {
693 pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context<Self>) {
694 if let Motion::ZedSearchResult {
695 prior_selections, ..
696 } = &m
697 {
698 match self.mode {
699 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
700 if !prior_selections.is_empty() {
701 self.update_editor(cx, |_, editor, cx| {
702 editor.change_selections(Default::default(), window, cx, |s| {
703 s.select_ranges(prior_selections.iter().cloned())
704 })
705 });
706 }
707 }
708 Mode::Normal | Mode::Replace | Mode::Insert => {
709 if self.active_operator().is_none() {
710 return;
711 }
712 }
713 Mode::HelixNormal | Mode::HelixSelect => {}
714 }
715 }
716
717 self.motion(m, window, cx)
718 }
719
720 pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context<Self>) {
721 if let Some(Operator::FindForward { .. })
722 | Some(Operator::Sneak { .. })
723 | Some(Operator::SneakBackward { .. })
724 | Some(Operator::FindBackward { .. }) = self.active_operator()
725 {
726 self.pop_operator(window, cx);
727 }
728
729 let count = Vim::take_count(cx);
730 let forced_motion = Vim::take_forced_motion(cx);
731 let active_operator = self.active_operator();
732 let mut waiting_operator: Option<Operator> = None;
733 match self.mode {
734 Mode::Normal | Mode::Replace | Mode::Insert => {
735 if active_operator == Some(Operator::AddSurrounds { target: None }) {
736 waiting_operator = Some(Operator::AddSurrounds {
737 target: Some(SurroundsType::Motion(motion)),
738 });
739 } else {
740 self.normal_motion(motion, active_operator, count, forced_motion, window, cx)
741 }
742 }
743 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
744 self.visual_motion(motion, count, window, cx)
745 }
746
747 Mode::HelixNormal => self.helix_normal_motion(motion, count, window, cx),
748 Mode::HelixSelect => self.helix_select_motion(motion, count, window, cx),
749 }
750 self.clear_operator(window, cx);
751 if let Some(operator) = waiting_operator {
752 self.push_operator(operator, window, cx);
753 Vim::globals(cx).pre_count = count
754 }
755 }
756}
757
758// Motion handling is specified here:
759// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
760impl Motion {
761 fn default_kind(&self) -> MotionKind {
762 use Motion::*;
763 match self {
764 Down { .. }
765 | Up { .. }
766 | StartOfDocument
767 | EndOfDocument
768 | CurrentLine
769 | NextLineStart
770 | PreviousLineStart
771 | StartOfLineDownward
772 | WindowTop
773 | WindowMiddle
774 | WindowBottom
775 | NextSectionStart
776 | NextSectionEnd
777 | PreviousSectionStart
778 | PreviousSectionEnd
779 | NextMethodStart
780 | NextMethodEnd
781 | PreviousMethodStart
782 | PreviousMethodEnd
783 | NextComment
784 | PreviousComment
785 | PreviousLesserIndent
786 | PreviousGreaterIndent
787 | PreviousSameIndent
788 | NextLesserIndent
789 | NextGreaterIndent
790 | NextSameIndent
791 | GoToPercentage
792 | Jump { line: true, .. } => MotionKind::Linewise,
793 EndOfLine { .. }
794 | EndOfLineDownward
795 | Matching { .. }
796 | FindForward { .. }
797 | NextWordEnd { .. }
798 | PreviousWordEnd { .. }
799 | NextSubwordEnd { .. }
800 | PreviousSubwordEnd { .. } => MotionKind::Inclusive,
801 Left
802 | WrappingLeft
803 | Right
804 | WrappingRight
805 | StartOfLine { .. }
806 | StartOfParagraph
807 | EndOfParagraph
808 | SentenceBackward
809 | SentenceForward
810 | GoToColumn
811 | MiddleOfLine { .. }
812 | UnmatchedForward { .. }
813 | UnmatchedBackward { .. }
814 | NextWordStart { .. }
815 | PreviousWordStart { .. }
816 | NextSubwordStart { .. }
817 | PreviousSubwordStart { .. }
818 | FirstNonWhitespace { .. }
819 | FindBackward { .. }
820 | Sneak { .. }
821 | SneakBackward { .. }
822 | Jump { .. }
823 | ZedSearchResult { .. } => MotionKind::Exclusive,
824 RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => {
825 motion.default_kind()
826 }
827 }
828 }
829
830 fn skip_exclusive_special_case(&self) -> bool {
831 matches!(self, Motion::WrappingLeft | Motion::WrappingRight)
832 }
833
834 pub(crate) fn push_to_jump_list(&self) -> bool {
835 use Motion::*;
836 match self {
837 CurrentLine
838 | Down { .. }
839 | EndOfLine { .. }
840 | EndOfLineDownward
841 | FindBackward { .. }
842 | FindForward { .. }
843 | FirstNonWhitespace { .. }
844 | GoToColumn
845 | Left
846 | MiddleOfLine { .. }
847 | NextLineStart
848 | NextSubwordEnd { .. }
849 | NextSubwordStart { .. }
850 | NextWordEnd { .. }
851 | NextWordStart { .. }
852 | PreviousLineStart
853 | PreviousSubwordEnd { .. }
854 | PreviousSubwordStart { .. }
855 | PreviousWordEnd { .. }
856 | PreviousWordStart { .. }
857 | RepeatFind { .. }
858 | RepeatFindReversed { .. }
859 | Right
860 | StartOfLine { .. }
861 | StartOfLineDownward
862 | Up { .. }
863 | WrappingLeft
864 | WrappingRight => false,
865 EndOfDocument
866 | EndOfParagraph
867 | GoToPercentage
868 | Jump { .. }
869 | Matching { .. }
870 | NextComment
871 | NextGreaterIndent
872 | NextLesserIndent
873 | NextMethodEnd
874 | NextMethodStart
875 | NextSameIndent
876 | NextSectionEnd
877 | NextSectionStart
878 | PreviousComment
879 | PreviousGreaterIndent
880 | PreviousLesserIndent
881 | PreviousMethodEnd
882 | PreviousMethodStart
883 | PreviousSameIndent
884 | PreviousSectionEnd
885 | PreviousSectionStart
886 | SentenceBackward
887 | SentenceForward
888 | Sneak { .. }
889 | SneakBackward { .. }
890 | StartOfDocument
891 | StartOfParagraph
892 | UnmatchedBackward { .. }
893 | UnmatchedForward { .. }
894 | WindowBottom
895 | WindowMiddle
896 | WindowTop
897 | ZedSearchResult { .. } => true,
898 }
899 }
900
901 pub fn infallible(&self) -> bool {
902 use Motion::*;
903 match self {
904 StartOfDocument | EndOfDocument | CurrentLine | EndOfLine { .. } => true,
905 Down { .. }
906 | Up { .. }
907 | MiddleOfLine { .. }
908 | Matching { .. }
909 | UnmatchedForward { .. }
910 | UnmatchedBackward { .. }
911 | FindForward { .. }
912 | RepeatFind { .. }
913 | Left
914 | WrappingLeft
915 | Right
916 | WrappingRight
917 | StartOfLine { .. }
918 | StartOfParagraph
919 | EndOfParagraph
920 | SentenceBackward
921 | SentenceForward
922 | StartOfLineDownward
923 | EndOfLineDownward
924 | GoToColumn
925 | GoToPercentage
926 | NextWordStart { .. }
927 | NextWordEnd { .. }
928 | PreviousWordStart { .. }
929 | PreviousWordEnd { .. }
930 | NextSubwordStart { .. }
931 | NextSubwordEnd { .. }
932 | PreviousSubwordStart { .. }
933 | PreviousSubwordEnd { .. }
934 | FirstNonWhitespace { .. }
935 | FindBackward { .. }
936 | Sneak { .. }
937 | SneakBackward { .. }
938 | RepeatFindReversed { .. }
939 | WindowTop
940 | WindowMiddle
941 | WindowBottom
942 | NextLineStart
943 | PreviousLineStart
944 | ZedSearchResult { .. }
945 | NextSectionStart
946 | NextSectionEnd
947 | PreviousSectionStart
948 | PreviousSectionEnd
949 | NextMethodStart
950 | NextMethodEnd
951 | PreviousMethodStart
952 | PreviousMethodEnd
953 | NextComment
954 | PreviousComment
955 | PreviousLesserIndent
956 | PreviousGreaterIndent
957 | PreviousSameIndent
958 | NextLesserIndent
959 | NextGreaterIndent
960 | NextSameIndent
961 | Jump { .. } => false,
962 }
963 }
964
965 pub fn move_point(
966 &self,
967 map: &DisplaySnapshot,
968 point: DisplayPoint,
969 goal: SelectionGoal,
970 maybe_times: Option<usize>,
971 text_layout_details: &TextLayoutDetails,
972 ) -> Option<(DisplayPoint, SelectionGoal)> {
973 let times = maybe_times.unwrap_or(1);
974 use Motion::*;
975 let infallible = self.infallible();
976 let (new_point, goal) = match self {
977 Left => (left(map, point, times), SelectionGoal::None),
978 WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None),
979 Down {
980 display_lines: false,
981 } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details),
982 Down {
983 display_lines: true,
984 } => down_display(map, point, goal, times, text_layout_details),
985 Up {
986 display_lines: false,
987 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details),
988 Up {
989 display_lines: true,
990 } => up_display(map, point, goal, times, text_layout_details),
991 Right => (right(map, point, times), SelectionGoal::None),
992 WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None),
993 NextWordStart { ignore_punctuation } => (
994 next_word_start(map, point, *ignore_punctuation, times),
995 SelectionGoal::None,
996 ),
997 NextWordEnd { ignore_punctuation } => (
998 next_word_end(map, point, *ignore_punctuation, times, true, true),
999 SelectionGoal::None,
1000 ),
1001 PreviousWordStart { ignore_punctuation } => (
1002 previous_word_start(map, point, *ignore_punctuation, times),
1003 SelectionGoal::None,
1004 ),
1005 PreviousWordEnd { ignore_punctuation } => (
1006 previous_word_end(map, point, *ignore_punctuation, times),
1007 SelectionGoal::None,
1008 ),
1009 NextSubwordStart { ignore_punctuation } => (
1010 next_subword_start(map, point, *ignore_punctuation, times),
1011 SelectionGoal::None,
1012 ),
1013 NextSubwordEnd { ignore_punctuation } => (
1014 next_subword_end(map, point, *ignore_punctuation, times, true),
1015 SelectionGoal::None,
1016 ),
1017 PreviousSubwordStart { ignore_punctuation } => (
1018 previous_subword_start(map, point, *ignore_punctuation, times),
1019 SelectionGoal::None,
1020 ),
1021 PreviousSubwordEnd { ignore_punctuation } => (
1022 previous_subword_end(map, point, *ignore_punctuation, times),
1023 SelectionGoal::None,
1024 ),
1025 FirstNonWhitespace { display_lines } => (
1026 first_non_whitespace(map, *display_lines, point),
1027 SelectionGoal::None,
1028 ),
1029 StartOfLine { display_lines } => (
1030 start_of_line(map, *display_lines, point),
1031 SelectionGoal::None,
1032 ),
1033 MiddleOfLine { display_lines } => (
1034 middle_of_line(map, *display_lines, point, maybe_times),
1035 SelectionGoal::None,
1036 ),
1037 EndOfLine { display_lines } => (
1038 end_of_line(map, *display_lines, point, times),
1039 SelectionGoal::HorizontalPosition(f64::INFINITY),
1040 ),
1041 SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
1042 SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
1043 StartOfParagraph => (start_of_paragraph(map, point, times), SelectionGoal::None),
1044 EndOfParagraph => (
1045 map.clip_at_line_end(end_of_paragraph(map, point, times)),
1046 SelectionGoal::None,
1047 ),
1048 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
1049 StartOfDocument => (
1050 start_of_document(map, point, maybe_times),
1051 SelectionGoal::None,
1052 ),
1053 EndOfDocument => (
1054 end_of_document(map, point, maybe_times),
1055 SelectionGoal::None,
1056 ),
1057 Matching { match_quotes } => (matching(map, point, *match_quotes), SelectionGoal::None),
1058 GoToPercentage => (go_to_percentage(map, point, times), SelectionGoal::None),
1059 UnmatchedForward { char } => (
1060 unmatched_forward(map, point, *char, times),
1061 SelectionGoal::None,
1062 ),
1063 UnmatchedBackward { char } => (
1064 unmatched_backward(map, point, *char, times),
1065 SelectionGoal::None,
1066 ),
1067 // t f
1068 FindForward {
1069 before,
1070 char,
1071 mode,
1072 smartcase,
1073 } => {
1074 return find_forward(map, point, *before, *char, times, *mode, *smartcase)
1075 .map(|new_point| (new_point, SelectionGoal::None));
1076 }
1077 // T F
1078 FindBackward {
1079 after,
1080 char,
1081 mode,
1082 smartcase,
1083 } => (
1084 find_backward(map, point, *after, *char, times, *mode, *smartcase),
1085 SelectionGoal::None,
1086 ),
1087 Sneak {
1088 first_char,
1089 second_char,
1090 smartcase,
1091 } => {
1092 return sneak(map, point, *first_char, *second_char, times, *smartcase)
1093 .map(|new_point| (new_point, SelectionGoal::None));
1094 }
1095 SneakBackward {
1096 first_char,
1097 second_char,
1098 smartcase,
1099 } => {
1100 return sneak_backward(map, point, *first_char, *second_char, times, *smartcase)
1101 .map(|new_point| (new_point, SelectionGoal::None));
1102 }
1103 // ; -- repeat the last find done with t, f, T, F
1104 RepeatFind { last_find } => match **last_find {
1105 Motion::FindForward {
1106 before,
1107 char,
1108 mode,
1109 smartcase,
1110 } => {
1111 let mut new_point =
1112 find_forward(map, point, before, char, times, mode, smartcase);
1113 if new_point == Some(point) {
1114 new_point =
1115 find_forward(map, point, before, char, times + 1, mode, smartcase);
1116 }
1117
1118 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1119 }
1120
1121 Motion::FindBackward {
1122 after,
1123 char,
1124 mode,
1125 smartcase,
1126 } => {
1127 let mut new_point =
1128 find_backward(map, point, after, char, times, mode, smartcase);
1129 if new_point == point {
1130 new_point =
1131 find_backward(map, point, after, char, times + 1, mode, smartcase);
1132 }
1133
1134 (new_point, SelectionGoal::None)
1135 }
1136 Motion::Sneak {
1137 first_char,
1138 second_char,
1139 smartcase,
1140 } => {
1141 let mut new_point =
1142 sneak(map, point, first_char, second_char, times, smartcase);
1143 if new_point == Some(point) {
1144 new_point =
1145 sneak(map, point, first_char, second_char, times + 1, smartcase);
1146 }
1147
1148 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1149 }
1150
1151 Motion::SneakBackward {
1152 first_char,
1153 second_char,
1154 smartcase,
1155 } => {
1156 let mut new_point =
1157 sneak_backward(map, point, first_char, second_char, times, smartcase);
1158 if new_point == Some(point) {
1159 new_point = sneak_backward(
1160 map,
1161 point,
1162 first_char,
1163 second_char,
1164 times + 1,
1165 smartcase,
1166 );
1167 }
1168
1169 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1170 }
1171 _ => return None,
1172 },
1173 // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction
1174 RepeatFindReversed { last_find } => match **last_find {
1175 Motion::FindForward {
1176 before,
1177 char,
1178 mode,
1179 smartcase,
1180 } => {
1181 let mut new_point =
1182 find_backward(map, point, before, char, times, mode, smartcase);
1183 if new_point == point {
1184 new_point =
1185 find_backward(map, point, before, char, times + 1, mode, smartcase);
1186 }
1187
1188 (new_point, SelectionGoal::None)
1189 }
1190
1191 Motion::FindBackward {
1192 after,
1193 char,
1194 mode,
1195 smartcase,
1196 } => {
1197 let mut new_point =
1198 find_forward(map, point, after, char, times, mode, smartcase);
1199 if new_point == Some(point) {
1200 new_point =
1201 find_forward(map, point, after, char, times + 1, mode, smartcase);
1202 }
1203
1204 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1205 }
1206
1207 Motion::Sneak {
1208 first_char,
1209 second_char,
1210 smartcase,
1211 } => {
1212 let mut new_point =
1213 sneak_backward(map, point, first_char, second_char, times, smartcase);
1214 if new_point == Some(point) {
1215 new_point = sneak_backward(
1216 map,
1217 point,
1218 first_char,
1219 second_char,
1220 times + 1,
1221 smartcase,
1222 );
1223 }
1224
1225 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1226 }
1227
1228 Motion::SneakBackward {
1229 first_char,
1230 second_char,
1231 smartcase,
1232 } => {
1233 let mut new_point =
1234 sneak(map, point, first_char, second_char, times, smartcase);
1235 if new_point == Some(point) {
1236 new_point =
1237 sneak(map, point, first_char, second_char, times + 1, smartcase);
1238 }
1239
1240 return new_point.map(|new_point| (new_point, SelectionGoal::None));
1241 }
1242 _ => return None,
1243 },
1244 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
1245 PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None),
1246 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
1247 EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None),
1248 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
1249 WindowTop => window_top(map, point, text_layout_details, times - 1),
1250 WindowMiddle => window_middle(map, point, text_layout_details),
1251 WindowBottom => window_bottom(map, point, text_layout_details, times - 1),
1252 Jump { line, anchor } => mark::jump_motion(map, *anchor, *line),
1253 ZedSearchResult { new_selections, .. } => {
1254 // There will be only one selection, as
1255 // Search::SelectNextMatch selects a single match.
1256 if let Some(new_selection) = new_selections.first() {
1257 (
1258 new_selection.start.to_display_point(map),
1259 SelectionGoal::None,
1260 )
1261 } else {
1262 return None;
1263 }
1264 }
1265 NextSectionStart => (
1266 section_motion(map, point, times, Direction::Next, true),
1267 SelectionGoal::None,
1268 ),
1269 NextSectionEnd => (
1270 section_motion(map, point, times, Direction::Next, false),
1271 SelectionGoal::None,
1272 ),
1273 PreviousSectionStart => (
1274 section_motion(map, point, times, Direction::Prev, true),
1275 SelectionGoal::None,
1276 ),
1277 PreviousSectionEnd => (
1278 section_motion(map, point, times, Direction::Prev, false),
1279 SelectionGoal::None,
1280 ),
1281
1282 NextMethodStart => (
1283 method_motion(map, point, times, Direction::Next, true),
1284 SelectionGoal::None,
1285 ),
1286 NextMethodEnd => (
1287 method_motion(map, point, times, Direction::Next, false),
1288 SelectionGoal::None,
1289 ),
1290 PreviousMethodStart => (
1291 method_motion(map, point, times, Direction::Prev, true),
1292 SelectionGoal::None,
1293 ),
1294 PreviousMethodEnd => (
1295 method_motion(map, point, times, Direction::Prev, false),
1296 SelectionGoal::None,
1297 ),
1298 NextComment => (
1299 comment_motion(map, point, times, Direction::Next),
1300 SelectionGoal::None,
1301 ),
1302 PreviousComment => (
1303 comment_motion(map, point, times, Direction::Prev),
1304 SelectionGoal::None,
1305 ),
1306 PreviousLesserIndent => (
1307 indent_motion(map, point, times, Direction::Prev, IndentType::Lesser),
1308 SelectionGoal::None,
1309 ),
1310 PreviousGreaterIndent => (
1311 indent_motion(map, point, times, Direction::Prev, IndentType::Greater),
1312 SelectionGoal::None,
1313 ),
1314 PreviousSameIndent => (
1315 indent_motion(map, point, times, Direction::Prev, IndentType::Same),
1316 SelectionGoal::None,
1317 ),
1318 NextLesserIndent => (
1319 indent_motion(map, point, times, Direction::Next, IndentType::Lesser),
1320 SelectionGoal::None,
1321 ),
1322 NextGreaterIndent => (
1323 indent_motion(map, point, times, Direction::Next, IndentType::Greater),
1324 SelectionGoal::None,
1325 ),
1326 NextSameIndent => (
1327 indent_motion(map, point, times, Direction::Next, IndentType::Same),
1328 SelectionGoal::None,
1329 ),
1330 };
1331 (new_point != point || infallible).then_some((new_point, goal))
1332 }
1333
1334 // Get the range value after self is applied to the specified selection.
1335 pub fn range(
1336 &self,
1337 map: &DisplaySnapshot,
1338 mut selection: Selection<DisplayPoint>,
1339 times: Option<usize>,
1340 text_layout_details: &TextLayoutDetails,
1341 forced_motion: bool,
1342 ) -> Option<(Range<DisplayPoint>, MotionKind)> {
1343 if let Motion::ZedSearchResult {
1344 prior_selections,
1345 new_selections,
1346 } = self
1347 {
1348 if let Some((prior_selection, new_selection)) =
1349 prior_selections.first().zip(new_selections.first())
1350 {
1351 let start = prior_selection
1352 .start
1353 .to_display_point(map)
1354 .min(new_selection.start.to_display_point(map));
1355 let end = new_selection
1356 .end
1357 .to_display_point(map)
1358 .max(prior_selection.end.to_display_point(map));
1359
1360 if start < end {
1361 return Some((start..end, MotionKind::Exclusive));
1362 } else {
1363 return Some((end..start, MotionKind::Exclusive));
1364 }
1365 } else {
1366 return None;
1367 }
1368 }
1369 let maybe_new_point = self.move_point(
1370 map,
1371 selection.head(),
1372 selection.goal,
1373 times,
1374 text_layout_details,
1375 );
1376
1377 let (new_head, goal) = match (maybe_new_point, forced_motion) {
1378 (Some((p, g)), _) => Some((p, g)),
1379 (None, false) => None,
1380 (None, true) => Some((selection.head(), selection.goal)),
1381 }?;
1382
1383 selection.set_head(new_head, goal);
1384
1385 let mut kind = match (self.default_kind(), forced_motion) {
1386 (MotionKind::Linewise, true) => MotionKind::Exclusive,
1387 (MotionKind::Exclusive, true) => MotionKind::Inclusive,
1388 (MotionKind::Inclusive, true) => MotionKind::Exclusive,
1389 (kind, false) => kind,
1390 };
1391
1392 if let Motion::NextWordStart {
1393 ignore_punctuation: _,
1394 } = self
1395 {
1396 // Another special case: When using the "w" motion in combination with an
1397 // operator and the last word moved over is at the end of a line, the end of
1398 // that word becomes the end of the operated text, not the first word in the
1399 // next line.
1400 let start = selection.start.to_point(map);
1401 let end = selection.end.to_point(map);
1402 let start_row = MultiBufferRow(selection.start.to_point(map).row);
1403 if end.row > start.row {
1404 selection.end = Point::new(start_row.0, map.buffer_snapshot().line_len(start_row))
1405 .to_display_point(map);
1406
1407 // a bit of a hack, we need `cw` on a blank line to not delete the newline,
1408 // but dw on a blank line should. The `Linewise` returned from this method
1409 // causes the `d` operator to include the trailing newline.
1410 if selection.start == selection.end {
1411 return Some((selection.start..selection.end, MotionKind::Linewise));
1412 }
1413 }
1414 } else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() {
1415 let start_point = selection.start.to_point(map);
1416 let mut end_point = selection.end.to_point(map);
1417 let mut next_point = selection.end;
1418 *next_point.column_mut() += 1;
1419 next_point = map.clip_point(next_point, Bias::Right);
1420 if next_point.to_point(map) == end_point && forced_motion {
1421 selection.end = movement::saturating_left(map, selection.end);
1422 }
1423
1424 if end_point.row > start_point.row {
1425 let first_non_blank_of_start_row = map
1426 .line_indent_for_buffer_row(MultiBufferRow(start_point.row))
1427 .raw_len();
1428 // https://github.com/neovim/neovim/blob/ee143aaf65a0e662c42c636aa4a959682858b3e7/src/nvim/ops.c#L6178-L6203
1429 if end_point.column == 0 {
1430 // If the motion is exclusive and the end of the motion is in column 1, the
1431 // end of the motion is moved to the end of the previous line and the motion
1432 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
1433 // but "d}" will not include that line.
1434 //
1435 // If the motion is exclusive, the end of the motion is in column 1 and the
1436 // start of the motion was at or before the first non-blank in the line, the
1437 // motion becomes linewise. Example: If a paragraph begins with some blanks
1438 // and you do "d}" while standing on the first non-blank, all the lines of
1439 // the paragraph are deleted, including the blanks.
1440 if start_point.column <= first_non_blank_of_start_row {
1441 kind = MotionKind::Linewise;
1442 } else {
1443 kind = MotionKind::Inclusive;
1444 }
1445 end_point.row -= 1;
1446 end_point.column = 0;
1447 selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left);
1448 } else if let Motion::EndOfParagraph = self {
1449 // Special case: When using the "}" motion, it's possible
1450 // that there's no blank lines after the paragraph the
1451 // cursor is currently on.
1452 // In this situation the `end_point.column` value will be
1453 // greater than 0, so the selection doesn't actually end on
1454 // the first character of a blank line. In that case, we'll
1455 // want to move one column to the right, to actually include
1456 // all characters of the last non-blank line.
1457 selection.end = movement::saturating_right(map, selection.end)
1458 }
1459 }
1460 } else if kind == MotionKind::Inclusive {
1461 selection.end = movement::saturating_right(map, selection.end)
1462 }
1463
1464 if kind == MotionKind::Linewise {
1465 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
1466 selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
1467 }
1468 Some((selection.start..selection.end, kind))
1469 }
1470
1471 // Expands a selection using self for an operator
1472 pub fn expand_selection(
1473 &self,
1474 map: &DisplaySnapshot,
1475 selection: &mut Selection<DisplayPoint>,
1476 times: Option<usize>,
1477 text_layout_details: &TextLayoutDetails,
1478 forced_motion: bool,
1479 ) -> Option<MotionKind> {
1480 let (range, kind) = self.range(
1481 map,
1482 selection.clone(),
1483 times,
1484 text_layout_details,
1485 forced_motion,
1486 )?;
1487 selection.start = range.start;
1488 selection.end = range.end;
1489 Some(kind)
1490 }
1491}
1492
1493fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1494 for _ in 0..times {
1495 point = movement::saturating_left(map, point);
1496 if point.column() == 0 {
1497 break;
1498 }
1499 }
1500 point
1501}
1502
1503pub(crate) fn wrapping_left(
1504 map: &DisplaySnapshot,
1505 mut point: DisplayPoint,
1506 times: usize,
1507) -> DisplayPoint {
1508 for _ in 0..times {
1509 point = movement::left(map, point);
1510 if point.is_zero() {
1511 break;
1512 }
1513 }
1514 point
1515}
1516
1517fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1518 for _ in 0..times {
1519 point = wrapping_right_single(map, point);
1520 if point == map.max_point() {
1521 break;
1522 }
1523 }
1524 point
1525}
1526
1527fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
1528 let mut next_point = point;
1529 *next_point.column_mut() += 1;
1530 next_point = map.clip_point(next_point, Bias::Right);
1531 if next_point == point {
1532 if next_point.row() == map.max_point().row() {
1533 next_point
1534 } else {
1535 DisplayPoint::new(next_point.row().next_row(), 0)
1536 }
1537 } else {
1538 next_point
1539 }
1540}
1541
1542fn up_down_buffer_rows(
1543 map: &DisplaySnapshot,
1544 mut point: DisplayPoint,
1545 mut goal: SelectionGoal,
1546 mut times: isize,
1547 text_layout_details: &TextLayoutDetails,
1548) -> (DisplayPoint, SelectionGoal) {
1549 let bias = if times < 0 { Bias::Left } else { Bias::Right };
1550
1551 while map.is_folded_buffer_header(point.row()) {
1552 if times < 0 {
1553 (point, _) = movement::up(map, point, goal, true, text_layout_details);
1554 times += 1;
1555 } else if times > 0 {
1556 (point, _) = movement::down(map, point, goal, true, text_layout_details);
1557 times -= 1;
1558 } else {
1559 break;
1560 }
1561 }
1562
1563 let start = map.display_point_to_fold_point(point, Bias::Left);
1564 let begin_folded_line = map.fold_point_to_display_point(
1565 map.fold_snapshot()
1566 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
1567 );
1568 let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0;
1569
1570 let (goal_wrap, goal_x) = match goal {
1571 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
1572 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end as f32),
1573 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x as f32),
1574 _ => {
1575 let x = map.x_for_display_point(point, text_layout_details);
1576 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.into()));
1577 (select_nth_wrapped_row, x.into())
1578 }
1579 };
1580
1581 let target = start.row() as isize + times;
1582 let new_row = (target.max(0) as u32).min(map.fold_snapshot().max_point().row());
1583
1584 let mut begin_folded_line = map.fold_point_to_display_point(
1585 map.fold_snapshot()
1586 .clip_point(FoldPoint::new(new_row, 0), bias),
1587 );
1588
1589 let mut i = 0;
1590 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
1591 let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0);
1592 if map
1593 .display_point_to_fold_point(next_folded_line, bias)
1594 .row()
1595 == new_row
1596 {
1597 i += 1;
1598 begin_folded_line = next_folded_line;
1599 } else {
1600 break;
1601 }
1602 }
1603
1604 let new_col = if i == goal_wrap {
1605 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
1606 } else {
1607 map.line_len(begin_folded_line.row())
1608 };
1609
1610 let point = DisplayPoint::new(begin_folded_line.row(), new_col);
1611 let mut clipped_point = map.clip_point(point, bias);
1612
1613 // When navigating vertically in vim mode with inlay hints present,
1614 // we need to handle the case where clipping moves us to a different row.
1615 // This can happen when moving down (Bias::Right) and hitting an inlay hint.
1616 // Re-clip with opposite bias to stay on the intended line.
1617 //
1618 // See: https://github.com/zed-industries/zed/issues/29134
1619 if clipped_point.row() > point.row() {
1620 clipped_point = map.clip_point(point, Bias::Left);
1621 }
1622
1623 (clipped_point, goal)
1624}
1625
1626fn down_display(
1627 map: &DisplaySnapshot,
1628 mut point: DisplayPoint,
1629 mut goal: SelectionGoal,
1630 times: usize,
1631 text_layout_details: &TextLayoutDetails,
1632) -> (DisplayPoint, SelectionGoal) {
1633 for _ in 0..times {
1634 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
1635 }
1636
1637 (point, goal)
1638}
1639
1640fn up_display(
1641 map: &DisplaySnapshot,
1642 mut point: DisplayPoint,
1643 mut goal: SelectionGoal,
1644 times: usize,
1645 text_layout_details: &TextLayoutDetails,
1646) -> (DisplayPoint, SelectionGoal) {
1647 for _ in 0..times {
1648 (point, goal) = movement::up(map, point, goal, true, text_layout_details);
1649 }
1650
1651 (point, goal)
1652}
1653
1654pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
1655 for _ in 0..times {
1656 let new_point = movement::saturating_right(map, point);
1657 if point == new_point {
1658 break;
1659 }
1660 point = new_point;
1661 }
1662 point
1663}
1664
1665pub(crate) fn next_char(
1666 map: &DisplaySnapshot,
1667 point: DisplayPoint,
1668 allow_cross_newline: bool,
1669) -> DisplayPoint {
1670 let mut new_point = point;
1671 let mut max_column = map.line_len(new_point.row());
1672 if !allow_cross_newline {
1673 max_column -= 1;
1674 }
1675 if new_point.column() < max_column {
1676 *new_point.column_mut() += 1;
1677 } else if new_point < map.max_point() && allow_cross_newline {
1678 *new_point.row_mut() += 1;
1679 *new_point.column_mut() = 0;
1680 }
1681 map.clip_ignoring_line_ends(new_point, Bias::Right)
1682}
1683
1684pub(crate) fn next_word_start(
1685 map: &DisplaySnapshot,
1686 mut point: DisplayPoint,
1687 ignore_punctuation: bool,
1688 times: usize,
1689) -> DisplayPoint {
1690 let classifier = map
1691 .buffer_snapshot()
1692 .char_classifier_at(point.to_point(map))
1693 .ignore_punctuation(ignore_punctuation);
1694 for _ in 0..times {
1695 let mut crossed_newline = false;
1696 let new_point =
1697 movement::find_boundary(map, point, FindRange::MultiLine, &mut |left, right| {
1698 let left_kind = classifier.kind(left);
1699 let right_kind = classifier.kind(right);
1700 let at_newline = right == '\n';
1701
1702 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
1703 || at_newline && crossed_newline
1704 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1705
1706 crossed_newline |= at_newline;
1707 found
1708 });
1709 if point == new_point {
1710 break;
1711 }
1712 point = new_point;
1713 }
1714 point
1715}
1716
1717fn next_end_impl(
1718 map: &DisplaySnapshot,
1719 mut point: DisplayPoint,
1720 times: usize,
1721 allow_cross_newline: bool,
1722 always_advance: bool,
1723 is_boundary: &mut dyn FnMut(char, char) -> bool,
1724) -> DisplayPoint {
1725 for _ in 0..times {
1726 let mut need_next_char = false;
1727 let new_point = if always_advance {
1728 next_char(map, point, allow_cross_newline)
1729 } else {
1730 point
1731 };
1732 let new_point = movement::find_boundary_exclusive(
1733 map,
1734 new_point,
1735 FindRange::MultiLine,
1736 &mut |left, right| {
1737 let at_newline = right == '\n';
1738
1739 if !allow_cross_newline && at_newline {
1740 need_next_char = true;
1741 return true;
1742 }
1743
1744 is_boundary(left, right)
1745 },
1746 );
1747 let new_point = if need_next_char {
1748 next_char(map, new_point, true)
1749 } else {
1750 new_point
1751 };
1752 let new_point = map.clip_point(new_point, Bias::Left);
1753 if point == new_point {
1754 break;
1755 }
1756 point = new_point;
1757 }
1758 point
1759}
1760
1761pub(crate) fn next_word_end(
1762 map: &DisplaySnapshot,
1763 point: DisplayPoint,
1764 ignore_punctuation: bool,
1765 times: usize,
1766 allow_cross_newline: bool,
1767 always_advance: bool,
1768) -> DisplayPoint {
1769 let classifier = map
1770 .buffer_snapshot()
1771 .char_classifier_at(point.to_point(map))
1772 .ignore_punctuation(ignore_punctuation);
1773
1774 next_end_impl(
1775 map,
1776 point,
1777 times,
1778 allow_cross_newline,
1779 always_advance,
1780 &mut |left, right| {
1781 let left_kind = classifier.kind(left);
1782 let right_kind = classifier.kind(right);
1783 left_kind != right_kind && left_kind != CharKind::Whitespace
1784 },
1785 )
1786}
1787
1788pub(crate) fn next_subword_end(
1789 map: &DisplaySnapshot,
1790 point: DisplayPoint,
1791 ignore_punctuation: bool,
1792 times: usize,
1793 allow_cross_newline: bool,
1794) -> DisplayPoint {
1795 let classifier = map
1796 .buffer_snapshot()
1797 .char_classifier_at(point.to_point(map))
1798 .ignore_punctuation(ignore_punctuation);
1799
1800 next_end_impl(
1801 map,
1802 point,
1803 times,
1804 allow_cross_newline,
1805 true,
1806 &mut |left, right| {
1807 let left_kind = classifier.kind(left);
1808 let right_kind = classifier.kind(right);
1809 let is_stopping_punct = |c: char| ".$=\"'{}[]()<>".contains(c);
1810 let found_subword_end = is_subword_end(left, right, "$_-");
1811 let is_word_end = (left_kind != right_kind)
1812 && (!left.is_ascii_punctuation() || is_stopping_punct(left));
1813
1814 !left.is_whitespace() && (is_word_end || found_subword_end)
1815 },
1816 )
1817}
1818
1819fn previous_word_start(
1820 map: &DisplaySnapshot,
1821 mut point: DisplayPoint,
1822 ignore_punctuation: bool,
1823 times: usize,
1824) -> DisplayPoint {
1825 let classifier = map
1826 .buffer_snapshot()
1827 .char_classifier_at(point.to_point(map))
1828 .ignore_punctuation(ignore_punctuation);
1829 for _ in 0..times {
1830 // This works even though find_preceding_boundary is called for every character in the line containing
1831 // cursor because the newline is checked only once.
1832 let new_point = movement::find_preceding_boundary_display_point(
1833 map,
1834 point,
1835 FindRange::MultiLine,
1836 &mut |left, right| {
1837 let left_kind = classifier.kind(left);
1838 let right_kind = classifier.kind(right);
1839
1840 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
1841 },
1842 );
1843 if point == new_point {
1844 break;
1845 }
1846 point = new_point;
1847 }
1848 point
1849}
1850
1851fn previous_word_end(
1852 map: &DisplaySnapshot,
1853 point: DisplayPoint,
1854 ignore_punctuation: bool,
1855 times: usize,
1856) -> DisplayPoint {
1857 let classifier = map
1858 .buffer_snapshot()
1859 .char_classifier_at(point.to_point(map))
1860 .ignore_punctuation(ignore_punctuation);
1861 let mut point = point.to_point(map);
1862
1863 if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row))
1864 && let Some(ch) = map.buffer_snapshot().chars_at(point).next()
1865 {
1866 point.column += ch.len_utf8() as u32;
1867 }
1868 for _ in 0..times {
1869 let new_point = movement::find_preceding_boundary_point(
1870 &map.buffer_snapshot(),
1871 point,
1872 FindRange::MultiLine,
1873 &mut |left, right| {
1874 let left_kind = classifier.kind(left);
1875 let right_kind = classifier.kind(right);
1876 match (left_kind, right_kind) {
1877 (CharKind::Punctuation, CharKind::Whitespace)
1878 | (CharKind::Punctuation, CharKind::Word)
1879 | (CharKind::Word, CharKind::Whitespace)
1880 | (CharKind::Word, CharKind::Punctuation) => true,
1881 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
1882 _ => false,
1883 }
1884 },
1885 );
1886 if new_point == point {
1887 break;
1888 }
1889 point = new_point;
1890 }
1891 movement::saturating_left(map, point.to_display_point(map))
1892}
1893
1894/// Checks if there's a subword boundary start between `left` and `right` characters.
1895/// This detects transitions like `_b` (separator to non-separator) or `aB` (lowercase to uppercase).
1896pub(crate) fn is_subword_start(left: char, right: char, separators: &str) -> bool {
1897 let is_separator = |c: char| separators.contains(c);
1898 (is_separator(left) && !is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
1899}
1900
1901/// Checks if there's a subword boundary end between `left` and `right` characters.
1902/// This detects transitions like `a_` (non-separator to separator) or `aB` (lowercase to uppercase).
1903pub(crate) fn is_subword_end(left: char, right: char, separators: &str) -> bool {
1904 let is_separator = |c: char| separators.contains(c);
1905 (!is_separator(left) && is_separator(right)) || (left.is_lowercase() && right.is_uppercase())
1906}
1907
1908fn next_subword_start(
1909 map: &DisplaySnapshot,
1910 mut point: DisplayPoint,
1911 ignore_punctuation: bool,
1912 times: usize,
1913) -> DisplayPoint {
1914 let classifier = map
1915 .buffer_snapshot()
1916 .char_classifier_at(point.to_point(map))
1917 .ignore_punctuation(ignore_punctuation);
1918 for _ in 0..times {
1919 let mut crossed_newline = false;
1920 let new_point =
1921 movement::find_boundary(map, point, FindRange::MultiLine, &mut |left, right| {
1922 let left_kind = classifier.kind(left);
1923 let right_kind = classifier.kind(right);
1924 let at_newline = right == '\n';
1925 let is_stopping_punct = |c: char| "$=\"'{}[]()<>".contains(c);
1926 let found_subword_start = is_subword_start(left, right, ".$_-");
1927 let is_word_start = (left_kind != right_kind)
1928 && (!right.is_ascii_punctuation() || is_stopping_punct(right));
1929
1930 let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
1931 || at_newline && crossed_newline
1932 || right == '\n' && left == '\n'; // Prevents skipping repeated empty lines
1933
1934 crossed_newline |= at_newline;
1935 found
1936 });
1937 if point == new_point {
1938 break;
1939 }
1940 point = new_point;
1941 }
1942 point
1943}
1944
1945fn previous_subword_start(
1946 map: &DisplaySnapshot,
1947 mut point: DisplayPoint,
1948 ignore_punctuation: bool,
1949 times: usize,
1950) -> DisplayPoint {
1951 let classifier = map
1952 .buffer_snapshot()
1953 .char_classifier_at(point.to_point(map))
1954 .ignore_punctuation(ignore_punctuation);
1955 for _ in 0..times {
1956 let mut crossed_newline = false;
1957 // This works even though find_preceding_boundary is called for every character in the line containing
1958 // cursor because the newline is checked only once.
1959 let new_point = movement::find_preceding_boundary_display_point(
1960 map,
1961 point,
1962 FindRange::MultiLine,
1963 &mut |left, right| {
1964 let left_kind = classifier.kind(left);
1965 let right_kind = classifier.kind(right);
1966 let at_newline = right == '\n';
1967
1968 let is_stopping_punct = |c: char| ".$=\"'{}[]()<>".contains(c);
1969 let is_word_start = (left_kind != right_kind)
1970 && (is_stopping_punct(right) || !right.is_ascii_punctuation());
1971 let found_subword_start = is_subword_start(left, right, ".$_-");
1972
1973 let found = (!right.is_whitespace() && (is_word_start || found_subword_start))
1974 || at_newline && crossed_newline
1975 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
1976
1977 crossed_newline |= at_newline;
1978
1979 found
1980 },
1981 );
1982 if point == new_point {
1983 break;
1984 }
1985 point = new_point;
1986 }
1987 point
1988}
1989
1990fn previous_subword_end(
1991 map: &DisplaySnapshot,
1992 point: DisplayPoint,
1993 ignore_punctuation: bool,
1994 times: usize,
1995) -> DisplayPoint {
1996 let classifier = map
1997 .buffer_snapshot()
1998 .char_classifier_at(point.to_point(map))
1999 .ignore_punctuation(ignore_punctuation);
2000 let mut point = point.to_point(map);
2001
2002 if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row))
2003 && let Some(ch) = map.buffer_snapshot().chars_at(point).next()
2004 {
2005 point.column += ch.len_utf8() as u32;
2006 }
2007 for _ in 0..times {
2008 let new_point = movement::find_preceding_boundary_point(
2009 &map.buffer_snapshot(),
2010 point,
2011 FindRange::MultiLine,
2012 &mut |left, right| {
2013 let left_kind = classifier.kind(left);
2014 let right_kind = classifier.kind(right);
2015
2016 let is_stopping_punct = |c: char| ".$;=\"'{}[]()<>".contains(c);
2017 let found_subword_end = is_subword_end(left, right, "$_-");
2018
2019 if found_subword_end {
2020 return true;
2021 }
2022
2023 match (left_kind, right_kind) {
2024 (CharKind::Word, CharKind::Whitespace)
2025 | (CharKind::Word, CharKind::Punctuation) => true,
2026 (CharKind::Punctuation, _) if is_stopping_punct(left) => true,
2027 (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n',
2028 _ => false,
2029 }
2030 },
2031 );
2032 if new_point == point {
2033 break;
2034 }
2035 point = new_point;
2036 }
2037 movement::saturating_left(map, point.to_display_point(map))
2038}
2039
2040pub(crate) fn first_non_whitespace(
2041 map: &DisplaySnapshot,
2042 display_lines: bool,
2043 from: DisplayPoint,
2044) -> DisplayPoint {
2045 let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left);
2046 let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map));
2047 for (ch, offset) in map.buffer_chars_at(start_offset) {
2048 if ch == '\n' {
2049 return from;
2050 }
2051
2052 start_offset = offset;
2053
2054 if classifier.kind(ch) != CharKind::Whitespace {
2055 break;
2056 }
2057 }
2058
2059 start_offset.to_display_point(map)
2060}
2061
2062pub(crate) fn last_non_whitespace(
2063 map: &DisplaySnapshot,
2064 from: DisplayPoint,
2065 count: usize,
2066) -> DisplayPoint {
2067 let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left);
2068 let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map));
2069
2070 // NOTE: depending on clip_at_line_end we may already be one char back from the end.
2071 if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next()
2072 && classifier.kind(ch) != CharKind::Whitespace
2073 {
2074 return end_of_line.to_display_point(map);
2075 }
2076
2077 for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) {
2078 if ch == '\n' {
2079 break;
2080 }
2081 end_of_line = offset;
2082 if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' {
2083 break;
2084 }
2085 }
2086
2087 end_of_line.to_display_point(map)
2088}
2089
2090pub(crate) fn start_of_line(
2091 map: &DisplaySnapshot,
2092 display_lines: bool,
2093 point: DisplayPoint,
2094) -> DisplayPoint {
2095 if display_lines {
2096 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
2097 } else {
2098 map.prev_line_boundary(point.to_point(map)).1
2099 }
2100}
2101
2102pub(crate) fn middle_of_line(
2103 map: &DisplaySnapshot,
2104 display_lines: bool,
2105 point: DisplayPoint,
2106 times: Option<usize>,
2107) -> DisplayPoint {
2108 let percent = if let Some(times) = times.filter(|&t| t <= 100) {
2109 times as f64 / 100.
2110 } else {
2111 0.5
2112 };
2113 if display_lines {
2114 map.clip_point(
2115 DisplayPoint::new(
2116 point.row(),
2117 (map.line_len(point.row()) as f64 * percent) as u32,
2118 ),
2119 Bias::Left,
2120 )
2121 } else {
2122 let mut buffer_point = point.to_point(map);
2123 buffer_point.column = (map
2124 .buffer_snapshot()
2125 .line_len(MultiBufferRow(buffer_point.row)) as f64
2126 * percent) as u32;
2127
2128 map.clip_point(buffer_point.to_display_point(map), Bias::Left)
2129 }
2130}
2131
2132pub(crate) fn end_of_line(
2133 map: &DisplaySnapshot,
2134 display_lines: bool,
2135 mut point: DisplayPoint,
2136 times: usize,
2137) -> DisplayPoint {
2138 if times > 1 {
2139 point = map.start_of_relative_buffer_row(point, times as isize - 1);
2140 }
2141 if display_lines {
2142 map.clip_point(
2143 DisplayPoint::new(point.row(), map.line_len(point.row())),
2144 Bias::Left,
2145 )
2146 } else {
2147 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
2148 }
2149}
2150
2151pub(crate) fn sentence_backwards(
2152 map: &DisplaySnapshot,
2153 point: DisplayPoint,
2154 mut times: usize,
2155) -> DisplayPoint {
2156 let mut start = point.to_point(map).to_offset(&map.buffer_snapshot());
2157 let mut chars = map.reverse_buffer_chars_at(start).peekable();
2158
2159 let mut was_newline = map
2160 .buffer_chars_at(start)
2161 .next()
2162 .is_some_and(|(c, _)| c == '\n');
2163
2164 while let Some((ch, offset)) = chars.next() {
2165 let start_of_next_sentence = if was_newline && ch == '\n' {
2166 Some(offset + ch.len_utf8())
2167 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2168 Some(next_non_blank(map, offset + ch.len_utf8()))
2169 } else if ch == '.' || ch == '?' || ch == '!' {
2170 start_of_next_sentence(map, offset + ch.len_utf8())
2171 } else {
2172 None
2173 };
2174
2175 if let Some(start_of_next_sentence) = start_of_next_sentence {
2176 if start_of_next_sentence < start {
2177 times = times.saturating_sub(1);
2178 }
2179 if times == 0 || offset.0 == 0 {
2180 return map.clip_point(
2181 start_of_next_sentence
2182 .to_offset(&map.buffer_snapshot())
2183 .to_display_point(map),
2184 Bias::Left,
2185 );
2186 }
2187 }
2188 if was_newline {
2189 start = offset;
2190 }
2191 was_newline = ch == '\n';
2192 }
2193
2194 DisplayPoint::zero()
2195}
2196
2197pub(crate) fn sentence_forwards(
2198 map: &DisplaySnapshot,
2199 point: DisplayPoint,
2200 mut times: usize,
2201) -> DisplayPoint {
2202 let start = point.to_point(map).to_offset(&map.buffer_snapshot());
2203 let mut chars = map.buffer_chars_at(start).peekable();
2204
2205 let mut was_newline = map
2206 .reverse_buffer_chars_at(start)
2207 .next()
2208 .is_some_and(|(c, _)| c == '\n')
2209 && chars.peek().is_some_and(|(c, _)| *c == '\n');
2210
2211 while let Some((ch, offset)) = chars.next() {
2212 if was_newline && ch == '\n' {
2213 continue;
2214 }
2215 let start_of_next_sentence = if was_newline {
2216 Some(next_non_blank(map, offset))
2217 } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') {
2218 Some(next_non_blank(map, offset + ch.len_utf8()))
2219 } else if ch == '.' || ch == '?' || ch == '!' {
2220 start_of_next_sentence(map, offset + ch.len_utf8())
2221 } else {
2222 None
2223 };
2224
2225 if let Some(start_of_next_sentence) = start_of_next_sentence {
2226 times = times.saturating_sub(1);
2227 if times == 0 {
2228 return map.clip_point(
2229 start_of_next_sentence
2230 .to_offset(&map.buffer_snapshot())
2231 .to_display_point(map),
2232 Bias::Right,
2233 );
2234 }
2235 }
2236
2237 was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n');
2238 }
2239
2240 map.max_point()
2241}
2242
2243/// Returns a position of the start of the current paragraph for vim motions,
2244/// where a paragraph is defined as a run of non-empty lines. Lines containing
2245/// only whitespace are not considered empty and do not act as paragraph
2246/// boundaries.
2247pub(crate) fn start_of_paragraph(
2248 map: &DisplaySnapshot,
2249 display_point: DisplayPoint,
2250 mut count: usize,
2251) -> DisplayPoint {
2252 let point = display_point.to_point(map);
2253 if point.row == 0 {
2254 return DisplayPoint::zero();
2255 }
2256
2257 let mut found_non_empty_line = false;
2258 for row in (0..point.row + 1).rev() {
2259 let empty = map.buffer_snapshot().line_len(MultiBufferRow(row)) == 0;
2260 if found_non_empty_line && empty {
2261 if count <= 1 {
2262 return Point::new(row, 0).to_display_point(map);
2263 }
2264 count -= 1;
2265 found_non_empty_line = false;
2266 }
2267
2268 found_non_empty_line |= !empty;
2269 }
2270
2271 DisplayPoint::zero()
2272}
2273
2274/// Returns a position of the end of the current paragraph for vim motions,
2275/// where a paragraph is defined as a run of non-empty lines. Lines containing
2276/// only whitespace are not considered empty and do not act as paragraph
2277/// boundaries.
2278pub(crate) fn end_of_paragraph(
2279 map: &DisplaySnapshot,
2280 display_point: DisplayPoint,
2281 mut count: usize,
2282) -> DisplayPoint {
2283 let point = display_point.to_point(map);
2284 if point.row == map.buffer_snapshot().max_row().0 {
2285 return map.max_point();
2286 }
2287
2288 let mut found_non_empty_line = false;
2289 for row in point.row..=map.buffer_snapshot().max_row().0 {
2290 let empty = map.buffer_snapshot().line_len(MultiBufferRow(row)) == 0;
2291 if found_non_empty_line && empty {
2292 if count <= 1 {
2293 return Point::new(row, 0).to_display_point(map);
2294 }
2295 count -= 1;
2296 found_non_empty_line = false;
2297 }
2298
2299 found_non_empty_line |= !empty;
2300 }
2301
2302 map.max_point()
2303}
2304
2305fn next_non_blank(map: &DisplaySnapshot, start: MultiBufferOffset) -> MultiBufferOffset {
2306 for (c, o) in map.buffer_chars_at(start) {
2307 if c == '\n' || !c.is_whitespace() {
2308 return o;
2309 }
2310 }
2311
2312 map.buffer_snapshot().len()
2313}
2314
2315// given the offset after a ., !, or ? find the start of the next sentence.
2316// if this is not a sentence boundary, returns None.
2317fn start_of_next_sentence(
2318 map: &DisplaySnapshot,
2319 end_of_sentence: MultiBufferOffset,
2320) -> Option<MultiBufferOffset> {
2321 let chars = map.buffer_chars_at(end_of_sentence);
2322 let mut seen_space = false;
2323
2324 for (char, offset) in chars {
2325 if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') {
2326 continue;
2327 }
2328
2329 if char == '\n' && seen_space {
2330 return Some(offset);
2331 } else if char.is_whitespace() {
2332 seen_space = true;
2333 } else if seen_space {
2334 return Some(offset);
2335 } else {
2336 return None;
2337 }
2338 }
2339
2340 Some(map.buffer_snapshot().len())
2341}
2342
2343fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint {
2344 let point = map.display_point_to_point(display_point, Bias::Left);
2345 let snapshot = map.buffer_snapshot();
2346 let Some((buffer_snapshot, _)) = snapshot.point_to_buffer_point(point) else {
2347 return display_point;
2348 };
2349
2350 let Some(anchor) = snapshot.anchor_in_excerpt(buffer_snapshot.anchor_after(
2351 buffer_snapshot.clip_point(Point::new((line - 1) as u32, point.column), Bias::Left),
2352 )) else {
2353 return display_point;
2354 };
2355
2356 map.clip_point(
2357 map.point_to_display_point(anchor.to_point(snapshot), Bias::Left),
2358 Bias::Left,
2359 )
2360}
2361
2362fn start_of_document(
2363 map: &DisplaySnapshot,
2364 display_point: DisplayPoint,
2365 maybe_times: Option<usize>,
2366) -> DisplayPoint {
2367 if let Some(times) = maybe_times {
2368 return go_to_line(map, display_point, times);
2369 }
2370
2371 let point = map.display_point_to_point(display_point, Bias::Left);
2372 let mut first_point = Point::zero();
2373 first_point.column = point.column;
2374
2375 map.clip_point(
2376 map.point_to_display_point(
2377 map.buffer_snapshot().clip_point(first_point, Bias::Left),
2378 Bias::Left,
2379 ),
2380 Bias::Left,
2381 )
2382}
2383
2384fn end_of_document(
2385 map: &DisplaySnapshot,
2386 display_point: DisplayPoint,
2387 maybe_times: Option<usize>,
2388) -> DisplayPoint {
2389 if let Some(times) = maybe_times {
2390 return go_to_line(map, display_point, times);
2391 };
2392 let point = map.display_point_to_point(display_point, Bias::Left);
2393 let mut last_point = map.buffer_snapshot().max_point();
2394 last_point.column = point.column;
2395
2396 map.clip_point(
2397 map.point_to_display_point(
2398 map.buffer_snapshot().clip_point(last_point, Bias::Left),
2399 Bias::Left,
2400 ),
2401 Bias::Left,
2402 )
2403}
2404
2405fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option<DisplayPoint> {
2406 let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?;
2407 let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?;
2408
2409 if head > outer.start && head < inner.start {
2410 let mut offset = inner.end.to_offset(map, Bias::Left);
2411 for c in map.buffer_snapshot().chars_at(offset) {
2412 if c == '/' || c == '\n' || c == '>' {
2413 return Some(offset.to_display_point(map));
2414 }
2415 offset += c.len_utf8();
2416 }
2417 } else {
2418 let mut offset = outer.start.to_offset(map, Bias::Left);
2419 for c in map.buffer_snapshot().chars_at(offset) {
2420 offset += c.len_utf8();
2421 if c == '<' || c == '\n' {
2422 return Some(offset.to_display_point(map));
2423 }
2424 }
2425 }
2426
2427 None
2428}
2429
2430const BRACKET_PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
2431
2432fn get_bracket_pair(ch: char) -> Option<(char, char, bool)> {
2433 for (open, close) in BRACKET_PAIRS {
2434 if ch == open {
2435 return Some((open, close, true));
2436 }
2437 if ch == close {
2438 return Some((open, close, false));
2439 }
2440 }
2441 None
2442}
2443
2444fn find_matching_bracket_text_based(
2445 map: &DisplaySnapshot,
2446 offset: MultiBufferOffset,
2447 line_range: Range<MultiBufferOffset>,
2448) -> Option<MultiBufferOffset> {
2449 let bracket_info = map
2450 .buffer_chars_at(offset)
2451 .take_while(|(_, char_offset)| *char_offset < line_range.end)
2452 .find_map(|(ch, char_offset)| get_bracket_pair(ch).map(|info| (info, char_offset)));
2453
2454 if bracket_info.is_none() {
2455 return find_matching_c_preprocessor_directive(map, line_range, offset);
2456 }
2457
2458 let (open, close, is_opening) = bracket_info?.0;
2459 let bracket_offset = bracket_info?.1;
2460
2461 let mut depth = 0i32;
2462 if is_opening {
2463 for (ch, char_offset) in map.buffer_chars_at(bracket_offset) {
2464 if ch == open {
2465 depth += 1;
2466 } else if ch == close {
2467 depth -= 1;
2468 if depth == 0 {
2469 return Some(char_offset);
2470 }
2471 }
2472 }
2473 } else {
2474 for (ch, char_offset) in map.reverse_buffer_chars_at(bracket_offset + close.len_utf8()) {
2475 if ch == close {
2476 depth += 1;
2477 } else if ch == open {
2478 depth -= 1;
2479 if depth == 0 {
2480 return Some(char_offset);
2481 }
2482 }
2483 }
2484 }
2485
2486 None
2487}
2488
2489fn find_matching_c_preprocessor_directive(
2490 map: &DisplaySnapshot,
2491 line_range: Range<MultiBufferOffset>,
2492 offset: MultiBufferOffset,
2493) -> Option<MultiBufferOffset> {
2494 let line_start = map
2495 .buffer_chars_at(line_range.start)
2496 .skip_while(|(c, _)| *c == ' ' || *c == '\t')
2497 .take_while(|(c, char_offset)| *char_offset < line_range.end && !c.is_whitespace())
2498 .map(|(c, _)| c)
2499 .collect::<String>();
2500
2501 if line_range.start + line_start.len() < offset {
2502 return None;
2503 }
2504
2505 if line_start.starts_with("#if") || line_start.starts_with("#el") {
2506 let mut depth = 0i32;
2507 for (ch, char_offset) in map.buffer_chars_at(line_range.end) {
2508 if ch != '\n' {
2509 continue;
2510 }
2511 let mut line_offset = char_offset + '\n'.len_utf8();
2512
2513 // Skip leading whitespace
2514 map.buffer_chars_at(line_offset)
2515 .take_while(|(c, _)| *c == ' ' || *c == '\t')
2516 .for_each(|(_, _)| line_offset += 1);
2517
2518 // Check what directive starts the next line
2519 let next_line_start = map
2520 .buffer_chars_at(line_offset)
2521 .map(|(c, _)| c)
2522 .take(6)
2523 .collect::<String>();
2524
2525 if next_line_start.starts_with("#if") {
2526 depth += 1;
2527 } else if next_line_start.starts_with("#endif") {
2528 if depth > 0 {
2529 depth -= 1;
2530 } else {
2531 return Some(line_offset);
2532 }
2533 } else if next_line_start.starts_with("#else") || next_line_start.starts_with("#elif") {
2534 if depth == 0 {
2535 return Some(line_offset);
2536 }
2537 }
2538 }
2539 } else if line_start.starts_with("#endif") {
2540 let mut depth = 0i32;
2541 for (ch, char_offset) in
2542 map.reverse_buffer_chars_at(line_range.start.saturating_sub_usize(1))
2543 {
2544 let mut line_offset = if char_offset == MultiBufferOffset(0) {
2545 MultiBufferOffset(0)
2546 } else if ch != '\n' {
2547 continue;
2548 } else {
2549 char_offset + '\n'.len_utf8()
2550 };
2551
2552 // Skip leading whitespace
2553 map.buffer_chars_at(line_offset)
2554 .take_while(|(c, _)| *c == ' ' || *c == '\t')
2555 .for_each(|(_, _)| line_offset += 1);
2556
2557 // Check what directive starts this line
2558 let line_start = map
2559 .buffer_chars_at(line_offset)
2560 .skip_while(|(c, _)| *c == ' ' || *c == '\t')
2561 .map(|(c, _)| c)
2562 .take(6)
2563 .collect::<String>();
2564
2565 if line_start.starts_with("\n\n") {
2566 // empty line
2567 continue;
2568 } else if line_start.starts_with("#endif") {
2569 depth += 1;
2570 } else if line_start.starts_with("#if") {
2571 if depth > 0 {
2572 depth -= 1;
2573 } else {
2574 return Some(line_offset);
2575 }
2576 }
2577 }
2578 }
2579 None
2580}
2581
2582fn comment_delimiter_pair(
2583 map: &DisplaySnapshot,
2584 offset: MultiBufferOffset,
2585) -> Option<(Range<MultiBufferOffset>, Range<MultiBufferOffset>)> {
2586 let snapshot = map.buffer_snapshot();
2587 snapshot
2588 .text_object_ranges(offset..offset, TreeSitterOptions::default())
2589 .find_map(|(range, obj)| {
2590 if !matches!(obj, TextObject::InsideComment | TextObject::AroundComment)
2591 || !range.contains(&offset)
2592 {
2593 return None;
2594 }
2595
2596 let mut chars = snapshot.chars_at(range.start);
2597 if (Some('/'), Some('*')) != (chars.next(), chars.next()) {
2598 return None;
2599 }
2600
2601 let open_range = range.start..range.start + 2usize;
2602 let close_range = range.end - 2..range.end;
2603 Some((open_range, close_range))
2604 })
2605}
2606
2607fn matching(
2608 map: &DisplaySnapshot,
2609 display_point: DisplayPoint,
2610 match_quotes: bool,
2611) -> DisplayPoint {
2612 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
2613 let display_point = map.clip_at_line_end(display_point);
2614 let point = display_point.to_point(map);
2615 let offset = point.to_offset(&map.buffer_snapshot());
2616 let snapshot = map.buffer_snapshot();
2617
2618 // Ensure the range is contained by the current line.
2619 let mut line_end = map.next_line_boundary(point).0;
2620 let max_point = map.max_point().to_point(map);
2621
2622 // Only widen to EOF when the cursor is actually at EOF.
2623 // This avoids expanding a blank current line into start..EOF.
2624 if line_end == point && point == max_point {
2625 line_end = max_point;
2626 }
2627
2628 let line_range = map.prev_line_boundary(point).0..line_end;
2629 let line_range = line_range.start.to_offset(&map.buffer_snapshot())
2630 ..line_range.end.to_offset(&map.buffer_snapshot());
2631
2632 if let Some(preproc_range) = find_matching_c_preprocessor_directive(map, line_range, offset) {
2633 return preproc_range.to_display_point(map);
2634 }
2635
2636 if let Some((open_range, close_range)) = comment_delimiter_pair(map, offset) {
2637 if open_range.contains(&offset) {
2638 return close_range.start.to_display_point(map);
2639 }
2640
2641 if close_range.contains(&offset) {
2642 return open_range.start.to_display_point(map);
2643 }
2644 }
2645
2646 let is_quote_char = |ch: char| matches!(ch, '\'' | '"' | '`');
2647
2648 // The filter receives buffer-local ranges, not multibuffer offsets.
2649 let buffer_offset = snapshot
2650 .point_to_buffer_offset(offset)
2651 .map(|(_, buffer_offset)| buffer_offset);
2652
2653 let make_range_filter = |require_on_bracket: bool| {
2654 move |buffer: &language::BufferSnapshot,
2655 opening_range: Range<BufferOffset>,
2656 closing_range: Range<BufferOffset>| {
2657 if !match_quotes
2658 && buffer
2659 .chars_at(opening_range.start)
2660 .next()
2661 .is_some_and(is_quote_char)
2662 {
2663 return false;
2664 }
2665
2666 if require_on_bracket {
2667 // Attempt to find the smallest enclosing bracket range that also contains
2668 // the offset, which only happens if the cursor is currently in a bracket.
2669 buffer_offset.is_some_and(|buffer_offset| {
2670 opening_range.contains(&buffer_offset) || closing_range.contains(&buffer_offset)
2671 })
2672 } else {
2673 true
2674 }
2675 }
2676 };
2677
2678 let bracket_ranges = snapshot
2679 .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(true)))
2680 .or_else(|| {
2681 snapshot
2682 .innermost_enclosing_bracket_ranges(offset..offset, Some(&make_range_filter(false)))
2683 });
2684
2685 if let Some((opening_range, closing_range)) = bracket_ranges {
2686 let mut chars = map.buffer_snapshot().chars_at(offset);
2687 match chars.next() {
2688 Some('/') => {}
2689 _ => {
2690 if opening_range.contains(&offset) {
2691 return closing_range.start.to_display_point(map);
2692 } else if closing_range.contains(&offset) {
2693 return opening_range.start.to_display_point(map);
2694 }
2695 }
2696 }
2697 }
2698
2699 let line_range = map.prev_line_boundary(point).0..line_end;
2700 let visible_line_range =
2701 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
2702 let line_range = line_range.start.to_offset(&map.buffer_snapshot())
2703 ..line_range.end.to_offset(&map.buffer_snapshot());
2704 let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range);
2705 if let Some(ranges) = ranges {
2706 let mut closest_pair_destination = None;
2707 let mut closest_distance = usize::MAX;
2708
2709 for (open_range, close_range) in ranges {
2710 if !match_quotes
2711 && map
2712 .buffer_snapshot()
2713 .chars_at(open_range.start)
2714 .next()
2715 .is_some_and(is_quote_char)
2716 {
2717 continue;
2718 }
2719
2720 if map.buffer_snapshot().chars_at(open_range.start).next() == Some('<') {
2721 if offset > open_range.start && offset < close_range.start {
2722 let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2723 if (Some('/'), Some('>')) == (chars.next(), chars.next()) {
2724 return display_point;
2725 }
2726 if let Some(tag) = matching_tag(map, display_point) {
2727 return tag;
2728 }
2729 } else if close_range.contains(&offset) {
2730 return open_range.start.to_display_point(map);
2731 } else if open_range.contains(&offset) {
2732 return (close_range.end - 1).to_display_point(map);
2733 }
2734 }
2735
2736 if (open_range.contains(&offset) || open_range.start >= offset)
2737 && line_range.contains(&open_range.start)
2738 {
2739 let distance = open_range.start.saturating_sub(offset);
2740 if distance < closest_distance {
2741 closest_pair_destination = Some(close_range.start);
2742 closest_distance = distance;
2743 }
2744 }
2745
2746 if (close_range.contains(&offset) || close_range.start >= offset)
2747 && line_range.contains(&close_range.start)
2748 {
2749 let distance = close_range.start.saturating_sub(offset);
2750 if distance < closest_distance {
2751 closest_pair_destination = Some(open_range.start);
2752 closest_distance = distance;
2753 }
2754 }
2755
2756 continue;
2757 }
2758
2759 closest_pair_destination
2760 .map(|destination| destination.to_display_point(map))
2761 .unwrap_or_else(|| {
2762 find_matching_bracket_text_based(map, offset, line_range.clone())
2763 .map(|o| o.to_display_point(map))
2764 .unwrap_or(display_point)
2765 })
2766 } else {
2767 find_matching_bracket_text_based(map, offset, line_range)
2768 .map(|o| o.to_display_point(map))
2769 .unwrap_or(display_point)
2770 }
2771}
2772
2773// Go to {count} percentage in the file, on the first
2774// non-blank in the line linewise. To compute the new
2775// line number this formula is used:
2776// ({count} * number-of-lines + 99) / 100
2777//
2778// https://neovim.io/doc/user/motion.html#N%25
2779fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint {
2780 let total_lines = map.buffer_snapshot().max_point().row + 1;
2781 let target_line = (count * total_lines as usize).div_ceil(100);
2782 let target_point = DisplayPoint::new(
2783 DisplayRow(target_line.saturating_sub(1) as u32),
2784 point.column(),
2785 );
2786 map.clip_point(target_point, Bias::Left)
2787}
2788
2789fn unmatched_forward(
2790 map: &DisplaySnapshot,
2791 mut display_point: DisplayPoint,
2792 char: char,
2793 times: usize,
2794) -> DisplayPoint {
2795 for _ in 0..times {
2796 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245
2797 let point = display_point.to_point(map);
2798 let offset = point.to_offset(&map.buffer_snapshot());
2799
2800 let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2801 let Some(ranges) = ranges else { break };
2802 let mut closest_closing_destination = None;
2803 let mut closest_distance = usize::MAX;
2804
2805 for (_, close_range) in ranges {
2806 if close_range.start > offset {
2807 let mut chars = map.buffer_snapshot().chars_at(close_range.start);
2808 if Some(char) == chars.next() {
2809 let distance = close_range.start - offset;
2810 if distance < closest_distance {
2811 closest_closing_destination = Some(close_range.start);
2812 closest_distance = distance;
2813 continue;
2814 }
2815 }
2816 }
2817 }
2818
2819 let new_point = closest_closing_destination
2820 .map(|destination| destination.to_display_point(map))
2821 .unwrap_or(display_point);
2822 if new_point == display_point {
2823 break;
2824 }
2825 display_point = new_point;
2826 }
2827 display_point
2828}
2829
2830fn unmatched_backward(
2831 map: &DisplaySnapshot,
2832 mut display_point: DisplayPoint,
2833 char: char,
2834 times: usize,
2835) -> DisplayPoint {
2836 for _ in 0..times {
2837 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239
2838 let point = display_point.to_point(map);
2839 let offset = point.to_offset(&map.buffer_snapshot());
2840
2841 let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point);
2842 let Some(ranges) = ranges else {
2843 break;
2844 };
2845
2846 let mut closest_starting_destination = None;
2847 let mut closest_distance = usize::MAX;
2848
2849 for (start_range, _) in ranges {
2850 if start_range.start < offset {
2851 let mut chars = map.buffer_snapshot().chars_at(start_range.start);
2852 if Some(char) == chars.next() {
2853 let distance = offset - start_range.start;
2854 if distance < closest_distance {
2855 closest_starting_destination = Some(start_range.start);
2856 closest_distance = distance;
2857 continue;
2858 }
2859 }
2860 }
2861 }
2862
2863 let new_point = closest_starting_destination
2864 .map(|destination| destination.to_display_point(map))
2865 .unwrap_or(display_point);
2866 if new_point == display_point {
2867 break;
2868 } else {
2869 display_point = new_point;
2870 }
2871 }
2872 display_point
2873}
2874
2875fn find_forward(
2876 map: &DisplaySnapshot,
2877 from: DisplayPoint,
2878 before: bool,
2879 target: char,
2880 times: usize,
2881 mode: FindRange,
2882 smartcase: bool,
2883) -> Option<DisplayPoint> {
2884 let mut to = from;
2885 let mut found = false;
2886
2887 for _ in 0..times {
2888 found = false;
2889 let new_to = find_boundary(map, to, mode, &mut |_, right| {
2890 found = is_character_match(target, right, smartcase);
2891 found
2892 });
2893 if to == new_to {
2894 break;
2895 }
2896 to = new_to;
2897 }
2898
2899 if found {
2900 if before && to.column() > 0 {
2901 *to.column_mut() -= 1;
2902 Some(map.clip_point(to, Bias::Left))
2903 } else if before && to.row().0 > 0 {
2904 *to.row_mut() -= 1;
2905 *to.column_mut() = map.line(to.row()).len() as u32;
2906 Some(map.clip_point(to, Bias::Left))
2907 } else {
2908 Some(to)
2909 }
2910 } else {
2911 None
2912 }
2913}
2914
2915fn find_backward(
2916 map: &DisplaySnapshot,
2917 from: DisplayPoint,
2918 after: bool,
2919 target: char,
2920 times: usize,
2921 mode: FindRange,
2922 smartcase: bool,
2923) -> DisplayPoint {
2924 let mut to = from;
2925
2926 for _ in 0..times {
2927 let new_to = find_preceding_boundary_display_point(map, to, mode, &mut |_, right| {
2928 is_character_match(target, right, smartcase)
2929 });
2930 if to == new_to {
2931 break;
2932 }
2933 to = new_to;
2934 }
2935
2936 let next = map.buffer_snapshot().chars_at(to.to_point(map)).next();
2937 if next.is_some() && is_character_match(target, next.unwrap(), smartcase) {
2938 if after {
2939 *to.column_mut() += 1;
2940 map.clip_point(to, Bias::Right)
2941 } else {
2942 to
2943 }
2944 } else {
2945 from
2946 }
2947}
2948
2949/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true).
2950pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool {
2951 if smartcase {
2952 if target.is_uppercase() {
2953 target == other
2954 } else {
2955 target == other.to_ascii_lowercase()
2956 }
2957 } else {
2958 target == other
2959 }
2960}
2961
2962fn sneak(
2963 map: &DisplaySnapshot,
2964 from: DisplayPoint,
2965 first_target: char,
2966 second_target: char,
2967 times: usize,
2968 smartcase: bool,
2969) -> Option<DisplayPoint> {
2970 let mut to = from;
2971 let mut found = false;
2972
2973 for _ in 0..times {
2974 found = false;
2975 let new_to = find_boundary(
2976 map,
2977 movement::right(map, to),
2978 FindRange::MultiLine,
2979 &mut |left, right| {
2980 found = is_character_match(first_target, left, smartcase)
2981 && is_character_match(second_target, right, smartcase);
2982 found
2983 },
2984 );
2985 if to == new_to {
2986 break;
2987 }
2988 to = new_to;
2989 }
2990
2991 if found {
2992 Some(movement::left(map, to))
2993 } else {
2994 None
2995 }
2996}
2997
2998fn sneak_backward(
2999 map: &DisplaySnapshot,
3000 from: DisplayPoint,
3001 first_target: char,
3002 second_target: char,
3003 times: usize,
3004 smartcase: bool,
3005) -> Option<DisplayPoint> {
3006 let mut to = from;
3007 let mut found = false;
3008
3009 for _ in 0..times {
3010 found = false;
3011 let new_to = find_preceding_boundary_display_point(
3012 map,
3013 to,
3014 FindRange::MultiLine,
3015 &mut |left, right| {
3016 found = is_character_match(first_target, left, smartcase)
3017 && is_character_match(second_target, right, smartcase);
3018 found
3019 },
3020 );
3021 if to == new_to {
3022 break;
3023 }
3024 to = new_to;
3025 }
3026
3027 if found {
3028 Some(movement::left(map, to))
3029 } else {
3030 None
3031 }
3032}
3033
3034fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
3035 let correct_line = map.start_of_relative_buffer_row(point, times as isize);
3036 first_non_whitespace(map, false, correct_line)
3037}
3038
3039fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
3040 let correct_line = map.start_of_relative_buffer_row(point, -(times as isize));
3041 first_non_whitespace(map, false, correct_line)
3042}
3043
3044fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
3045 let correct_line = map.start_of_relative_buffer_row(point, 0);
3046 right(map, correct_line, times.saturating_sub(1))
3047}
3048
3049pub(crate) fn next_line_end(
3050 map: &DisplaySnapshot,
3051 mut point: DisplayPoint,
3052 times: usize,
3053) -> DisplayPoint {
3054 if times > 1 {
3055 point = map.start_of_relative_buffer_row(point, times as isize - 1);
3056 }
3057 end_of_line(map, false, point, 1)
3058}
3059
3060fn window_top(
3061 map: &DisplaySnapshot,
3062 point: DisplayPoint,
3063 text_layout_details: &TextLayoutDetails,
3064 mut times: usize,
3065) -> (DisplayPoint, SelectionGoal) {
3066 let first_visible_line = text_layout_details
3067 .scroll_anchor
3068 .scroll_top_display_point(map);
3069
3070 if first_visible_line.row() != DisplayRow(0)
3071 && text_layout_details.vertical_scroll_margin as usize > times
3072 {
3073 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
3074 }
3075
3076 if let Some(visible_rows) = text_layout_details.visible_rows {
3077 let bottom_row = first_visible_line.row().0 + visible_rows as u32;
3078 let new_row = (first_visible_line.row().0 + (times as u32))
3079 .min(bottom_row)
3080 .min(map.max_point().row().0);
3081 let new_col = point.column().min(map.line_len(first_visible_line.row()));
3082
3083 let new_point = DisplayPoint::new(DisplayRow(new_row), new_col);
3084 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
3085 } else {
3086 let new_row =
3087 DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0));
3088 let new_col = point.column().min(map.line_len(first_visible_line.row()));
3089
3090 let new_point = DisplayPoint::new(new_row, new_col);
3091 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
3092 }
3093}
3094
3095fn window_middle(
3096 map: &DisplaySnapshot,
3097 point: DisplayPoint,
3098 text_layout_details: &TextLayoutDetails,
3099) -> (DisplayPoint, SelectionGoal) {
3100 if let Some(visible_rows) = text_layout_details.visible_rows {
3101 let first_visible_line = text_layout_details
3102 .scroll_anchor
3103 .scroll_top_display_point(map);
3104
3105 let max_visible_rows =
3106 (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0);
3107
3108 let new_row =
3109 (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0);
3110 let new_row = DisplayRow(new_row);
3111 let new_col = point.column().min(map.line_len(new_row));
3112 let new_point = DisplayPoint::new(new_row, new_col);
3113 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
3114 } else {
3115 (point, SelectionGoal::None)
3116 }
3117}
3118
3119fn window_bottom(
3120 map: &DisplaySnapshot,
3121 point: DisplayPoint,
3122 text_layout_details: &TextLayoutDetails,
3123 mut times: usize,
3124) -> (DisplayPoint, SelectionGoal) {
3125 if let Some(visible_rows) = text_layout_details.visible_rows {
3126 let first_visible_line = text_layout_details
3127 .scroll_anchor
3128 .scroll_top_display_point(map);
3129 let bottom_row = first_visible_line.row().0
3130 + (visible_rows + text_layout_details.scroll_anchor.scroll_anchor.offset.y - 1.).floor()
3131 as u32;
3132 if bottom_row < map.max_point().row().0
3133 && text_layout_details.vertical_scroll_margin as usize > times
3134 {
3135 times = text_layout_details.vertical_scroll_margin.ceil() as usize;
3136 }
3137 let bottom_row_capped = bottom_row.min(map.max_point().row().0);
3138 let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0
3139 {
3140 first_visible_line.row()
3141 } else {
3142 DisplayRow(bottom_row_capped.saturating_sub(times as u32))
3143 };
3144 let new_col = point.column().min(map.line_len(new_row));
3145 let new_point = DisplayPoint::new(new_row, new_col);
3146 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
3147 } else {
3148 (point, SelectionGoal::None)
3149 }
3150}
3151
3152fn method_motion(
3153 map: &DisplaySnapshot,
3154 mut display_point: DisplayPoint,
3155 times: usize,
3156 direction: Direction,
3157 is_start: bool,
3158) -> DisplayPoint {
3159 let snapshot = map.buffer_snapshot();
3160 if snapshot.as_singleton().is_none() {
3161 return display_point;
3162 }
3163
3164 for _ in 0..times {
3165 let offset = map
3166 .display_point_to_point(display_point, Bias::Left)
3167 .to_offset(&snapshot);
3168 let range = if direction == Direction::Prev {
3169 MultiBufferOffset(0)..offset
3170 } else {
3171 offset..snapshot.len()
3172 };
3173
3174 let possibilities = snapshot
3175 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4))
3176 .filter_map(|(range, object)| {
3177 if !matches!(object, language::TextObject::AroundFunction) {
3178 return None;
3179 }
3180
3181 let relevant = if is_start { range.start } else { range.end };
3182 if direction == Direction::Prev && relevant < offset {
3183 Some(relevant)
3184 } else if direction == Direction::Next && relevant > offset + 1usize {
3185 Some(relevant)
3186 } else {
3187 None
3188 }
3189 });
3190
3191 let dest = if direction == Direction::Prev {
3192 possibilities.max().unwrap_or(offset)
3193 } else {
3194 possibilities.min().unwrap_or(offset)
3195 };
3196 let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
3197 if new_point == display_point {
3198 break;
3199 }
3200 display_point = new_point;
3201 }
3202 display_point
3203}
3204
3205fn comment_motion(
3206 map: &DisplaySnapshot,
3207 mut display_point: DisplayPoint,
3208 times: usize,
3209 direction: Direction,
3210) -> DisplayPoint {
3211 let snapshot = map.buffer_snapshot();
3212 if snapshot.as_singleton().is_none() {
3213 return display_point;
3214 }
3215
3216 for _ in 0..times {
3217 let offset = map
3218 .display_point_to_point(display_point, Bias::Left)
3219 .to_offset(&snapshot);
3220 let range = if direction == Direction::Prev {
3221 MultiBufferOffset(0)..offset
3222 } else {
3223 offset..snapshot.len()
3224 };
3225
3226 let possibilities = snapshot
3227 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6))
3228 .filter_map(|(range, object)| {
3229 if !matches!(object, language::TextObject::AroundComment) {
3230 return None;
3231 }
3232
3233 let relevant = if direction == Direction::Prev {
3234 range.start
3235 } else {
3236 range.end
3237 };
3238 if direction == Direction::Prev && relevant < offset {
3239 Some(relevant)
3240 } else if direction == Direction::Next && relevant > offset + 1usize {
3241 Some(relevant)
3242 } else {
3243 None
3244 }
3245 });
3246
3247 let dest = if direction == Direction::Prev {
3248 possibilities.max().unwrap_or(offset)
3249 } else {
3250 possibilities.min().unwrap_or(offset)
3251 };
3252 let new_point = map.clip_point(dest.to_display_point(map), Bias::Left);
3253 if new_point == display_point {
3254 break;
3255 }
3256 display_point = new_point;
3257 }
3258
3259 display_point
3260}
3261
3262fn section_motion(
3263 map: &DisplaySnapshot,
3264 mut display_point: DisplayPoint,
3265 times: usize,
3266 direction: Direction,
3267 is_start: bool,
3268) -> DisplayPoint {
3269 if map.buffer_snapshot().as_singleton().is_some() {
3270 for _ in 0..times {
3271 let offset = map
3272 .display_point_to_point(display_point, Bias::Left)
3273 .to_offset(&map.buffer_snapshot());
3274 let range = if direction == Direction::Prev {
3275 MultiBufferOffset(0)..offset
3276 } else {
3277 offset..map.buffer_snapshot().len()
3278 };
3279
3280 // we set a max start depth here because we want a section to only be "top level"
3281 // similar to vim's default of '{' in the first column.
3282 // (and without it, ]] at the start of editor.rs is -very- slow)
3283 let mut possibilities = map
3284 .buffer_snapshot()
3285 .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3))
3286 .filter(|(_, object)| {
3287 matches!(
3288 object,
3289 language::TextObject::AroundClass | language::TextObject::AroundFunction
3290 )
3291 })
3292 .collect::<Vec<_>>();
3293 possibilities.sort_by_key(|(range_a, _)| range_a.start);
3294 let mut prev_end = None;
3295 let possibilities = possibilities.into_iter().filter_map(|(range, t)| {
3296 if t == language::TextObject::AroundFunction
3297 && prev_end.is_some_and(|prev_end| prev_end > range.start)
3298 {
3299 return None;
3300 }
3301 prev_end = Some(range.end);
3302
3303 let relevant = if is_start { range.start } else { range.end };
3304 if direction == Direction::Prev && relevant < offset {
3305 Some(relevant)
3306 } else if direction == Direction::Next && relevant > offset + 1usize {
3307 Some(relevant)
3308 } else {
3309 None
3310 }
3311 });
3312
3313 let offset = if direction == Direction::Prev {
3314 possibilities.max().unwrap_or(MultiBufferOffset(0))
3315 } else {
3316 possibilities.min().unwrap_or(map.buffer_snapshot().len())
3317 };
3318
3319 let new_point = map.clip_point(offset.to_display_point(map), Bias::Left);
3320 if new_point == display_point {
3321 break;
3322 }
3323 display_point = new_point;
3324 }
3325 return display_point;
3326 };
3327
3328 for _ in 0..times {
3329 let next_point = if is_start {
3330 movement::start_of_excerpt(map, display_point, direction)
3331 } else {
3332 movement::end_of_excerpt(map, display_point, direction)
3333 };
3334 if next_point == display_point {
3335 break;
3336 }
3337 display_point = next_point;
3338 }
3339
3340 display_point
3341}
3342
3343fn matches_indent_type(
3344 target_indent: &text::LineIndent,
3345 current_indent: &text::LineIndent,
3346 indent_type: IndentType,
3347) -> bool {
3348 match indent_type {
3349 IndentType::Lesser => {
3350 target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs
3351 }
3352 IndentType::Greater => {
3353 target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs
3354 }
3355 IndentType::Same => {
3356 target_indent.spaces == current_indent.spaces
3357 && target_indent.tabs == current_indent.tabs
3358 }
3359 }
3360}
3361
3362fn indent_motion(
3363 map: &DisplaySnapshot,
3364 mut display_point: DisplayPoint,
3365 times: usize,
3366 direction: Direction,
3367 indent_type: IndentType,
3368) -> DisplayPoint {
3369 let buffer_point = map.display_point_to_point(display_point, Bias::Left);
3370 let current_row = MultiBufferRow(buffer_point.row);
3371 let current_indent = map.line_indent_for_buffer_row(current_row);
3372 if current_indent.is_line_empty() {
3373 return display_point;
3374 }
3375 let max_row = map.max_point().to_point(map).row;
3376
3377 for _ in 0..times {
3378 let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row;
3379
3380 let target_row = match direction {
3381 Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| {
3382 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3383 !indent.is_line_empty()
3384 && matches_indent_type(&indent, ¤t_indent, indent_type)
3385 }),
3386 Direction::Prev => (0..current_buffer_row).rev().find(|&row| {
3387 let indent = map.line_indent_for_buffer_row(MultiBufferRow(row));
3388 !indent.is_line_empty()
3389 && matches_indent_type(&indent, ¤t_indent, indent_type)
3390 }),
3391 }
3392 .unwrap_or(current_buffer_row);
3393
3394 let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right);
3395 let new_point = first_non_whitespace(map, false, new_point);
3396 if new_point == display_point {
3397 break;
3398 }
3399 display_point = new_point;
3400 }
3401 display_point
3402}
3403
3404#[cfg(test)]
3405mod test {
3406
3407 use crate::{
3408 motion::Matching,
3409 state::Mode,
3410 test::{NeovimBackedTestContext, VimTestContext},
3411 };
3412 use editor::{
3413 Editor, EditorMode, Inlay, MultiBuffer, test::editor_test_context::EditorTestContext,
3414 };
3415 use gpui::KeyBinding;
3416 use indoc::indoc;
3417 use language::Point;
3418 use multi_buffer::MultiBufferRow;
3419
3420 #[gpui::test]
3421 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
3422 let mut cx = NeovimBackedTestContext::new(cx).await;
3423
3424 let initial_state = indoc! {r"ˇabc
3425 def
3426
3427 paragraph
3428 the second
3429
3430
3431
3432 third and
3433 final"};
3434
3435 // goes down once
3436 cx.set_shared_state(initial_state).await;
3437 cx.simulate_shared_keystrokes("}").await;
3438 cx.shared_state().await.assert_eq(indoc! {r"abc
3439 def
3440 ˇ
3441 paragraph
3442 the second
3443
3444
3445
3446 third and
3447 final"});
3448
3449 // goes up once
3450 cx.simulate_shared_keystrokes("{").await;
3451 cx.shared_state().await.assert_eq(initial_state);
3452
3453 // goes down twice
3454 cx.simulate_shared_keystrokes("2 }").await;
3455 cx.shared_state().await.assert_eq(indoc! {r"abc
3456 def
3457
3458 paragraph
3459 the second
3460 ˇ
3461
3462
3463 third and
3464 final"});
3465
3466 // goes down over multiple blanks
3467 cx.simulate_shared_keystrokes("}").await;
3468 cx.shared_state().await.assert_eq(indoc! {r"abc
3469 def
3470
3471 paragraph
3472 the second
3473
3474
3475
3476 third and
3477 finaˇl"});
3478
3479 // goes up twice
3480 cx.simulate_shared_keystrokes("2 {").await;
3481 cx.shared_state().await.assert_eq(indoc! {r"abc
3482 def
3483 ˇ
3484 paragraph
3485 the second
3486
3487
3488
3489 third and
3490 final"});
3491 }
3492
3493 #[gpui::test]
3494 async fn test_paragraph_motion_with_whitespace_lines(cx: &mut gpui::TestAppContext) {
3495 let mut cx = NeovimBackedTestContext::new(cx).await;
3496
3497 // Test that whitespace-only lines are NOT treated as paragraph boundaries
3498 // Per vim's :help paragraph - only truly empty lines are boundaries
3499 // Line 2 has 4 spaces (whitespace-only), line 4 is truly empty
3500 cx.set_shared_state("ˇfirst\n \nstill first\n\nsecond")
3501 .await;
3502 cx.simulate_shared_keystrokes("}").await;
3503
3504 // Should skip whitespace-only line and stop at truly empty line
3505 let mut shared_state = cx.shared_state().await;
3506 shared_state.assert_eq("first\n \nstill first\nˇ\nsecond");
3507 shared_state.assert_matches();
3508
3509 // Should go back to original position
3510 cx.simulate_shared_keystrokes("{").await;
3511 let mut shared_state = cx.shared_state().await;
3512 shared_state.assert_eq("ˇfirst\n \nstill first\n\nsecond");
3513 shared_state.assert_matches();
3514 }
3515
3516 #[gpui::test]
3517 async fn test_matching(cx: &mut gpui::TestAppContext) {
3518 let mut cx = NeovimBackedTestContext::new(cx).await;
3519
3520 cx.set_shared_state(indoc! {r"func ˇ(a string) {
3521 do(something(with<Types>.and_arrays[0, 2]))
3522 }"})
3523 .await;
3524 cx.simulate_shared_keystrokes("%").await;
3525 cx.shared_state()
3526 .await
3527 .assert_eq(indoc! {r"func (a stringˇ) {
3528 do(something(with<Types>.and_arrays[0, 2]))
3529 }"});
3530
3531 // test it works on the last character of the line
3532 cx.set_shared_state(indoc! {r"func (a string) ˇ{
3533 do(something(with<Types>.and_arrays[0, 2]))
3534 }"})
3535 .await;
3536 cx.simulate_shared_keystrokes("%").await;
3537 cx.shared_state()
3538 .await
3539 .assert_eq(indoc! {r"func (a string) {
3540 do(something(with<Types>.and_arrays[0, 2]))
3541 ˇ}"});
3542
3543 // test it works on immediate nesting
3544 cx.set_shared_state("ˇ{()}").await;
3545 cx.simulate_shared_keystrokes("%").await;
3546 cx.shared_state().await.assert_eq("{()ˇ}");
3547 cx.simulate_shared_keystrokes("%").await;
3548 cx.shared_state().await.assert_eq("ˇ{()}");
3549
3550 // test it works on immediate nesting inside braces
3551 cx.set_shared_state("{\n ˇ{()}\n}").await;
3552 cx.simulate_shared_keystrokes("%").await;
3553 cx.shared_state().await.assert_eq("{\n {()ˇ}\n}");
3554
3555 // test it jumps to the next paren on a line
3556 cx.set_shared_state("func ˇboop() {\n}").await;
3557 cx.simulate_shared_keystrokes("%").await;
3558 cx.shared_state().await.assert_eq("func boop(ˇ) {\n}");
3559 }
3560
3561 #[gpui::test]
3562 async fn test_matching_in_multibuffer(cx: &mut gpui::TestAppContext) {
3563 let mut cx = VimTestContext::new(cx, true).await;
3564
3565 let (editor, cx) = cx.add_window_view(|window, cx| {
3566 let multi_buffer = MultiBuffer::build_multi(
3567 [
3568 (
3569 "fn a() {\n let x = 1;\n}\n",
3570 vec![Point::row_range(0..3)],
3571 ),
3572 (
3573 "fn b() {\n let y = 2;\n}\n",
3574 vec![Point::row_range(0..3)],
3575 ),
3576 ],
3577 cx,
3578 );
3579
3580 let buffer_ids = multi_buffer
3581 .read(cx)
3582 .snapshot(cx)
3583 .excerpts()
3584 .map(|excerpt| excerpt.context.start.buffer_id)
3585 .collect::<Vec<_>>();
3586
3587 for buffer_id in buffer_ids {
3588 if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_id) {
3589 buffer.update(cx, |buffer, cx| {
3590 buffer.set_language(Some(language::rust_lang()), cx);
3591 });
3592 }
3593 }
3594
3595 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
3596 });
3597
3598 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
3599
3600 cx.simulate_keystrokes("j j j j f {");
3601 cx.assert_excerpts_with_selections(indoc! {"
3602 [EXCERPT]
3603 fn a() {
3604 let x = 1;
3605 }
3606 [EXCERPT]
3607 fn b() ˇ{
3608 let y = 2;
3609 }
3610 "
3611 });
3612
3613 cx.simulate_keystrokes("%");
3614 cx.assert_excerpts_with_selections(indoc! {"
3615 [EXCERPT]
3616 fn a() {
3617 let x = 1;
3618 }
3619 [EXCERPT]
3620 fn b() {
3621 let y = 2;
3622 ˇ}
3623 "
3624 });
3625
3626 cx.simulate_keystrokes("%");
3627 cx.assert_excerpts_with_selections(indoc! {"
3628 [EXCERPT]
3629 fn a() {
3630 let x = 1;
3631 }
3632 [EXCERPT]
3633 fn b() ˇ{
3634 let y = 2;
3635 }
3636 "
3637 });
3638 }
3639
3640 #[gpui::test]
3641 async fn test_matching_quotes_disabled(cx: &mut gpui::TestAppContext) {
3642 let mut cx = NeovimBackedTestContext::new(cx).await;
3643
3644 // Bind % to Matching with match_quotes: false to match Neovim's behavior
3645 // (Neovim's % doesn't match quotes by default)
3646 cx.update(|_, cx| {
3647 cx.bind_keys([KeyBinding::new(
3648 "%",
3649 Matching {
3650 match_quotes: false,
3651 },
3652 None,
3653 )]);
3654 });
3655
3656 cx.set_shared_state("one {two 'thˇree' four}").await;
3657 cx.simulate_shared_keystrokes("%").await;
3658 cx.shared_state().await.assert_eq("one ˇ{two 'three' four}");
3659
3660 cx.set_shared_state("'hello wˇorld'").await;
3661 cx.simulate_shared_keystrokes("%").await;
3662 cx.shared_state().await.assert_eq("'hello wˇorld'");
3663
3664 cx.set_shared_state(r#"func ("teˇst") {}"#).await;
3665 cx.simulate_shared_keystrokes("%").await;
3666 cx.shared_state().await.assert_eq(r#"func ˇ("test") {}"#);
3667
3668 cx.set_shared_state("ˇ'hello'").await;
3669 cx.simulate_shared_keystrokes("%").await;
3670 cx.shared_state().await.assert_eq("ˇ'hello'");
3671
3672 cx.set_shared_state("'helloˇ'").await;
3673 cx.simulate_shared_keystrokes("%").await;
3674 cx.shared_state().await.assert_eq("'helloˇ'");
3675
3676 cx.set_shared_state(indoc! {r"func (a string) {
3677 do('somethiˇng'))
3678 }"})
3679 .await;
3680 cx.simulate_shared_keystrokes("%").await;
3681 cx.shared_state()
3682 .await
3683 .assert_eq(indoc! {r"func (a string) {
3684 doˇ('something'))
3685 }"});
3686 }
3687
3688 #[gpui::test]
3689 async fn test_matching_quotes_enabled(cx: &mut gpui::TestAppContext) {
3690 let mut cx = VimTestContext::new_markdown_with_rust(cx).await;
3691
3692 // Test default behavior (match_quotes: true as configured in keymap/vim.json)
3693 cx.set_state("one {two 'thˇree' four}", Mode::Normal);
3694 cx.simulate_keystrokes("%");
3695 cx.assert_state("one {two ˇ'three' four}", Mode::Normal);
3696
3697 cx.set_state("'hello wˇorld'", Mode::Normal);
3698 cx.simulate_keystrokes("%");
3699 cx.assert_state("ˇ'hello world'", Mode::Normal);
3700
3701 cx.set_state(r#"func ('teˇst') {}"#, Mode::Normal);
3702 cx.simulate_keystrokes("%");
3703 cx.assert_state(r#"func (ˇ'test') {}"#, Mode::Normal);
3704
3705 cx.set_state("ˇ'hello'", Mode::Normal);
3706 cx.simulate_keystrokes("%");
3707 cx.assert_state("'helloˇ'", Mode::Normal);
3708
3709 cx.set_state("'helloˇ'", Mode::Normal);
3710 cx.simulate_keystrokes("%");
3711 cx.assert_state("ˇ'hello'", Mode::Normal);
3712
3713 cx.set_state(
3714 indoc! {r"func (a string) {
3715 do('somethiˇng'))
3716 }"},
3717 Mode::Normal,
3718 );
3719 cx.simulate_keystrokes("%");
3720 cx.assert_state(
3721 indoc! {r"func (a string) {
3722 do(ˇ'something'))
3723 }"},
3724 Mode::Normal,
3725 );
3726 }
3727
3728 #[gpui::test]
3729 async fn test_matching_comments(cx: &mut gpui::TestAppContext) {
3730 let mut cx = NeovimBackedTestContext::new(cx).await;
3731
3732 cx.set_shared_state(indoc! {r"ˇ/*
3733 this is a comment
3734 */"})
3735 .await;
3736 cx.simulate_shared_keystrokes("%").await;
3737 cx.shared_state().await.assert_eq(indoc! {r"/*
3738 this is a comment
3739 ˇ*/"});
3740 cx.simulate_shared_keystrokes("%").await;
3741 cx.shared_state().await.assert_eq(indoc! {r"ˇ/*
3742 this is a comment
3743 */"});
3744 cx.simulate_shared_keystrokes("%").await;
3745 cx.shared_state().await.assert_eq(indoc! {r"/*
3746 this is a comment
3747 ˇ*/"});
3748 cx.simulate_shared_keystrokes("k %").await;
3749 cx.shared_state().await.assert_eq(indoc! {r"/*
3750 ˇ this is a comment
3751 */"});
3752
3753 cx.set_shared_state("ˇ// comment").await;
3754 cx.simulate_shared_keystrokes("%").await;
3755 cx.shared_state().await.assert_eq("ˇ// comment");
3756 }
3757
3758 #[gpui::test]
3759 async fn test_matching_preprocessor_directives(cx: &mut gpui::TestAppContext) {
3760 let mut cx = NeovimBackedTestContext::new(cx).await;
3761
3762 cx.set_shared_state(indoc! {r"
3763 #ˇif
3764
3765 #else
3766
3767 #endif
3768 "})
3769 .await;
3770 cx.simulate_shared_keystrokes("%").await;
3771 cx.shared_state().await.assert_eq(indoc! {r"
3772 #if
3773
3774 ˇ#else
3775
3776 #endif
3777 "});
3778
3779 cx.simulate_shared_keystrokes("%").await;
3780 cx.shared_state().await.assert_eq(indoc! {r"
3781 #if
3782
3783 #else
3784
3785 ˇ#endif
3786 "});
3787
3788 cx.simulate_shared_keystrokes("%").await;
3789 cx.shared_state().await.assert_eq(indoc! {r"
3790 ˇ#if
3791
3792 #else
3793
3794 #endif
3795 "});
3796
3797 cx.set_shared_state(indoc! {r"
3798 #ˇif
3799 #if
3800
3801 #else
3802
3803 #endif
3804
3805 #else
3806
3807 #endif
3808 "})
3809 .await;
3810
3811 cx.simulate_shared_keystrokes("%").await;
3812 cx.shared_state().await.assert_eq(indoc! {r"
3813 #if
3814 #if
3815
3816 #else
3817
3818 #endif
3819
3820 ˇ#else
3821
3822 #endif
3823 "});
3824
3825 cx.simulate_shared_keystrokes("% %").await;
3826 cx.shared_state().await.assert_eq(indoc! {r"
3827 ˇ#if
3828 #if
3829
3830 #else
3831
3832 #endif
3833
3834 #else
3835
3836 #endif
3837 "});
3838 cx.simulate_shared_keystrokes("j % % %").await;
3839 cx.shared_state().await.assert_eq(indoc! {r"
3840 #if
3841 ˇ#if
3842
3843 #else
3844
3845 #endif
3846
3847 #else
3848
3849 #endif
3850 "});
3851
3852 cx.set_shared_state(indoc! {r"
3853 #if definedˇ(something)
3854
3855 #endif
3856 "})
3857 .await;
3858 cx.simulate_shared_keystrokes("%").await;
3859 cx.shared_state().await.assert_eq(indoc! {r"
3860 #if defined(somethingˇ)
3861
3862 #endif
3863 "});
3864 cx.simulate_shared_keystrokes("0 %").await;
3865 cx.shared_state().await.assert_eq(indoc! {r"
3866 #if defined(something)
3867
3868 ˇ#endif
3869 "});
3870 }
3871
3872 #[gpui::test]
3873 async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) {
3874 let mut cx = NeovimBackedTestContext::new(cx).await;
3875
3876 // test it works with curly braces
3877 cx.set_shared_state(indoc! {r"func (a string) {
3878 do(something(with<Types>.anˇd_arrays[0, 2]))
3879 }"})
3880 .await;
3881 cx.simulate_shared_keystrokes("] }").await;
3882 cx.shared_state()
3883 .await
3884 .assert_eq(indoc! {r"func (a string) {
3885 do(something(with<Types>.and_arrays[0, 2]))
3886 ˇ}"});
3887
3888 // test it works with brackets
3889 cx.set_shared_state(indoc! {r"func (a string) {
3890 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3891 }"})
3892 .await;
3893 cx.simulate_shared_keystrokes("] )").await;
3894 cx.shared_state()
3895 .await
3896 .assert_eq(indoc! {r"func (a string) {
3897 do(something(with<Types>.and_arrays[0, 2])ˇ)
3898 }"});
3899
3900 cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"})
3901 .await;
3902 cx.simulate_shared_keystrokes("] )").await;
3903 cx.shared_state()
3904 .await
3905 .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"});
3906
3907 // test it works on immediate nesting
3908 cx.set_shared_state("{ˇ {}{}}").await;
3909 cx.simulate_shared_keystrokes("] }").await;
3910 cx.shared_state().await.assert_eq("{ {}{}ˇ}");
3911 cx.set_shared_state("(ˇ ()())").await;
3912 cx.simulate_shared_keystrokes("] )").await;
3913 cx.shared_state().await.assert_eq("( ()()ˇ)");
3914
3915 // test it works on immediate nesting inside braces
3916 cx.set_shared_state("{\n ˇ {()}\n}").await;
3917 cx.simulate_shared_keystrokes("] }").await;
3918 cx.shared_state().await.assert_eq("{\n {()}\nˇ}");
3919 cx.set_shared_state("(\n ˇ {()}\n)").await;
3920 cx.simulate_shared_keystrokes("] )").await;
3921 cx.shared_state().await.assert_eq("(\n {()}\nˇ)");
3922 }
3923
3924 #[gpui::test]
3925 async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) {
3926 let mut cx = NeovimBackedTestContext::new(cx).await;
3927
3928 // test it works with curly braces
3929 cx.set_shared_state(indoc! {r"func (a string) {
3930 do(something(with<Types>.anˇd_arrays[0, 2]))
3931 }"})
3932 .await;
3933 cx.simulate_shared_keystrokes("[ {").await;
3934 cx.shared_state()
3935 .await
3936 .assert_eq(indoc! {r"func (a string) ˇ{
3937 do(something(with<Types>.and_arrays[0, 2]))
3938 }"});
3939
3940 // test it works with brackets
3941 cx.set_shared_state(indoc! {r"func (a string) {
3942 do(somethiˇng(with<Types>.and_arrays[0, 2]))
3943 }"})
3944 .await;
3945 cx.simulate_shared_keystrokes("[ (").await;
3946 cx.shared_state()
3947 .await
3948 .assert_eq(indoc! {r"func (a string) {
3949 doˇ(something(with<Types>.and_arrays[0, 2]))
3950 }"});
3951
3952 // test it works on immediate nesting
3953 cx.set_shared_state("{{}{} ˇ }").await;
3954 cx.simulate_shared_keystrokes("[ {").await;
3955 cx.shared_state().await.assert_eq("ˇ{{}{} }");
3956 cx.set_shared_state("(()() ˇ )").await;
3957 cx.simulate_shared_keystrokes("[ (").await;
3958 cx.shared_state().await.assert_eq("ˇ(()() )");
3959
3960 // test it works on immediate nesting inside braces
3961 cx.set_shared_state("{\n {()} ˇ\n}").await;
3962 cx.simulate_shared_keystrokes("[ {").await;
3963 cx.shared_state().await.assert_eq("ˇ{\n {()} \n}");
3964 cx.set_shared_state("(\n {()} ˇ\n)").await;
3965 cx.simulate_shared_keystrokes("[ (").await;
3966 cx.shared_state().await.assert_eq("ˇ(\n {()} \n)");
3967 }
3968
3969 #[gpui::test]
3970 async fn test_unmatched_forward_markdown(cx: &mut gpui::TestAppContext) {
3971 let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
3972
3973 cx.neovim.exec("set filetype=markdown").await;
3974
3975 cx.set_shared_state(indoc! {r"
3976 ```rs
3977 impl Worktree {
3978 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3979 ˇ }
3980 }
3981 ```
3982 "})
3983 .await;
3984 cx.simulate_shared_keystrokes("] }").await;
3985 cx.shared_state().await.assert_eq(indoc! {r"
3986 ```rs
3987 impl Worktree {
3988 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3989 ˇ}
3990 }
3991 ```
3992 "});
3993
3994 cx.set_shared_state(indoc! {r"
3995 ```rs
3996 impl Worktree {
3997 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
3998 } ˇ
3999 }
4000 ```
4001 "})
4002 .await;
4003 cx.simulate_shared_keystrokes("] }").await;
4004 cx.shared_state().await.assert_eq(indoc! {r"
4005 ```rs
4006 impl Worktree {
4007 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
4008 } •
4009 ˇ}
4010 ```
4011 "});
4012 }
4013
4014 #[gpui::test]
4015 async fn test_unmatched_backward_markdown(cx: &mut gpui::TestAppContext) {
4016 let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await;
4017
4018 cx.neovim.exec("set filetype=markdown").await;
4019
4020 cx.set_shared_state(indoc! {r"
4021 ```rs
4022 impl Worktree {
4023 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
4024 ˇ }
4025 }
4026 ```
4027 "})
4028 .await;
4029 cx.simulate_shared_keystrokes("[ {").await;
4030 cx.shared_state().await.assert_eq(indoc! {r"
4031 ```rs
4032 impl Worktree {
4033 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
4034 }
4035 }
4036 ```
4037 "});
4038
4039 cx.set_shared_state(indoc! {r"
4040 ```rs
4041 impl Worktree {
4042 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
4043 } ˇ
4044 }
4045 ```
4046 "})
4047 .await;
4048 cx.simulate_shared_keystrokes("[ {").await;
4049 cx.shared_state().await.assert_eq(indoc! {r"
4050 ```rs
4051 impl Worktree ˇ{
4052 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
4053 } •
4054 }
4055 ```
4056 "});
4057 }
4058
4059 #[gpui::test]
4060 async fn test_matching_tags(cx: &mut gpui::TestAppContext) {
4061 let mut cx = NeovimBackedTestContext::new_html(cx).await;
4062
4063 cx.neovim.exec("set filetype=html").await;
4064
4065 cx.set_shared_state(indoc! {r"<bˇody></body>"}).await;
4066 cx.simulate_shared_keystrokes("%").await;
4067 cx.shared_state()
4068 .await
4069 .assert_eq(indoc! {r"<body><ˇ/body>"});
4070 cx.simulate_shared_keystrokes("%").await;
4071
4072 // test jumping backwards
4073 cx.shared_state()
4074 .await
4075 .assert_eq(indoc! {r"<ˇbody></body>"});
4076
4077 // test self-closing tags
4078 cx.set_shared_state(indoc! {r"<a><bˇr/></a>"}).await;
4079 cx.simulate_shared_keystrokes("%").await;
4080 cx.shared_state().await.assert_eq(indoc! {r"<a><bˇr/></a>"});
4081
4082 // test tag with attributes
4083 cx.set_shared_state(indoc! {r"<div class='test' ˇid='main'>
4084 </div>
4085 "})
4086 .await;
4087 cx.simulate_shared_keystrokes("%").await;
4088 cx.shared_state()
4089 .await
4090 .assert_eq(indoc! {r"<div class='test' id='main'>
4091 <ˇ/div>
4092 "});
4093
4094 // test multi-line self-closing tag
4095 cx.set_shared_state(indoc! {r#"<a>
4096 <br
4097 test = "test"
4098 /ˇ>
4099 </a>"#})
4100 .await;
4101 cx.simulate_shared_keystrokes("%").await;
4102 cx.shared_state().await.assert_eq(indoc! {r#"<a>
4103 ˇ<br
4104 test = "test"
4105 />
4106 </a>"#});
4107
4108 // test nested closing tag
4109 cx.set_shared_state(indoc! {r#"<html>
4110 <bˇody>
4111 </body>
4112 </html>"#})
4113 .await;
4114 cx.simulate_shared_keystrokes("%").await;
4115 cx.shared_state().await.assert_eq(indoc! {r#"<html>
4116 <body>
4117 <ˇ/body>
4118 </html>"#});
4119 cx.simulate_shared_keystrokes("%").await;
4120 cx.shared_state().await.assert_eq(indoc! {r#"<html>
4121 <ˇbody>
4122 </body>
4123 </html>"#});
4124 }
4125
4126 #[gpui::test]
4127 async fn test_matching_tag_with_quotes(cx: &mut gpui::TestAppContext) {
4128 let mut cx = NeovimBackedTestContext::new_html(cx).await;
4129 cx.update(|_, cx| {
4130 cx.bind_keys([KeyBinding::new(
4131 "%",
4132 Matching {
4133 match_quotes: false,
4134 },
4135 None,
4136 )]);
4137 });
4138
4139 cx.neovim.exec("set filetype=html").await;
4140 cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
4141 </div>
4142 "})
4143 .await;
4144 cx.simulate_shared_keystrokes("%").await;
4145 cx.shared_state()
4146 .await
4147 .assert_eq(indoc! {r"<div class='test' id='main'>
4148 <ˇ/div>
4149 "});
4150
4151 cx.update(|_, cx| {
4152 cx.bind_keys([KeyBinding::new("%", Matching { match_quotes: true }, None)]);
4153 });
4154
4155 cx.set_shared_state(indoc! {r"<div class='teˇst' id='main'>
4156 </div>
4157 "})
4158 .await;
4159 cx.simulate_shared_keystrokes("%").await;
4160 cx.shared_state()
4161 .await
4162 .assert_eq(indoc! {r"<div class='test' id='main'>
4163 <ˇ/div>
4164 "});
4165 }
4166 #[gpui::test]
4167 async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) {
4168 let mut cx = NeovimBackedTestContext::new_typescript(cx).await;
4169
4170 // test brackets within tags
4171 cx.set_shared_state(indoc! {r"function f() {
4172 return (
4173 <div rules={ˇ[{ a: 1 }]}>
4174 <h1>test</h1>
4175 </div>
4176 );
4177 }"})
4178 .await;
4179 cx.simulate_shared_keystrokes("%").await;
4180 cx.shared_state().await.assert_eq(indoc! {r"function f() {
4181 return (
4182 <div rules={[{ a: 1 }ˇ]}>
4183 <h1>test</h1>
4184 </div>
4185 );
4186 }"});
4187 }
4188
4189 #[gpui::test]
4190 async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) {
4191 let mut cx = NeovimBackedTestContext::new_tsx(cx).await;
4192
4193 cx.set_shared_state(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"})
4194 .await;
4195 cx.simulate_shared_keystrokes("%").await;
4196 cx.shared_state()
4197 .await
4198 .assert_eq(indoc! {r"<Button onClick={() => {}ˇ}></Button>"});
4199 cx.simulate_shared_keystrokes("%").await;
4200 cx.shared_state()
4201 .await
4202 .assert_eq(indoc! {r"<Button onClick=ˇ{() => {}}></Button>"});
4203 }
4204
4205 #[gpui::test]
4206 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
4207 let mut cx = NeovimBackedTestContext::new(cx).await;
4208
4209 // f and F
4210 cx.set_shared_state("ˇone two three four").await;
4211 cx.simulate_shared_keystrokes("f o").await;
4212 cx.shared_state().await.assert_eq("one twˇo three four");
4213 cx.simulate_shared_keystrokes(",").await;
4214 cx.shared_state().await.assert_eq("ˇone two three four");
4215 cx.simulate_shared_keystrokes("2 ;").await;
4216 cx.shared_state().await.assert_eq("one two three fˇour");
4217 cx.simulate_shared_keystrokes("shift-f e").await;
4218 cx.shared_state().await.assert_eq("one two threˇe four");
4219 cx.simulate_shared_keystrokes("2 ;").await;
4220 cx.shared_state().await.assert_eq("onˇe two three four");
4221 cx.simulate_shared_keystrokes(",").await;
4222 cx.shared_state().await.assert_eq("one two thrˇee four");
4223
4224 // t and T
4225 cx.set_shared_state("ˇone two three four").await;
4226 cx.simulate_shared_keystrokes("t o").await;
4227 cx.shared_state().await.assert_eq("one tˇwo three four");
4228 cx.simulate_shared_keystrokes(",").await;
4229 cx.shared_state().await.assert_eq("oˇne two three four");
4230 cx.simulate_shared_keystrokes("2 ;").await;
4231 cx.shared_state().await.assert_eq("one two three ˇfour");
4232 cx.simulate_shared_keystrokes("shift-t e").await;
4233 cx.shared_state().await.assert_eq("one two threeˇ four");
4234 cx.simulate_shared_keystrokes("3 ;").await;
4235 cx.shared_state().await.assert_eq("oneˇ two three four");
4236 cx.simulate_shared_keystrokes(",").await;
4237 cx.shared_state().await.assert_eq("one two thˇree four");
4238 }
4239
4240 #[gpui::test]
4241 async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) {
4242 let mut cx = NeovimBackedTestContext::new(cx).await;
4243 let initial_state = indoc! {r"something(ˇfoo)"};
4244 cx.set_shared_state(initial_state).await;
4245 cx.simulate_shared_keystrokes("}").await;
4246 cx.shared_state().await.assert_eq("something(fooˇ)");
4247 }
4248
4249 #[gpui::test]
4250 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
4251 let mut cx = NeovimBackedTestContext::new(cx).await;
4252 cx.set_shared_state("ˇone\n two\nthree").await;
4253 cx.simulate_shared_keystrokes("enter").await;
4254 cx.shared_state().await.assert_eq("one\n ˇtwo\nthree");
4255 }
4256
4257 #[gpui::test]
4258 async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) {
4259 let mut cx = NeovimBackedTestContext::new(cx).await;
4260 cx.set_shared_state("ˇ one\n two \nthree").await;
4261 cx.simulate_shared_keystrokes("g _").await;
4262 cx.shared_state().await.assert_eq(" onˇe\n two \nthree");
4263
4264 cx.set_shared_state("ˇ one \n two \nthree").await;
4265 cx.simulate_shared_keystrokes("g _").await;
4266 cx.shared_state().await.assert_eq(" onˇe \n two \nthree");
4267 cx.simulate_shared_keystrokes("2 g _").await;
4268 cx.shared_state().await.assert_eq(" one \n twˇo \nthree");
4269 }
4270
4271 #[gpui::test]
4272 async fn test_end_of_line_with_vertical_motion(cx: &mut gpui::TestAppContext) {
4273 let mut cx = NeovimBackedTestContext::new(cx).await;
4274
4275 // test $ followed by k maintains end-of-line position
4276 cx.set_shared_state(indoc! {"
4277 The quick brown
4278 fˇox
4279 jumps over the
4280 lazy dog
4281 "})
4282 .await;
4283 cx.simulate_shared_keystrokes("$ k").await;
4284 cx.shared_state().await.assert_eq(indoc! {"
4285 The quick browˇn
4286 fox
4287 jumps over the
4288 lazy dog
4289 "});
4290 cx.simulate_shared_keystrokes("j j").await;
4291 cx.shared_state().await.assert_eq(indoc! {"
4292 The quick brown
4293 fox
4294 jumps over thˇe
4295 lazy dog
4296 "});
4297
4298 // test horizontal movement resets the end-of-line behavior
4299 cx.set_shared_state(indoc! {"
4300 The quick brown fox
4301 jumps over the
4302 lazy ˇdog
4303 "})
4304 .await;
4305 cx.simulate_shared_keystrokes("$ k").await;
4306 cx.shared_state().await.assert_eq(indoc! {"
4307 The quick brown fox
4308 jumps over thˇe
4309 lazy dog
4310 "});
4311 cx.simulate_shared_keystrokes("b b").await;
4312 cx.shared_state().await.assert_eq(indoc! {"
4313 The quick brown fox
4314 jumps ˇover the
4315 lazy dog
4316 "});
4317 cx.simulate_shared_keystrokes("k").await;
4318 cx.shared_state().await.assert_eq(indoc! {"
4319 The quˇick brown fox
4320 jumps over the
4321 lazy dog
4322 "});
4323
4324 // Test that, when the cursor is moved to the end of the line using `l`,
4325 // if `$` is used, the cursor stays at the end of the line when moving
4326 // to a longer line, ensuring that the selection goal was correctly
4327 // updated.
4328 cx.set_shared_state(indoc! {"
4329 The quick brown fox
4330 jumps over the
4331 lazy dˇog
4332 "})
4333 .await;
4334 cx.simulate_shared_keystrokes("l").await;
4335 cx.shared_state().await.assert_eq(indoc! {"
4336 The quick brown fox
4337 jumps over the
4338 lazy doˇg
4339 "});
4340 cx.simulate_shared_keystrokes("$ k").await;
4341 cx.shared_state().await.assert_eq(indoc! {"
4342 The quick brown fox
4343 jumps over thˇe
4344 lazy dog
4345 "});
4346 }
4347
4348 #[gpui::test]
4349 async fn test_window_top(cx: &mut gpui::TestAppContext) {
4350 let mut cx = NeovimBackedTestContext::new(cx).await;
4351 let initial_state = indoc! {r"abc
4352 def
4353 paragraph
4354 the second
4355 third ˇand
4356 final"};
4357
4358 cx.set_shared_state(initial_state).await;
4359 cx.simulate_shared_keystrokes("shift-h").await;
4360 cx.shared_state().await.assert_eq(indoc! {r"abˇc
4361 def
4362 paragraph
4363 the second
4364 third and
4365 final"});
4366
4367 // clip point
4368 cx.set_shared_state(indoc! {r"
4369 1 2 3
4370 4 5 6
4371 7 8 ˇ9
4372 "})
4373 .await;
4374 cx.simulate_shared_keystrokes("shift-h").await;
4375 cx.shared_state().await.assert_eq(indoc! {"
4376 1 2 ˇ3
4377 4 5 6
4378 7 8 9
4379 "});
4380
4381 cx.set_shared_state(indoc! {r"
4382 1 2 3
4383 4 5 6
4384 ˇ7 8 9
4385 "})
4386 .await;
4387 cx.simulate_shared_keystrokes("shift-h").await;
4388 cx.shared_state().await.assert_eq(indoc! {"
4389 ˇ1 2 3
4390 4 5 6
4391 7 8 9
4392 "});
4393
4394 cx.set_shared_state(indoc! {r"
4395 1 2 3
4396 4 5 ˇ6
4397 7 8 9"})
4398 .await;
4399 cx.simulate_shared_keystrokes("9 shift-h").await;
4400 cx.shared_state().await.assert_eq(indoc! {"
4401 1 2 3
4402 4 5 6
4403 7 8 ˇ9"});
4404 }
4405
4406 #[gpui::test]
4407 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
4408 let mut cx = NeovimBackedTestContext::new(cx).await;
4409 let initial_state = indoc! {r"abˇc
4410 def
4411 paragraph
4412 the second
4413 third and
4414 final"};
4415
4416 cx.set_shared_state(initial_state).await;
4417 cx.simulate_shared_keystrokes("shift-m").await;
4418 cx.shared_state().await.assert_eq(indoc! {r"abc
4419 def
4420 paˇragraph
4421 the second
4422 third and
4423 final"});
4424
4425 cx.set_shared_state(indoc! {r"
4426 1 2 3
4427 4 5 6
4428 7 8 ˇ9
4429 "})
4430 .await;
4431 cx.simulate_shared_keystrokes("shift-m").await;
4432 cx.shared_state().await.assert_eq(indoc! {"
4433 1 2 3
4434 4 5 ˇ6
4435 7 8 9
4436 "});
4437 cx.set_shared_state(indoc! {r"
4438 1 2 3
4439 4 5 6
4440 ˇ7 8 9
4441 "})
4442 .await;
4443 cx.simulate_shared_keystrokes("shift-m").await;
4444 cx.shared_state().await.assert_eq(indoc! {"
4445 1 2 3
4446 ˇ4 5 6
4447 7 8 9
4448 "});
4449 cx.set_shared_state(indoc! {r"
4450 ˇ1 2 3
4451 4 5 6
4452 7 8 9
4453 "})
4454 .await;
4455 cx.simulate_shared_keystrokes("shift-m").await;
4456 cx.shared_state().await.assert_eq(indoc! {"
4457 1 2 3
4458 ˇ4 5 6
4459 7 8 9
4460 "});
4461 cx.set_shared_state(indoc! {r"
4462 1 2 3
4463 ˇ4 5 6
4464 7 8 9
4465 "})
4466 .await;
4467 cx.simulate_shared_keystrokes("shift-m").await;
4468 cx.shared_state().await.assert_eq(indoc! {"
4469 1 2 3
4470 ˇ4 5 6
4471 7 8 9
4472 "});
4473 cx.set_shared_state(indoc! {r"
4474 1 2 3
4475 4 5 ˇ6
4476 7 8 9
4477 "})
4478 .await;
4479 cx.simulate_shared_keystrokes("shift-m").await;
4480 cx.shared_state().await.assert_eq(indoc! {"
4481 1 2 3
4482 4 5 ˇ6
4483 7 8 9
4484 "});
4485 }
4486
4487 #[gpui::test]
4488 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
4489 let mut cx = NeovimBackedTestContext::new(cx).await;
4490 let initial_state = indoc! {r"abc
4491 deˇf
4492 paragraph
4493 the second
4494 third and
4495 final"};
4496
4497 cx.set_shared_state(initial_state).await;
4498 cx.simulate_shared_keystrokes("shift-l").await;
4499 cx.shared_state().await.assert_eq(indoc! {r"abc
4500 def
4501 paragraph
4502 the second
4503 third and
4504 fiˇnal"});
4505
4506 cx.set_shared_state(indoc! {r"
4507 1 2 3
4508 4 5 ˇ6
4509 7 8 9
4510 "})
4511 .await;
4512 cx.simulate_shared_keystrokes("shift-l").await;
4513 cx.shared_state().await.assert_eq(indoc! {"
4514 1 2 3
4515 4 5 6
4516 7 8 9
4517 ˇ"});
4518
4519 cx.set_shared_state(indoc! {r"
4520 1 2 3
4521 ˇ4 5 6
4522 7 8 9
4523 "})
4524 .await;
4525 cx.simulate_shared_keystrokes("shift-l").await;
4526 cx.shared_state().await.assert_eq(indoc! {"
4527 1 2 3
4528 4 5 6
4529 7 8 9
4530 ˇ"});
4531
4532 cx.set_shared_state(indoc! {r"
4533 1 2 ˇ3
4534 4 5 6
4535 7 8 9
4536 "})
4537 .await;
4538 cx.simulate_shared_keystrokes("shift-l").await;
4539 cx.shared_state().await.assert_eq(indoc! {"
4540 1 2 3
4541 4 5 6
4542 7 8 9
4543 ˇ"});
4544
4545 cx.set_shared_state(indoc! {r"
4546 ˇ1 2 3
4547 4 5 6
4548 7 8 9
4549 "})
4550 .await;
4551 cx.simulate_shared_keystrokes("shift-l").await;
4552 cx.shared_state().await.assert_eq(indoc! {"
4553 1 2 3
4554 4 5 6
4555 7 8 9
4556 ˇ"});
4557
4558 cx.set_shared_state(indoc! {r"
4559 1 2 3
4560 4 5 ˇ6
4561 7 8 9
4562 "})
4563 .await;
4564 cx.simulate_shared_keystrokes("9 shift-l").await;
4565 cx.shared_state().await.assert_eq(indoc! {"
4566 1 2 ˇ3
4567 4 5 6
4568 7 8 9
4569 "});
4570 }
4571
4572 #[gpui::test]
4573 async fn test_previous_word_end(cx: &mut gpui::TestAppContext) {
4574 let mut cx = NeovimBackedTestContext::new(cx).await;
4575 cx.set_shared_state(indoc! {r"
4576 456 5ˇ67 678
4577 "})
4578 .await;
4579 cx.simulate_shared_keystrokes("g e").await;
4580 cx.shared_state().await.assert_eq(indoc! {"
4581 45ˇ6 567 678
4582 "});
4583
4584 // Test times
4585 cx.set_shared_state(indoc! {r"
4586 123 234 345
4587 456 5ˇ67 678
4588 "})
4589 .await;
4590 cx.simulate_shared_keystrokes("4 g e").await;
4591 cx.shared_state().await.assert_eq(indoc! {"
4592 12ˇ3 234 345
4593 456 567 678
4594 "});
4595
4596 // With punctuation
4597 cx.set_shared_state(indoc! {r"
4598 123 234 345
4599 4;5.6 5ˇ67 678
4600 789 890 901
4601 "})
4602 .await;
4603 cx.simulate_shared_keystrokes("g e").await;
4604 cx.shared_state().await.assert_eq(indoc! {"
4605 123 234 345
4606 4;5.ˇ6 567 678
4607 789 890 901
4608 "});
4609
4610 // With punctuation and count
4611 cx.set_shared_state(indoc! {r"
4612 123 234 345
4613 4;5.6 5ˇ67 678
4614 789 890 901
4615 "})
4616 .await;
4617 cx.simulate_shared_keystrokes("5 g e").await;
4618 cx.shared_state().await.assert_eq(indoc! {"
4619 123 234 345
4620 ˇ4;5.6 567 678
4621 789 890 901
4622 "});
4623
4624 // newlines
4625 cx.set_shared_state(indoc! {r"
4626 123 234 345
4627
4628 78ˇ9 890 901
4629 "})
4630 .await;
4631 cx.simulate_shared_keystrokes("g e").await;
4632 cx.shared_state().await.assert_eq(indoc! {"
4633 123 234 345
4634 ˇ
4635 789 890 901
4636 "});
4637 cx.simulate_shared_keystrokes("g e").await;
4638 cx.shared_state().await.assert_eq(indoc! {"
4639 123 234 34ˇ5
4640
4641 789 890 901
4642 "});
4643
4644 // With punctuation
4645 cx.set_shared_state(indoc! {r"
4646 123 234 345
4647 4;5.ˇ6 567 678
4648 789 890 901
4649 "})
4650 .await;
4651 cx.simulate_shared_keystrokes("g shift-e").await;
4652 cx.shared_state().await.assert_eq(indoc! {"
4653 123 234 34ˇ5
4654 4;5.6 567 678
4655 789 890 901
4656 "});
4657
4658 // With multi byte char
4659 cx.set_shared_state(indoc! {r"
4660 bar ˇó
4661 "})
4662 .await;
4663 cx.simulate_shared_keystrokes("g e").await;
4664 cx.shared_state().await.assert_eq(indoc! {"
4665 baˇr ó
4666 "});
4667 }
4668
4669 #[gpui::test]
4670 async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) {
4671 let mut cx = NeovimBackedTestContext::new(cx).await;
4672
4673 cx.set_shared_state(indoc! {"
4674 fn aˇ() {
4675 return
4676 }
4677 "})
4678 .await;
4679 cx.simulate_shared_keystrokes("v $ %").await;
4680 cx.shared_state().await.assert_eq(indoc! {"
4681 fn a«() {
4682 return
4683 }ˇ»
4684 "});
4685 }
4686
4687 #[gpui::test]
4688 async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) {
4689 let mut cx = VimTestContext::new(cx, true).await;
4690
4691 cx.set_state(
4692 indoc! {"
4693 struct Foo {
4694 ˇ
4695 }
4696 "},
4697 Mode::Normal,
4698 );
4699
4700 cx.update_editor(|editor, _window, cx| {
4701 let range = editor.selections.newest_anchor().range();
4702 let inlay_text = " field: int,\n field2: string\n field3: float";
4703 let inlay = Inlay::edit_prediction(1, range.start, inlay_text);
4704 editor.splice_inlays(&[], vec![inlay], cx);
4705 });
4706
4707 cx.simulate_keystrokes("j");
4708 cx.assert_state(
4709 indoc! {"
4710 struct Foo {
4711
4712 ˇ}
4713 "},
4714 Mode::Normal,
4715 );
4716 }
4717
4718 #[gpui::test]
4719 async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) {
4720 let mut cx = VimTestContext::new(cx, true).await;
4721
4722 cx.set_state(
4723 indoc! {"
4724 ˇstruct Foo {
4725
4726 }
4727 "},
4728 Mode::Normal,
4729 );
4730 cx.update_editor(|editor, _window, cx| {
4731 let snapshot = editor.buffer().read(cx).snapshot(cx);
4732 let end_of_line =
4733 snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0))));
4734 let inlay_text = " hint";
4735 let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text);
4736 editor.splice_inlays(&[], vec![inlay], cx);
4737 });
4738 cx.simulate_keystrokes("$");
4739 cx.assert_state(
4740 indoc! {"
4741 struct Foo ˇ{
4742
4743 }
4744 "},
4745 Mode::Normal,
4746 );
4747 }
4748
4749 #[gpui::test]
4750 async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
4751 let mut cx = VimTestContext::new(cx, true).await;
4752
4753 // Test the exact scenario from issue #29134
4754 cx.set_state(
4755 indoc! {"
4756 fn main() {
4757 let this_is_a_long_name = Vec::<u32>::new();
4758 let new_oneˇ = this_is_a_long_name
4759 .iter()
4760 .map(|i| i + 1)
4761 .map(|i| i * 2)
4762 .collect::<Vec<_>>();
4763 }
4764 "},
4765 Mode::Normal,
4766 );
4767
4768 // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
4769 cx.update_editor(|editor, _window, cx| {
4770 let snapshot = editor.buffer().read(cx).snapshot(cx);
4771 // The empty line is at line 3 (0-indexed)
4772 let line_start = snapshot.anchor_after(Point::new(3, 0));
4773 let inlay_text = ": Vec<u32>";
4774 let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
4775 editor.splice_inlays(&[], vec![inlay], cx);
4776 });
4777
4778 // Enter visual mode
4779 cx.simulate_keystrokes("v");
4780 cx.assert_state(
4781 indoc! {"
4782 fn main() {
4783 let this_is_a_long_name = Vec::<u32>::new();
4784 let new_one« ˇ»= this_is_a_long_name
4785 .iter()
4786 .map(|i| i + 1)
4787 .map(|i| i * 2)
4788 .collect::<Vec<_>>();
4789 }
4790 "},
4791 Mode::Visual,
4792 );
4793
4794 // Move down - should go to the beginning of line 4, not skip to line 5
4795 cx.simulate_keystrokes("j");
4796 cx.assert_state(
4797 indoc! {"
4798 fn main() {
4799 let this_is_a_long_name = Vec::<u32>::new();
4800 let new_one« = this_is_a_long_name
4801 ˇ» .iter()
4802 .map(|i| i + 1)
4803 .map(|i| i * 2)
4804 .collect::<Vec<_>>();
4805 }
4806 "},
4807 Mode::Visual,
4808 );
4809
4810 // Test with multiple movements
4811 cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
4812
4813 // Add type hint on the empty line
4814 cx.update_editor(|editor, _window, cx| {
4815 let snapshot = editor.buffer().read(cx).snapshot(cx);
4816 let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
4817 let inlay_text = ": i32";
4818 let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
4819 editor.splice_inlays(&[], vec![inlay], cx);
4820 });
4821
4822 // Enter visual mode and move down twice
4823 cx.simulate_keystrokes("v j j");
4824 cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
4825 }
4826
4827 #[gpui::test]
4828 async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
4829 let mut cx = NeovimBackedTestContext::new(cx).await;
4830 // Normal mode
4831 cx.set_shared_state(indoc! {"
4832 The ˇquick brown
4833 fox jumps over
4834 the lazy dog
4835 The quick brown
4836 fox jumps over
4837 the lazy dog
4838 The quick brown
4839 fox jumps over
4840 the lazy dog"})
4841 .await;
4842 cx.simulate_shared_keystrokes("2 0 %").await;
4843 cx.shared_state().await.assert_eq(indoc! {"
4844 The quick brown
4845 fox ˇjumps over
4846 the lazy dog
4847 The quick brown
4848 fox jumps over
4849 the lazy dog
4850 The quick brown
4851 fox jumps over
4852 the lazy dog"});
4853
4854 cx.simulate_shared_keystrokes("2 5 %").await;
4855 cx.shared_state().await.assert_eq(indoc! {"
4856 The quick brown
4857 fox jumps over
4858 the ˇlazy dog
4859 The quick brown
4860 fox jumps over
4861 the lazy dog
4862 The quick brown
4863 fox jumps over
4864 the lazy dog"});
4865
4866 cx.simulate_shared_keystrokes("7 5 %").await;
4867 cx.shared_state().await.assert_eq(indoc! {"
4868 The quick brown
4869 fox jumps over
4870 the lazy dog
4871 The quick brown
4872 fox jumps over
4873 the lazy dog
4874 The ˇquick brown
4875 fox jumps over
4876 the lazy dog"});
4877
4878 // Visual mode
4879 cx.set_shared_state(indoc! {"
4880 The ˇquick brown
4881 fox jumps over
4882 the lazy dog
4883 The quick brown
4884 fox jumps over
4885 the lazy dog
4886 The quick brown
4887 fox jumps over
4888 the lazy dog"})
4889 .await;
4890 cx.simulate_shared_keystrokes("v 5 0 %").await;
4891 cx.shared_state().await.assert_eq(indoc! {"
4892 The «quick brown
4893 fox jumps over
4894 the lazy dog
4895 The quick brown
4896 fox jˇ»umps over
4897 the lazy dog
4898 The quick brown
4899 fox jumps over
4900 the lazy dog"});
4901
4902 cx.set_shared_state(indoc! {"
4903 The ˇquick brown
4904 fox jumps over
4905 the lazy dog
4906 The quick brown
4907 fox jumps over
4908 the lazy dog
4909 The quick brown
4910 fox jumps over
4911 the lazy dog"})
4912 .await;
4913 cx.simulate_shared_keystrokes("v 1 0 0 %").await;
4914 cx.shared_state().await.assert_eq(indoc! {"
4915 The «quick brown
4916 fox jumps over
4917 the lazy dog
4918 The quick brown
4919 fox jumps over
4920 the lazy dog
4921 The quick brown
4922 fox jumps over
4923 the lˇ»azy dog"});
4924 }
4925
4926 #[gpui::test]
4927 async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) {
4928 let mut cx = NeovimBackedTestContext::new(cx).await;
4929
4930 cx.set_shared_state("ˇπππππ").await;
4931 cx.simulate_shared_keystrokes("3 space").await;
4932 cx.shared_state().await.assert_eq("πππˇππ");
4933 }
4934
4935 #[gpui::test]
4936 async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) {
4937 let mut cx = NeovimBackedTestContext::new(cx).await;
4938
4939 cx.set_shared_state(indoc! {"
4940 ππππˇπ
4941 πanotherline"})
4942 .await;
4943 cx.simulate_shared_keystrokes("4 space").await;
4944 cx.shared_state().await.assert_eq(indoc! {"
4945 πππππ
4946 πanˇotherline"});
4947 }
4948
4949 #[gpui::test]
4950 async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) {
4951 let mut cx = NeovimBackedTestContext::new(cx).await;
4952
4953 cx.set_shared_state(indoc! {"
4954 ππππ
4955 πanˇotherline"})
4956 .await;
4957 cx.simulate_shared_keystrokes("4 backspace").await;
4958 cx.shared_state().await.assert_eq(indoc! {"
4959 πππˇπ
4960 πanotherline"});
4961 }
4962
4963 #[gpui::test]
4964 async fn test_go_to_indent(cx: &mut gpui::TestAppContext) {
4965 let mut cx = VimTestContext::new(cx, true).await;
4966 cx.set_state(
4967 indoc! {
4968 "func empty(a string) bool {
4969 ˇif a == \"\" {
4970 return true
4971 }
4972 return false
4973 }"
4974 },
4975 Mode::Normal,
4976 );
4977 cx.simulate_keystrokes("[ -");
4978 cx.assert_state(
4979 indoc! {
4980 "ˇfunc empty(a string) bool {
4981 if a == \"\" {
4982 return true
4983 }
4984 return false
4985 }"
4986 },
4987 Mode::Normal,
4988 );
4989 cx.simulate_keystrokes("] =");
4990 cx.assert_state(
4991 indoc! {
4992 "func empty(a string) bool {
4993 if a == \"\" {
4994 return true
4995 }
4996 return false
4997 ˇ}"
4998 },
4999 Mode::Normal,
5000 );
5001 cx.simulate_keystrokes("[ +");
5002 cx.assert_state(
5003 indoc! {
5004 "func empty(a string) bool {
5005 if a == \"\" {
5006 return true
5007 }
5008 ˇreturn false
5009 }"
5010 },
5011 Mode::Normal,
5012 );
5013 cx.simulate_keystrokes("2 [ =");
5014 cx.assert_state(
5015 indoc! {
5016 "func empty(a string) bool {
5017 ˇif a == \"\" {
5018 return true
5019 }
5020 return false
5021 }"
5022 },
5023 Mode::Normal,
5024 );
5025 cx.simulate_keystrokes("] +");
5026 cx.assert_state(
5027 indoc! {
5028 "func empty(a string) bool {
5029 if a == \"\" {
5030 ˇreturn true
5031 }
5032 return false
5033 }"
5034 },
5035 Mode::Normal,
5036 );
5037 cx.simulate_keystrokes("] -");
5038 cx.assert_state(
5039 indoc! {
5040 "func empty(a string) bool {
5041 if a == \"\" {
5042 return true
5043 ˇ}
5044 return false
5045 }"
5046 },
5047 Mode::Normal,
5048 );
5049 }
5050
5051 #[gpui::test]
5052 async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) {
5053 let mut cx = NeovimBackedTestContext::new(cx).await;
5054 cx.set_shared_state("abˇc").await;
5055 cx.simulate_shared_keystrokes("delete").await;
5056 cx.shared_state().await.assert_eq("aˇb");
5057 }
5058
5059 #[gpui::test]
5060 async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) {
5061 let mut cx = NeovimBackedTestContext::new(cx).await;
5062
5063 cx.set_shared_state(indoc! {"
5064 ˇthe quick brown fox
5065 jumped over the lazy dog"})
5066 .await;
5067 cx.simulate_shared_keystrokes("d v 0").await;
5068 cx.shared_state().await.assert_eq(indoc! {"
5069 ˇhe quick brown fox
5070 jumped over the lazy dog"});
5071 assert!(!cx.cx.forced_motion());
5072
5073 cx.set_shared_state(indoc! {"
5074 the quick bˇrown fox
5075 jumped over the lazy dog"})
5076 .await;
5077 cx.simulate_shared_keystrokes("d v 0").await;
5078 cx.shared_state().await.assert_eq(indoc! {"
5079 ˇown fox
5080 jumped over the lazy dog"});
5081 assert!(!cx.cx.forced_motion());
5082
5083 cx.set_shared_state(indoc! {"
5084 the quick brown foˇx
5085 jumped over the lazy dog"})
5086 .await;
5087 cx.simulate_shared_keystrokes("d v 0").await;
5088 cx.shared_state().await.assert_eq(indoc! {"
5089 ˇ
5090 jumped over the lazy dog"});
5091 assert!(!cx.cx.forced_motion());
5092 }
5093
5094 #[gpui::test]
5095 async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) {
5096 let mut cx = NeovimBackedTestContext::new(cx).await;
5097
5098 cx.set_shared_state(indoc! {"
5099 ˇthe quick brown fox
5100 jumped over the lazy dog"})
5101 .await;
5102 cx.simulate_shared_keystrokes("d v g shift-m").await;
5103 cx.shared_state().await.assert_eq(indoc! {"
5104 ˇbrown fox
5105 jumped over the lazy dog"});
5106 assert!(!cx.cx.forced_motion());
5107
5108 cx.set_shared_state(indoc! {"
5109 the quick bˇrown fox
5110 jumped over the lazy dog"})
5111 .await;
5112 cx.simulate_shared_keystrokes("d v g shift-m").await;
5113 cx.shared_state().await.assert_eq(indoc! {"
5114 the quickˇown fox
5115 jumped over the lazy dog"});
5116 assert!(!cx.cx.forced_motion());
5117
5118 cx.set_shared_state(indoc! {"
5119 the quick brown foˇx
5120 jumped over the lazy dog"})
5121 .await;
5122 cx.simulate_shared_keystrokes("d v g shift-m").await;
5123 cx.shared_state().await.assert_eq(indoc! {"
5124 the quicˇk
5125 jumped over the lazy dog"});
5126 assert!(!cx.cx.forced_motion());
5127
5128 cx.set_shared_state(indoc! {"
5129 ˇthe quick brown fox
5130 jumped over the lazy dog"})
5131 .await;
5132 cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await;
5133 cx.shared_state().await.assert_eq(indoc! {"
5134 ˇ fox
5135 jumped over the lazy dog"});
5136 assert!(!cx.cx.forced_motion());
5137
5138 cx.set_shared_state(indoc! {"
5139 ˇthe quick brown fox
5140 jumped over the lazy dog"})
5141 .await;
5142 cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await;
5143 cx.shared_state().await.assert_eq(indoc! {"
5144 ˇuick brown fox
5145 jumped over the lazy dog"});
5146 assert!(!cx.cx.forced_motion());
5147 }
5148
5149 #[gpui::test]
5150 async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) {
5151 let mut cx = NeovimBackedTestContext::new(cx).await;
5152
5153 cx.set_shared_state(indoc! {"
5154 the quick brown foˇx
5155 jumped over the lazy dog"})
5156 .await;
5157 cx.simulate_shared_keystrokes("d v $").await;
5158 cx.shared_state().await.assert_eq(indoc! {"
5159 the quick brown foˇx
5160 jumped over the lazy dog"});
5161 assert!(!cx.cx.forced_motion());
5162
5163 cx.set_shared_state(indoc! {"
5164 ˇthe quick brown fox
5165 jumped over the lazy dog"})
5166 .await;
5167 cx.simulate_shared_keystrokes("d v $").await;
5168 cx.shared_state().await.assert_eq(indoc! {"
5169 ˇx
5170 jumped over the lazy dog"});
5171 assert!(!cx.cx.forced_motion());
5172 }
5173
5174 #[gpui::test]
5175 async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) {
5176 let mut cx = NeovimBackedTestContext::new(cx).await;
5177
5178 cx.set_shared_state(indoc! {"
5179 ˇthe quick brown fox
5180 jumped over the lazy dog"})
5181 .await;
5182 cx.simulate_shared_keystrokes("y v j p").await;
5183 cx.shared_state().await.assert_eq(indoc! {"
5184 the quick brown fox
5185 ˇthe quick brown fox
5186 jumped over the lazy dog"});
5187 assert!(!cx.cx.forced_motion());
5188
5189 cx.set_shared_state(indoc! {"
5190 the quick bˇrown fox
5191 jumped over the lazy dog"})
5192 .await;
5193 cx.simulate_shared_keystrokes("y v j p").await;
5194 cx.shared_state().await.assert_eq(indoc! {"
5195 the quick brˇrown fox
5196 jumped overown fox
5197 jumped over the lazy dog"});
5198 assert!(!cx.cx.forced_motion());
5199
5200 cx.set_shared_state(indoc! {"
5201 the quick brown foˇx
5202 jumped over the lazy dog"})
5203 .await;
5204 cx.simulate_shared_keystrokes("y v j p").await;
5205 cx.shared_state().await.assert_eq(indoc! {"
5206 the quick brown foxˇx
5207 jumped over the la
5208 jumped over the lazy dog"});
5209 assert!(!cx.cx.forced_motion());
5210
5211 cx.set_shared_state(indoc! {"
5212 the quick brown fox
5213 jˇumped over the lazy dog"})
5214 .await;
5215 cx.simulate_shared_keystrokes("y v k p").await;
5216 cx.shared_state().await.assert_eq(indoc! {"
5217 thˇhe quick brown fox
5218 je quick brown fox
5219 jumped over the lazy dog"});
5220 assert!(!cx.cx.forced_motion());
5221 }
5222
5223 #[gpui::test]
5224 async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) {
5225 let mut cx = NeovimBackedTestContext::new(cx).await;
5226
5227 cx.set_shared_state(indoc! {"
5228 ˇthe quick brown fox
5229 jumped over the lazy dog"})
5230 .await;
5231 cx.simulate_shared_keystrokes("d v e").await;
5232 cx.shared_state().await.assert_eq(indoc! {"
5233 ˇe quick brown fox
5234 jumped over the lazy dog"});
5235 assert!(!cx.cx.forced_motion());
5236
5237 cx.set_shared_state(indoc! {"
5238 the quick bˇrown fox
5239 jumped over the lazy dog"})
5240 .await;
5241 cx.simulate_shared_keystrokes("d v e").await;
5242 cx.shared_state().await.assert_eq(indoc! {"
5243 the quick bˇn fox
5244 jumped over the lazy dog"});
5245 assert!(!cx.cx.forced_motion());
5246
5247 cx.set_shared_state(indoc! {"
5248 the quick brown foˇx
5249 jumped over the lazy dog"})
5250 .await;
5251 cx.simulate_shared_keystrokes("d v e").await;
5252 cx.shared_state().await.assert_eq(indoc! {"
5253 the quick brown foˇd over the lazy dog"});
5254 assert!(!cx.cx.forced_motion());
5255 }
5256
5257 #[gpui::test]
5258 async fn test_next_subword_start(cx: &mut gpui::TestAppContext) {
5259 let mut cx = VimTestContext::new(cx, true).await;
5260
5261 // Setup custom keybindings for subword motions so we can use the bindings
5262 // in `simulate_keystrokes`.
5263 cx.update(|_window, cx| {
5264 cx.bind_keys([KeyBinding::new(
5265 "w",
5266 super::NextSubwordStart {
5267 ignore_punctuation: false,
5268 },
5269 None,
5270 )]);
5271 });
5272
5273 cx.set_state("ˇfoo.bar", Mode::Normal);
5274 cx.simulate_keystrokes("w");
5275 cx.assert_state("foo.ˇbar", Mode::Normal);
5276
5277 cx.set_state("ˇfoo(bar)", Mode::Normal);
5278 cx.simulate_keystrokes("w");
5279 cx.assert_state("fooˇ(bar)", Mode::Normal);
5280 cx.simulate_keystrokes("w");
5281 cx.assert_state("foo(ˇbar)", Mode::Normal);
5282 cx.simulate_keystrokes("w");
5283 cx.assert_state("foo(barˇ)", Mode::Normal);
5284
5285 cx.set_state("ˇfoo_bar_baz", Mode::Normal);
5286 cx.simulate_keystrokes("w");
5287 cx.assert_state("foo_ˇbar_baz", Mode::Normal);
5288 cx.simulate_keystrokes("w");
5289 cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
5290
5291 cx.set_state("ˇfooBarBaz", Mode::Normal);
5292 cx.simulate_keystrokes("w");
5293 cx.assert_state("fooˇBarBaz", Mode::Normal);
5294 cx.simulate_keystrokes("w");
5295 cx.assert_state("fooBarˇBaz", Mode::Normal);
5296
5297 cx.set_state("ˇfoo;bar", Mode::Normal);
5298 cx.simulate_keystrokes("w");
5299 cx.assert_state("foo;ˇbar", Mode::Normal);
5300
5301 cx.set_state("ˇ<?php\n\n$someVariable = 2;", Mode::Normal);
5302 cx.simulate_keystrokes("w");
5303 cx.assert_state("<?ˇphp\n\n$someVariable = 2;", Mode::Normal);
5304 cx.simulate_keystrokes("w");
5305 cx.assert_state("<?php\nˇ\n$someVariable = 2;", Mode::Normal);
5306 cx.simulate_keystrokes("w");
5307 cx.assert_state("<?php\n\nˇ$someVariable = 2;", Mode::Normal);
5308 cx.simulate_keystrokes("w");
5309 cx.assert_state("<?php\n\n$ˇsomeVariable = 2;", Mode::Normal);
5310 cx.simulate_keystrokes("w");
5311 cx.assert_state("<?php\n\n$someˇVariable = 2;", Mode::Normal);
5312 cx.simulate_keystrokes("w");
5313 cx.assert_state("<?php\n\n$someVariable ˇ= 2;", Mode::Normal);
5314 cx.simulate_keystrokes("w");
5315 cx.assert_state("<?php\n\n$someVariable = ˇ2;", Mode::Normal);
5316 }
5317
5318 #[gpui::test]
5319 async fn test_next_subword_end(cx: &mut gpui::TestAppContext) {
5320 let mut cx = VimTestContext::new(cx, true).await;
5321
5322 // Setup custom keybindings for subword motions so we can use the bindings
5323 // in `simulate_keystrokes`.
5324 cx.update(|_window, cx| {
5325 cx.bind_keys([KeyBinding::new(
5326 "e",
5327 super::NextSubwordEnd {
5328 ignore_punctuation: false,
5329 },
5330 None,
5331 )]);
5332 });
5333
5334 cx.set_state("ˇfoo.bar", Mode::Normal);
5335 cx.simulate_keystrokes("e");
5336 cx.assert_state("foˇo.bar", Mode::Normal);
5337 cx.simulate_keystrokes("e");
5338 cx.assert_state("fooˇ.bar", Mode::Normal);
5339 cx.simulate_keystrokes("e");
5340 cx.assert_state("foo.baˇr", Mode::Normal);
5341
5342 cx.set_state("ˇfoo(bar)", Mode::Normal);
5343 cx.simulate_keystrokes("e");
5344 cx.assert_state("foˇo(bar)", Mode::Normal);
5345 cx.simulate_keystrokes("e");
5346 cx.assert_state("fooˇ(bar)", Mode::Normal);
5347 cx.simulate_keystrokes("e");
5348 cx.assert_state("foo(baˇr)", Mode::Normal);
5349 cx.simulate_keystrokes("e");
5350 cx.assert_state("foo(barˇ)", Mode::Normal);
5351
5352 cx.set_state("ˇfoo_bar_baz", Mode::Normal);
5353 cx.simulate_keystrokes("e");
5354 cx.assert_state("foˇo_bar_baz", Mode::Normal);
5355 cx.simulate_keystrokes("e");
5356 cx.assert_state("foo_baˇr_baz", Mode::Normal);
5357 cx.simulate_keystrokes("e");
5358 cx.assert_state("foo_bar_baˇz", Mode::Normal);
5359
5360 cx.set_state("ˇfooBarBaz", Mode::Normal);
5361 cx.simulate_keystrokes("e");
5362 cx.set_state("foˇoBarBaz", Mode::Normal);
5363 cx.simulate_keystrokes("e");
5364 cx.set_state("fooBaˇrBaz", Mode::Normal);
5365 cx.simulate_keystrokes("e");
5366 cx.set_state("fooBarBaˇz", Mode::Normal);
5367
5368 cx.set_state("ˇfoo;bar", Mode::Normal);
5369 cx.simulate_keystrokes("e");
5370 cx.set_state("foˇo;bar", Mode::Normal);
5371 cx.simulate_keystrokes("e");
5372 cx.set_state("fooˇ;bar", Mode::Normal);
5373 cx.simulate_keystrokes("e");
5374 cx.set_state("foo;baˇr", Mode::Normal);
5375 }
5376
5377 #[gpui::test]
5378 async fn test_previous_subword_start(cx: &mut gpui::TestAppContext) {
5379 let mut cx = VimTestContext::new(cx, true).await;
5380
5381 // Setup custom keybindings for subword motions so we can use the bindings
5382 // in `simulate_keystrokes`.
5383 cx.update(|_window, cx| {
5384 cx.bind_keys([KeyBinding::new(
5385 "b",
5386 super::PreviousSubwordStart {
5387 ignore_punctuation: false,
5388 },
5389 None,
5390 )]);
5391 });
5392
5393 cx.set_state("foo.barˇ", Mode::Normal);
5394 cx.simulate_keystrokes("b");
5395 cx.assert_state("foo.ˇbar", Mode::Normal);
5396 cx.simulate_keystrokes("b");
5397 cx.assert_state("fooˇ.bar", Mode::Normal);
5398 cx.simulate_keystrokes("b");
5399 cx.assert_state("ˇfoo.bar", Mode::Normal);
5400
5401 cx.set_state("foo(barˇ)", Mode::Normal);
5402 cx.simulate_keystrokes("b");
5403 cx.assert_state("foo(ˇbar)", Mode::Normal);
5404 cx.simulate_keystrokes("b");
5405 cx.assert_state("fooˇ(bar)", Mode::Normal);
5406 cx.simulate_keystrokes("b");
5407 cx.assert_state("ˇfoo(bar)", Mode::Normal);
5408
5409 cx.set_state("foo_bar_bazˇ", Mode::Normal);
5410 cx.simulate_keystrokes("b");
5411 cx.assert_state("foo_bar_ˇbaz", Mode::Normal);
5412 cx.simulate_keystrokes("b");
5413 cx.assert_state("foo_ˇbar_baz", Mode::Normal);
5414 cx.simulate_keystrokes("b");
5415 cx.assert_state("ˇfoo_bar_baz", Mode::Normal);
5416
5417 cx.set_state("fooBarBazˇ", Mode::Normal);
5418 cx.simulate_keystrokes("b");
5419 cx.assert_state("fooBarˇBaz", Mode::Normal);
5420 cx.simulate_keystrokes("b");
5421 cx.assert_state("fooˇBarBaz", Mode::Normal);
5422 cx.simulate_keystrokes("b");
5423 cx.assert_state("ˇfooBarBaz", Mode::Normal);
5424
5425 cx.set_state("foo;barˇ", Mode::Normal);
5426 cx.simulate_keystrokes("b");
5427 cx.assert_state("foo;ˇbar", Mode::Normal);
5428 cx.simulate_keystrokes("b");
5429 cx.assert_state("ˇfoo;bar", Mode::Normal);
5430
5431 cx.set_state("<?php\n\n$someVariable = 2ˇ;", Mode::Normal);
5432 cx.simulate_keystrokes("b");
5433 cx.assert_state("<?php\n\n$someVariable = ˇ2;", Mode::Normal);
5434 cx.simulate_keystrokes("b");
5435 cx.assert_state("<?php\n\n$someVariable ˇ= 2;", Mode::Normal);
5436 cx.simulate_keystrokes("b");
5437 cx.assert_state("<?php\n\n$someˇVariable = 2;", Mode::Normal);
5438 cx.simulate_keystrokes("b");
5439 cx.assert_state("<?php\n\n$ˇsomeVariable = 2;", Mode::Normal);
5440 cx.simulate_keystrokes("b");
5441 cx.assert_state("<?php\n\nˇ$someVariable = 2;", Mode::Normal);
5442 cx.simulate_keystrokes("b");
5443 cx.assert_state("<?php\nˇ\n$someVariable = 2;", Mode::Normal);
5444 cx.simulate_keystrokes("b");
5445 cx.assert_state("<?ˇphp\n\n$someVariable = 2;", Mode::Normal);
5446 cx.simulate_keystrokes("b");
5447 cx.assert_state("ˇ<?php\n\n$someVariable = 2;", Mode::Normal);
5448 }
5449
5450 #[gpui::test]
5451 async fn test_previous_subword_end(cx: &mut gpui::TestAppContext) {
5452 let mut cx = VimTestContext::new(cx, true).await;
5453
5454 // Setup custom keybindings for subword motions so we can use the bindings
5455 // in `simulate_keystrokes`.
5456 cx.update(|_window, cx| {
5457 cx.bind_keys([KeyBinding::new(
5458 "g e",
5459 super::PreviousSubwordEnd {
5460 ignore_punctuation: false,
5461 },
5462 None,
5463 )]);
5464 });
5465
5466 cx.set_state("foo.baˇr", Mode::Normal);
5467 cx.simulate_keystrokes("g e");
5468 cx.assert_state("fooˇ.bar", Mode::Normal);
5469 cx.simulate_keystrokes("g e");
5470 cx.assert_state("foˇo.bar", Mode::Normal);
5471
5472 cx.set_state("foo(barˇ)", Mode::Normal);
5473 cx.simulate_keystrokes("g e");
5474 cx.assert_state("foo(baˇr)", Mode::Normal);
5475 cx.simulate_keystrokes("g e");
5476 cx.assert_state("fooˇ(bar)", Mode::Normal);
5477 cx.simulate_keystrokes("g e");
5478 cx.assert_state("foˇo(bar)", Mode::Normal);
5479
5480 cx.set_state("foo_bar_baˇz", Mode::Normal);
5481 cx.simulate_keystrokes("g e");
5482 cx.assert_state("foo_baˇr_baz", Mode::Normal);
5483 cx.simulate_keystrokes("g e");
5484 cx.assert_state("foˇo_bar_baz", Mode::Normal);
5485
5486 cx.set_state("fooBarBaˇz", Mode::Normal);
5487 cx.simulate_keystrokes("g e");
5488 cx.assert_state("fooBaˇrBaz", Mode::Normal);
5489 cx.simulate_keystrokes("g e");
5490 cx.assert_state("foˇoBarBaz", Mode::Normal);
5491
5492 cx.set_state("foo;baˇr", Mode::Normal);
5493 cx.simulate_keystrokes("g e");
5494 cx.assert_state("fooˇ;bar", Mode::Normal);
5495 cx.simulate_keystrokes("g e");
5496 cx.assert_state("foˇo;bar", Mode::Normal);
5497 }
5498
5499 #[gpui::test]
5500 async fn test_method_motion_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
5501 let mut cx = VimTestContext::new(cx, true).await;
5502
5503 let diff_base = indoc! {r#"
5504 fn first() {
5505 println!("first");
5506 println!("removed line");
5507 }
5508
5509 fn second() {
5510 println!("second");
5511 }
5512
5513 fn third() {
5514 println!("third");
5515 }
5516 "#};
5517
5518 let current_text = indoc! {r#"
5519 fn first() {
5520 println!("first");
5521 }
5522
5523 fn second() {
5524 println!("second");
5525 }
5526
5527 fn third() {
5528 println!("third");
5529 }
5530 "#};
5531
5532 cx.set_state(&format!("ˇ{}", current_text), Mode::Normal);
5533 cx.set_head_text(diff_base);
5534 cx.update_editor(|editor, window, cx| {
5535 editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
5536 });
5537
5538 // When diff hunks are expanded, the deleted line from the diff base
5539 // appears in the MultiBuffer. The method motion should correctly
5540 // navigate to the second function even with this extra content.
5541 cx.simulate_keystrokes("] m");
5542 cx.assert_editor_state(indoc! {r#"
5543 fn first() {
5544 println!("first");
5545 println!("removed line");
5546 }
5547
5548 ˇfn second() {
5549 println!("second");
5550 }
5551
5552 fn third() {
5553 println!("third");
5554 }
5555 "#});
5556
5557 cx.simulate_keystrokes("] m");
5558 cx.assert_editor_state(indoc! {r#"
5559 fn first() {
5560 println!("first");
5561 println!("removed line");
5562 }
5563
5564 fn second() {
5565 println!("second");
5566 }
5567
5568 ˇfn third() {
5569 println!("third");
5570 }
5571 "#});
5572
5573 cx.simulate_keystrokes("[ m");
5574 cx.assert_editor_state(indoc! {r#"
5575 fn first() {
5576 println!("first");
5577 println!("removed line");
5578 }
5579
5580 ˇfn second() {
5581 println!("second");
5582 }
5583
5584 fn third() {
5585 println!("third");
5586 }
5587 "#});
5588
5589 cx.simulate_keystrokes("[ m");
5590 cx.assert_editor_state(indoc! {r#"
5591 ˇfn first() {
5592 println!("first");
5593 println!("removed line");
5594 }
5595
5596 fn second() {
5597 println!("second");
5598 }
5599
5600 fn third() {
5601 println!("third");
5602 }
5603 "#});
5604 }
5605
5606 #[gpui::test]
5607 async fn test_comment_motion_with_expanded_diff_hunks(cx: &mut gpui::TestAppContext) {
5608 let mut cx = VimTestContext::new(cx, true).await;
5609
5610 let diff_base = indoc! {r#"
5611 // first comment
5612 fn first() {
5613 // removed comment
5614 println!("first");
5615 }
5616
5617 // second comment
5618 fn second() { println!("second"); }
5619 "#};
5620
5621 let current_text = indoc! {r#"
5622 // first comment
5623 fn first() {
5624 println!("first");
5625 }
5626
5627 // second comment
5628 fn second() { println!("second"); }
5629 "#};
5630
5631 cx.set_state(&format!("ˇ{}", current_text), Mode::Normal);
5632 cx.set_head_text(diff_base);
5633 cx.update_editor(|editor, window, cx| {
5634 editor.expand_all_diff_hunks(&editor::actions::ExpandAllDiffHunks, window, cx);
5635 });
5636
5637 // The first `] /` (vim::NextComment) should go to the end of the first
5638 // comment.
5639 cx.simulate_keystrokes("] /");
5640 cx.assert_editor_state(indoc! {r#"
5641 // first commenˇt
5642 fn first() {
5643 // removed comment
5644 println!("first");
5645 }
5646
5647 // second comment
5648 fn second() { println!("second"); }
5649 "#});
5650
5651 // The next `] /` (vim::NextComment) should go to the end of the second
5652 // comment, skipping over the removed comment, since it's not in the
5653 // actual buffer.
5654 cx.simulate_keystrokes("] /");
5655 cx.assert_editor_state(indoc! {r#"
5656 // first comment
5657 fn first() {
5658 // removed comment
5659 println!("first");
5660 }
5661
5662 // second commenˇt
5663 fn second() { println!("second"); }
5664 "#});
5665
5666 // Going back to previous comment with `[ /` (vim::PreviousComment)
5667 // should go back to the start of the second comment.
5668 cx.simulate_keystrokes("[ /");
5669 cx.assert_editor_state(indoc! {r#"
5670 // first comment
5671 fn first() {
5672 // removed comment
5673 println!("first");
5674 }
5675
5676 ˇ// second comment
5677 fn second() { println!("second"); }
5678 "#});
5679
5680 // Going back again with `[ /` (vim::PreviousComment) should finally put
5681 // the cursor at the start of the first comment.
5682 cx.simulate_keystrokes("[ /");
5683 cx.assert_editor_state(indoc! {r#"
5684 ˇ// first comment
5685 fn first() {
5686 // removed comment
5687 println!("first");
5688 }
5689
5690 // second comment
5691 fn second() { println!("second"); }
5692 "#});
5693 }
5694}
5695