motion.rs

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