Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:33:56.227Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

motion.rs

5695 lines · 186.9 KB · rust
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, &current_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, &current_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
Served at tenant.openagents/omega Member data and write actions are omitted.