motion.rs

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