motion.rs

  1use std::{cmp, sync::Arc};
  2
  3use editor::{
  4    char_kind,
  5    display_map::{DisplaySnapshot, ToDisplayPoint},
  6    movement, Bias, CharKind, DisplayPoint, ToOffset,
  7};
  8use gpui::{actions, impl_actions, AppContext, WindowContext};
  9use language::{Point, Selection, SelectionGoal};
 10use serde::Deserialize;
 11use workspace::Workspace;
 12
 13use crate::{
 14    normal::normal_motion,
 15    state::{Mode, Operator},
 16    visual::visual_motion,
 17    Vim,
 18};
 19
 20#[derive(Clone, Debug, PartialEq, Eq)]
 21pub enum Motion {
 22    Left,
 23    Backspace,
 24    Down,
 25    Up,
 26    Right,
 27    NextWordStart { ignore_punctuation: bool },
 28    NextWordEnd { ignore_punctuation: bool },
 29    PreviousWordStart { ignore_punctuation: bool },
 30    FirstNonWhitespace,
 31    CurrentLine,
 32    StartOfLine,
 33    EndOfLine,
 34    StartOfParagraph,
 35    EndOfParagraph,
 36    StartOfDocument,
 37    EndOfDocument,
 38    Matching,
 39    FindForward { before: bool, text: Arc<str> },
 40    FindBackward { after: bool, text: Arc<str> },
 41    NextLineStart,
 42}
 43
 44#[derive(Clone, Deserialize, PartialEq)]
 45#[serde(rename_all = "camelCase")]
 46struct NextWordStart {
 47    #[serde(default)]
 48    ignore_punctuation: bool,
 49}
 50
 51#[derive(Clone, Deserialize, PartialEq)]
 52#[serde(rename_all = "camelCase")]
 53struct NextWordEnd {
 54    #[serde(default)]
 55    ignore_punctuation: bool,
 56}
 57
 58#[derive(Clone, Deserialize, PartialEq)]
 59#[serde(rename_all = "camelCase")]
 60struct PreviousWordStart {
 61    #[serde(default)]
 62    ignore_punctuation: bool,
 63}
 64
 65#[derive(Clone, Deserialize, PartialEq)]
 66struct RepeatFind {
 67    #[serde(default)]
 68    backwards: bool,
 69}
 70
 71actions!(
 72    vim,
 73    [
 74        Left,
 75        Backspace,
 76        Down,
 77        Up,
 78        Right,
 79        FirstNonWhitespace,
 80        StartOfLine,
 81        EndOfLine,
 82        CurrentLine,
 83        StartOfParagraph,
 84        EndOfParagraph,
 85        StartOfDocument,
 86        EndOfDocument,
 87        Matching,
 88        NextLineStart,
 89    ]
 90);
 91impl_actions!(
 92    vim,
 93    [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind]
 94);
 95
 96pub fn init(cx: &mut AppContext) {
 97    cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
 98    cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
 99    cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
100    cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
101    cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
102    cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
103        motion(Motion::FirstNonWhitespace, cx)
104    });
105    cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
106    cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
107    cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
108    cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
109        motion(Motion::StartOfParagraph, cx)
110    });
111    cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
112        motion(Motion::EndOfParagraph, cx)
113    });
114    cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
115        motion(Motion::StartOfDocument, cx)
116    });
117    cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
118    cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
119
120    cx.add_action(
121        |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
122            motion(Motion::NextWordStart { ignore_punctuation }, cx)
123        },
124    );
125    cx.add_action(
126        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
127            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
128        },
129    );
130    cx.add_action(
131        |_: &mut Workspace,
132         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
133         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
134    );
135    cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx));
136    cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
137        repeat_motion(action.backwards, cx)
138    })
139}
140
141pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
142    if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
143        Vim::read(cx).active_operator()
144    {
145        Vim::update(cx, |vim, cx| vim.pop_operator(cx));
146    }
147
148    let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
149    let operator = Vim::read(cx).active_operator();
150    match Vim::read(cx).state().mode {
151        Mode::Normal => normal_motion(motion, operator, times, cx),
152        Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, times, cx),
153        Mode::Insert => {
154            // Shouldn't execute a motion in insert mode. Ignoring
155        }
156    }
157    Vim::update(cx, |vim, cx| vim.clear_operator(cx));
158}
159
160fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
161    let find = match Vim::read(cx).workspace_state.last_find.clone() {
162        Some(Motion::FindForward { before, text }) => {
163            if backwards {
164                Motion::FindBackward {
165                    after: before,
166                    text,
167                }
168            } else {
169                Motion::FindForward { before, text }
170            }
171        }
172
173        Some(Motion::FindBackward { after, text }) => {
174            if backwards {
175                Motion::FindForward {
176                    before: after,
177                    text,
178                }
179            } else {
180                Motion::FindBackward { after, text }
181            }
182        }
183        _ => return,
184    };
185
186    motion(find, cx)
187}
188
189// Motion handling is specified here:
190// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
191impl Motion {
192    pub fn linewise(&self) -> bool {
193        use Motion::*;
194        match self {
195            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
196            | StartOfParagraph | EndOfParagraph => true,
197            EndOfLine
198            | NextWordEnd { .. }
199            | Matching
200            | FindForward { .. }
201            | Left
202            | Backspace
203            | Right
204            | StartOfLine
205            | NextWordStart { .. }
206            | PreviousWordStart { .. }
207            | FirstNonWhitespace
208            | FindBackward { .. } => false,
209        }
210    }
211
212    pub fn infallible(&self) -> bool {
213        use Motion::*;
214        match self {
215            StartOfDocument | EndOfDocument | CurrentLine => true,
216            Down
217            | Up
218            | EndOfLine
219            | NextWordEnd { .. }
220            | Matching
221            | FindForward { .. }
222            | Left
223            | Backspace
224            | Right
225            | StartOfLine
226            | StartOfParagraph
227            | EndOfParagraph
228            | NextWordStart { .. }
229            | PreviousWordStart { .. }
230            | FirstNonWhitespace
231            | FindBackward { .. }
232            | NextLineStart => false,
233        }
234    }
235
236    pub fn inclusive(&self) -> bool {
237        use Motion::*;
238        match self {
239            Down
240            | Up
241            | StartOfDocument
242            | EndOfDocument
243            | CurrentLine
244            | EndOfLine
245            | NextWordEnd { .. }
246            | Matching
247            | FindForward { .. }
248            | NextLineStart => true,
249            Left
250            | Backspace
251            | Right
252            | StartOfLine
253            | StartOfParagraph
254            | EndOfParagraph
255            | NextWordStart { .. }
256            | PreviousWordStart { .. }
257            | FirstNonWhitespace
258            | FindBackward { .. } => false,
259        }
260    }
261
262    pub fn move_point(
263        &self,
264        map: &DisplaySnapshot,
265        point: DisplayPoint,
266        goal: SelectionGoal,
267        maybe_times: Option<usize>,
268    ) -> Option<(DisplayPoint, SelectionGoal)> {
269        let times = maybe_times.unwrap_or(1);
270        use Motion::*;
271        let infallible = self.infallible();
272        let (new_point, goal) = match self {
273            Left => (left(map, point, times), SelectionGoal::None),
274            Backspace => (backspace(map, point, times), SelectionGoal::None),
275            Down => down(map, point, goal, times),
276            Up => up(map, point, goal, times),
277            Right => (right(map, point, times), SelectionGoal::None),
278            NextWordStart { ignore_punctuation } => (
279                next_word_start(map, point, *ignore_punctuation, times),
280                SelectionGoal::None,
281            ),
282            NextWordEnd { ignore_punctuation } => (
283                next_word_end(map, point, *ignore_punctuation, times),
284                SelectionGoal::None,
285            ),
286            PreviousWordStart { ignore_punctuation } => (
287                previous_word_start(map, point, *ignore_punctuation, times),
288                SelectionGoal::None,
289            ),
290            FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
291            StartOfLine => (start_of_line(map, point), SelectionGoal::None),
292            EndOfLine => (end_of_line(map, point), SelectionGoal::None),
293            StartOfParagraph => (
294                movement::start_of_paragraph(map, point, times),
295                SelectionGoal::None,
296            ),
297            EndOfParagraph => (
298                map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
299                SelectionGoal::None,
300            ),
301            CurrentLine => (end_of_line(map, point), SelectionGoal::None),
302            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
303            EndOfDocument => (
304                end_of_document(map, point, maybe_times),
305                SelectionGoal::None,
306            ),
307            Matching => (matching(map, point), SelectionGoal::None),
308            FindForward { before, text } => (
309                find_forward(map, point, *before, text.clone(), times),
310                SelectionGoal::None,
311            ),
312            FindBackward { after, text } => (
313                find_backward(map, point, *after, text.clone(), times),
314                SelectionGoal::None,
315            ),
316            NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
317        };
318
319        (new_point != point || infallible).then_some((new_point, goal))
320    }
321
322    // Expands a selection using self motion for an operator
323    pub fn expand_selection(
324        &self,
325        map: &DisplaySnapshot,
326        selection: &mut Selection<DisplayPoint>,
327        times: Option<usize>,
328        expand_to_surrounding_newline: bool,
329    ) -> bool {
330        if let Some((new_head, goal)) =
331            self.move_point(map, selection.head(), selection.goal, times)
332        {
333            selection.set_head(new_head, goal);
334
335            if self.linewise() {
336                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
337
338                if expand_to_surrounding_newline {
339                    if selection.end.row() < map.max_point().row() {
340                        *selection.end.row_mut() += 1;
341                        *selection.end.column_mut() = 0;
342                        selection.end = map.clip_point(selection.end, Bias::Right);
343                        // Don't reset the end here
344                        return true;
345                    } else if selection.start.row() > 0 {
346                        *selection.start.row_mut() -= 1;
347                        *selection.start.column_mut() = map.line_len(selection.start.row());
348                        selection.start = map.clip_point(selection.start, Bias::Left);
349                    }
350                }
351
352                (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
353            } else {
354                // If the motion is exclusive and the end of the motion is in column 1, the
355                // end of the motion is moved to the end of the previous line and the motion
356                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
357                // but "d}" will not include that line.
358                let mut inclusive = self.inclusive();
359                if !inclusive
360                    && self != &Motion::Backspace
361                    && selection.end.row() > selection.start.row()
362                    && selection.end.column() == 0
363                {
364                    inclusive = true;
365                    *selection.end.row_mut() -= 1;
366                    *selection.end.column_mut() = 0;
367                    selection.end = map.clip_point(
368                        map.next_line_boundary(selection.end.to_point(map)).1,
369                        Bias::Left,
370                    );
371                }
372
373                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
374                    *selection.end.column_mut() += 1;
375                }
376            }
377            true
378        } else {
379            false
380        }
381    }
382}
383
384fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
385    for _ in 0..times {
386        point = movement::saturating_left(map, point);
387        if point.column() == 0 {
388            break;
389        }
390    }
391    point
392}
393
394fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
395    for _ in 0..times {
396        point = movement::left(map, point);
397    }
398    point
399}
400
401fn down(
402    map: &DisplaySnapshot,
403    mut point: DisplayPoint,
404    mut goal: SelectionGoal,
405    times: usize,
406) -> (DisplayPoint, SelectionGoal) {
407    let start_row = point.to_point(map).row;
408    let target = cmp::min(map.max_buffer_row(), start_row + times as u32);
409
410    while point.to_point(map).row < target {
411        (point, goal) = movement::down(map, point, goal, true);
412    }
413
414    (point, goal)
415}
416
417fn up(
418    map: &DisplaySnapshot,
419    mut point: DisplayPoint,
420    mut goal: SelectionGoal,
421    times: usize,
422) -> (DisplayPoint, SelectionGoal) {
423    let start_row = point.to_point(map).row;
424    let target = start_row.saturating_sub(times as u32);
425
426    while point.to_point(map).row > target {
427        (point, goal) = movement::up(map, point, goal, true);
428    }
429    (point, goal)
430}
431
432pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
433    for _ in 0..times {
434        let new_point = movement::saturating_right(map, point);
435        if point == new_point {
436            break;
437        }
438        point = new_point;
439    }
440    point
441}
442
443pub(crate) fn next_word_start(
444    map: &DisplaySnapshot,
445    mut point: DisplayPoint,
446    ignore_punctuation: bool,
447    times: usize,
448) -> DisplayPoint {
449    let language = map.buffer_snapshot.language_at(point.to_point(map));
450    for _ in 0..times {
451        let mut crossed_newline = false;
452        point = movement::find_boundary(map, point, |left, right| {
453            let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
454            let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
455            let at_newline = right == '\n';
456
457            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
458                || at_newline && crossed_newline
459                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
460
461            crossed_newline |= at_newline;
462            found
463        })
464    }
465    point
466}
467
468fn next_word_end(
469    map: &DisplaySnapshot,
470    mut point: DisplayPoint,
471    ignore_punctuation: bool,
472    times: usize,
473) -> DisplayPoint {
474    let language = map.buffer_snapshot.language_at(point.to_point(map));
475    for _ in 0..times {
476        *point.column_mut() += 1;
477        point = movement::find_boundary(map, point, |left, right| {
478            let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
479            let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
480
481            left_kind != right_kind && left_kind != CharKind::Whitespace
482        });
483
484        // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
485        // we have backtracked already
486        if !map
487            .chars_at(point)
488            .nth(1)
489            .map(|(c, _)| c == '\n')
490            .unwrap_or(true)
491        {
492            *point.column_mut() = point.column().saturating_sub(1);
493        }
494        point = map.clip_point(point, Bias::Left);
495    }
496    point
497}
498
499fn previous_word_start(
500    map: &DisplaySnapshot,
501    mut point: DisplayPoint,
502    ignore_punctuation: bool,
503    times: usize,
504) -> DisplayPoint {
505    let language = map.buffer_snapshot.language_at(point.to_point(map));
506    for _ in 0..times {
507        // This works even though find_preceding_boundary is called for every character in the line containing
508        // cursor because the newline is checked only once.
509        point = movement::find_preceding_boundary(map, point, |left, right| {
510            let left_kind = char_kind(language, left).coerce_punctuation(ignore_punctuation);
511            let right_kind = char_kind(language, right).coerce_punctuation(ignore_punctuation);
512
513            (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
514        });
515    }
516    point
517}
518
519fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
520    let mut last_point = DisplayPoint::new(from.row(), 0);
521    let language = map.buffer_snapshot.language_at(from.to_point(map));
522    for (ch, point) in map.chars_at(last_point) {
523        if ch == '\n' {
524            return from;
525        }
526
527        last_point = point;
528
529        if char_kind(language, ch) != CharKind::Whitespace {
530            break;
531        }
532    }
533
534    map.clip_point(last_point, Bias::Left)
535}
536
537fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
538    map.prev_line_boundary(point.to_point(map)).1
539}
540
541fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
542    map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
543}
544
545fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
546    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
547    *new_point.column_mut() = point.column();
548    map.clip_point(new_point, Bias::Left)
549}
550
551fn end_of_document(
552    map: &DisplaySnapshot,
553    point: DisplayPoint,
554    line: Option<usize>,
555) -> DisplayPoint {
556    let new_row = if let Some(line) = line {
557        (line - 1) as u32
558    } else {
559        map.max_buffer_row()
560    };
561
562    let new_point = Point::new(new_row, point.column());
563    map.clip_point(new_point.to_display_point(map), Bias::Left)
564}
565
566fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
567    // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
568    let point = display_point.to_point(map);
569    let offset = point.to_offset(&map.buffer_snapshot);
570
571    // Ensure the range is contained by the current line.
572    let mut line_end = map.next_line_boundary(point).0;
573    if line_end == point {
574        line_end = map.max_point().to_point(map);
575    }
576
577    let line_range = map.prev_line_boundary(point).0..line_end;
578    let visible_line_range =
579        line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
580    let ranges = map
581        .buffer_snapshot
582        .bracket_ranges(visible_line_range.clone());
583    if let Some(ranges) = ranges {
584        let line_range = line_range.start.to_offset(&map.buffer_snapshot)
585            ..line_range.end.to_offset(&map.buffer_snapshot);
586        let mut closest_pair_destination = None;
587        let mut closest_distance = usize::MAX;
588
589        for (open_range, close_range) in ranges {
590            if open_range.start >= offset && line_range.contains(&open_range.start) {
591                let distance = open_range.start - offset;
592                if distance < closest_distance {
593                    closest_pair_destination = Some(close_range.start);
594                    closest_distance = distance;
595                    continue;
596                }
597            }
598
599            if close_range.start >= offset && line_range.contains(&close_range.start) {
600                let distance = close_range.start - offset;
601                if distance < closest_distance {
602                    closest_pair_destination = Some(open_range.start);
603                    closest_distance = distance;
604                    continue;
605                }
606            }
607
608            continue;
609        }
610
611        closest_pair_destination
612            .map(|destination| destination.to_display_point(map))
613            .unwrap_or(display_point)
614    } else {
615        display_point
616    }
617}
618
619fn find_forward(
620    map: &DisplaySnapshot,
621    from: DisplayPoint,
622    before: bool,
623    target: Arc<str>,
624    times: usize,
625) -> DisplayPoint {
626    map.find_while(from, target.as_ref(), |ch, _| ch != '\n')
627        .skip_while(|found_at| found_at == &from)
628        .nth(times - 1)
629        .map(|mut found| {
630            if before {
631                *found.column_mut() -= 1;
632                found = map.clip_point(found, Bias::Right);
633                found
634            } else {
635                found
636            }
637        })
638        .unwrap_or(from)
639}
640
641fn find_backward(
642    map: &DisplaySnapshot,
643    from: DisplayPoint,
644    after: bool,
645    target: Arc<str>,
646    times: usize,
647) -> DisplayPoint {
648    map.reverse_find_while(from, target.as_ref(), |ch, _| ch != '\n')
649        .skip_while(|found_at| found_at == &from)
650        .nth(times - 1)
651        .map(|mut found| {
652            if after {
653                *found.column_mut() += 1;
654                found = map.clip_point(found, Bias::Left);
655                found
656            } else {
657                found
658            }
659        })
660        .unwrap_or(from)
661}
662
663fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
664    let new_row = (point.row() + times as u32).min(map.max_buffer_row());
665    first_non_whitespace(
666        map,
667        map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left),
668    )
669}
670
671#[cfg(test)]
672
673mod test {
674
675    use crate::test::NeovimBackedTestContext;
676    use indoc::indoc;
677
678    #[gpui::test]
679    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
680        let mut cx = NeovimBackedTestContext::new(cx).await;
681
682        let initial_state = indoc! {r"ˇabc
683            def
684
685            paragraph
686            the second
687
688
689
690            third and
691            final"};
692
693        // goes down once
694        cx.set_shared_state(initial_state).await;
695        cx.simulate_shared_keystrokes(["}"]).await;
696        cx.assert_shared_state(indoc! {r"abc
697            def
698            ˇ
699            paragraph
700            the second
701
702
703
704            third and
705            final"})
706            .await;
707
708        // goes up once
709        cx.simulate_shared_keystrokes(["{"]).await;
710        cx.assert_shared_state(initial_state).await;
711
712        // goes down twice
713        cx.simulate_shared_keystrokes(["2", "}"]).await;
714        cx.assert_shared_state(indoc! {r"abc
715            def
716
717            paragraph
718            the second
719            ˇ
720
721
722            third and
723            final"})
724            .await;
725
726        // goes down over multiple blanks
727        cx.simulate_shared_keystrokes(["}"]).await;
728        cx.assert_shared_state(indoc! {r"abc
729                def
730
731                paragraph
732                the second
733
734
735
736                third and
737                finaˇl"})
738            .await;
739
740        // goes up twice
741        cx.simulate_shared_keystrokes(["2", "{"]).await;
742        cx.assert_shared_state(indoc! {r"abc
743                def
744                ˇ
745                paragraph
746                the second
747
748
749
750                third and
751                final"})
752            .await
753    }
754
755    #[gpui::test]
756    async fn test_matching(cx: &mut gpui::TestAppContext) {
757        let mut cx = NeovimBackedTestContext::new(cx).await;
758
759        cx.set_shared_state(indoc! {r"func ˇ(a string) {
760                do(something(with<Types>.and_arrays[0, 2]))
761            }"})
762            .await;
763        cx.simulate_shared_keystrokes(["%"]).await;
764        cx.assert_shared_state(indoc! {r"func (a stringˇ) {
765                do(something(with<Types>.and_arrays[0, 2]))
766            }"})
767            .await;
768
769        // test it works on the last character of the line
770        cx.set_shared_state(indoc! {r"func (a string) ˇ{
771            do(something(with<Types>.and_arrays[0, 2]))
772            }"})
773            .await;
774        cx.simulate_shared_keystrokes(["%"]).await;
775        cx.assert_shared_state(indoc! {r"func (a string) {
776            do(something(with<Types>.and_arrays[0, 2]))
777            ˇ}"})
778            .await;
779
780        // test it works on immediate nesting
781        cx.set_shared_state("ˇ{()}").await;
782        cx.simulate_shared_keystrokes(["%"]).await;
783        cx.assert_shared_state("{()ˇ}").await;
784        cx.simulate_shared_keystrokes(["%"]).await;
785        cx.assert_shared_state("ˇ{()}").await;
786
787        // test it works on immediate nesting inside braces
788        cx.set_shared_state("{\n    ˇ{()}\n}").await;
789        cx.simulate_shared_keystrokes(["%"]).await;
790        cx.assert_shared_state("{\n    {()ˇ}\n}").await;
791
792        // test it jumps to the next paren on a line
793        cx.set_shared_state("func ˇboop() {\n}").await;
794        cx.simulate_shared_keystrokes(["%"]).await;
795        cx.assert_shared_state("func boop(ˇ) {\n}").await;
796    }
797
798    #[gpui::test]
799    async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
800        let mut cx = NeovimBackedTestContext::new(cx).await;
801
802        cx.set_shared_state("ˇone two three four").await;
803        cx.simulate_shared_keystrokes(["f", "o"]).await;
804        cx.assert_shared_state("one twˇo three four").await;
805        cx.simulate_shared_keystrokes([","]).await;
806        cx.assert_shared_state("ˇone two three four").await;
807        cx.simulate_shared_keystrokes(["2", ";"]).await;
808        cx.assert_shared_state("one two three fˇour").await;
809        cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
810        cx.assert_shared_state("one two threeˇ four").await;
811        cx.simulate_shared_keystrokes(["3", ";"]).await;
812        cx.assert_shared_state("oneˇ two three four").await;
813        cx.simulate_shared_keystrokes([","]).await;
814        cx.assert_shared_state("one two thˇree four").await;
815    }
816
817    #[gpui::test]
818    async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
819        let mut cx = NeovimBackedTestContext::new(cx).await;
820        cx.set_shared_state("ˇone\n  two\nthree").await;
821        cx.simulate_shared_keystrokes(["enter"]).await;
822        cx.assert_shared_state("one\n  ˇtwo\nthree").await;
823    }
824}