motion.rs

  1use editor::{
  2    char_kind,
  3    display_map::{DisplaySnapshot, ToDisplayPoint},
  4    movement, Bias, CharKind, DisplayPoint,
  5};
  6use gpui::{actions, impl_actions, keymap_matcher::KeyPressed, MutableAppContext};
  7use language::{Point, Selection, SelectionGoal};
  8use serde::Deserialize;
  9use workspace::Workspace;
 10
 11use crate::{
 12    normal::normal_motion,
 13    state::{Mode, Operator},
 14    visual::visual_motion,
 15    Vim,
 16};
 17
 18#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 19pub enum Motion {
 20    Left,
 21    Backspace,
 22    Down,
 23    Up,
 24    Right,
 25    NextWordStart { ignore_punctuation: bool },
 26    NextWordEnd { ignore_punctuation: bool },
 27    PreviousWordStart { ignore_punctuation: bool },
 28    FirstNonWhitespace,
 29    CurrentLine,
 30    StartOfLine,
 31    EndOfLine,
 32    StartOfDocument,
 33    EndOfDocument,
 34    Matching,
 35    FindForward { before: bool, character: char },
 36    FindBackward { after: bool, character: char },
 37}
 38
 39#[derive(Clone, Deserialize, PartialEq)]
 40#[serde(rename_all = "camelCase")]
 41struct NextWordStart {
 42    #[serde(default)]
 43    ignore_punctuation: bool,
 44}
 45
 46#[derive(Clone, Deserialize, PartialEq)]
 47#[serde(rename_all = "camelCase")]
 48struct NextWordEnd {
 49    #[serde(default)]
 50    ignore_punctuation: bool,
 51}
 52
 53#[derive(Clone, Deserialize, PartialEq)]
 54#[serde(rename_all = "camelCase")]
 55struct PreviousWordStart {
 56    #[serde(default)]
 57    ignore_punctuation: bool,
 58}
 59
 60actions!(
 61    vim,
 62    [
 63        Left,
 64        Backspace,
 65        Down,
 66        Up,
 67        Right,
 68        FirstNonWhitespace,
 69        StartOfLine,
 70        EndOfLine,
 71        CurrentLine,
 72        StartOfDocument,
 73        EndOfDocument,
 74        Matching,
 75    ]
 76);
 77impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
 78
 79pub fn init(cx: &mut MutableAppContext) {
 80    cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
 81    cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
 82    cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
 83    cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
 84    cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
 85    cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
 86        motion(Motion::FirstNonWhitespace, cx)
 87    });
 88    cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
 89    cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
 90    cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
 91    cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
 92        motion(Motion::StartOfDocument, cx)
 93    });
 94    cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
 95    cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
 96
 97    cx.add_action(
 98        |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
 99            motion(Motion::NextWordStart { ignore_punctuation }, cx)
100        },
101    );
102    cx.add_action(
103        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
104            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
105        },
106    );
107    cx.add_action(
108        |_: &mut Workspace,
109         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
110         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
111    );
112    cx.add_action(
113        |_: &mut Workspace, KeyPressed { keystroke }: &KeyPressed, cx| match Vim::read(cx)
114            .active_operator()
115        {
116            Some(Operator::FindForward { before }) => motion(
117                Motion::FindForward {
118                    before,
119                    character: keystroke.key.chars().next().unwrap(),
120                },
121                cx,
122            ),
123            Some(Operator::FindBackward { after }) => motion(
124                Motion::FindBackward {
125                    after,
126                    character: keystroke.key.chars().next().unwrap(),
127                },
128                cx,
129            ),
130            _ => cx.propagate_action(),
131        },
132    )
133}
134
135pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
136    if let Some(Operator::Namespace(_))
137    | Some(Operator::FindForward { .. })
138    | Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
139    {
140        Vim::update(cx, |vim, cx| vim.pop_operator(cx));
141    }
142
143    let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
144    let operator = Vim::read(cx).active_operator();
145    match Vim::read(cx).state.mode {
146        Mode::Normal => normal_motion(motion, operator, times, cx),
147        Mode::Visual { .. } => visual_motion(motion, times, cx),
148        Mode::Insert => {
149            // Shouldn't execute a motion in insert mode. Ignoring
150        }
151    }
152    Vim::update(cx, |vim, cx| vim.clear_operator(cx));
153}
154
155// Motion handling is specified here:
156// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
157impl Motion {
158    pub fn linewise(self) -> bool {
159        use Motion::*;
160        matches!(
161            self,
162            Down | Up | StartOfDocument | EndOfDocument | CurrentLine
163        )
164    }
165
166    pub fn infallible(self) -> bool {
167        use Motion::*;
168        matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
169    }
170
171    pub fn inclusive(self) -> bool {
172        use Motion::*;
173        match self {
174            Down
175            | Up
176            | StartOfDocument
177            | EndOfDocument
178            | CurrentLine
179            | EndOfLine
180            | NextWordEnd { .. }
181            | Matching
182            | FindForward { .. } => true,
183            Left
184            | Backspace
185            | Right
186            | StartOfLine
187            | NextWordStart { .. }
188            | PreviousWordStart { .. }
189            | FirstNonWhitespace
190            | FindBackward { .. } => false,
191        }
192    }
193
194    pub fn move_point(
195        self,
196        map: &DisplaySnapshot,
197        point: DisplayPoint,
198        goal: SelectionGoal,
199        times: usize,
200    ) -> Option<(DisplayPoint, SelectionGoal)> {
201        use Motion::*;
202        let (new_point, goal) = match self {
203            Left => (left(map, point, times), SelectionGoal::None),
204            Backspace => (backspace(map, point, times), SelectionGoal::None),
205            Down => down(map, point, goal, times),
206            Up => up(map, point, goal, times),
207            Right => (right(map, point, times), SelectionGoal::None),
208            NextWordStart { ignore_punctuation } => (
209                next_word_start(map, point, ignore_punctuation, times),
210                SelectionGoal::None,
211            ),
212            NextWordEnd { ignore_punctuation } => (
213                next_word_end(map, point, ignore_punctuation, times),
214                SelectionGoal::None,
215            ),
216            PreviousWordStart { ignore_punctuation } => (
217                previous_word_start(map, point, ignore_punctuation, times),
218                SelectionGoal::None,
219            ),
220            FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
221            StartOfLine => (start_of_line(map, point), SelectionGoal::None),
222            EndOfLine => (end_of_line(map, point), SelectionGoal::None),
223            CurrentLine => (end_of_line(map, point), SelectionGoal::None),
224            StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
225            EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
226            Matching => (matching(map, point), SelectionGoal::None),
227            FindForward { before, character } => (
228                find_forward(map, point, before, character, times),
229                SelectionGoal::None,
230            ),
231            FindBackward { after, character } => (
232                find_backward(map, point, after, character, times),
233                SelectionGoal::None,
234            ),
235        };
236
237        (new_point != point || self.infallible()).then_some((new_point, goal))
238    }
239
240    // Expands a selection using self motion for an operator
241    pub fn expand_selection(
242        self,
243        map: &DisplaySnapshot,
244        selection: &mut Selection<DisplayPoint>,
245        times: usize,
246        expand_to_surrounding_newline: bool,
247    ) -> bool {
248        if let Some((new_head, goal)) =
249            self.move_point(map, selection.head(), selection.goal, times)
250        {
251            selection.set_head(new_head, goal);
252
253            if self.linewise() {
254                selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
255
256                if expand_to_surrounding_newline {
257                    if selection.end.row() < map.max_point().row() {
258                        *selection.end.row_mut() += 1;
259                        *selection.end.column_mut() = 0;
260                        selection.end = map.clip_point(selection.end, Bias::Right);
261                        // Don't reset the end here
262                        return true;
263                    } else if selection.start.row() > 0 {
264                        *selection.start.row_mut() -= 1;
265                        *selection.start.column_mut() = map.line_len(selection.start.row());
266                        selection.start = map.clip_point(selection.start, Bias::Left);
267                    }
268                }
269
270                (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
271            } else {
272                // If the motion is exclusive and the end of the motion is in column 1, the
273                // end of the motion is moved to the end of the previous line and the motion
274                // becomes inclusive. Example: "}" moves to the first line after a paragraph,
275                // but "d}" will not include that line.
276                let mut inclusive = self.inclusive();
277                if !inclusive
278                    && self != Motion::Backspace
279                    && selection.end.row() > selection.start.row()
280                    && selection.end.column() == 0
281                {
282                    inclusive = true;
283                    *selection.end.row_mut() -= 1;
284                    *selection.end.column_mut() = 0;
285                    selection.end = map.clip_point(
286                        map.next_line_boundary(selection.end.to_point(map)).1,
287                        Bias::Left,
288                    );
289                }
290
291                if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
292                    *selection.end.column_mut() += 1;
293                }
294            }
295            true
296        } else {
297            false
298        }
299    }
300}
301
302fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
303    for _ in 0..times {
304        *point.column_mut() = point.column().saturating_sub(1);
305        point = map.clip_point(point, Bias::Left);
306        if point.column() == 0 {
307            break;
308        }
309    }
310    point
311}
312
313fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
314    for _ in 0..times {
315        point = movement::left(map, point);
316    }
317    point
318}
319
320fn down(
321    map: &DisplaySnapshot,
322    mut point: DisplayPoint,
323    mut goal: SelectionGoal,
324    times: usize,
325) -> (DisplayPoint, SelectionGoal) {
326    for _ in 0..times {
327        (point, goal) = movement::down(map, point, goal, true);
328    }
329    (point, goal)
330}
331
332fn up(
333    map: &DisplaySnapshot,
334    mut point: DisplayPoint,
335    mut goal: SelectionGoal,
336    times: usize,
337) -> (DisplayPoint, SelectionGoal) {
338    for _ in 0..times {
339        (point, goal) = movement::up(map, point, goal, true);
340    }
341    (point, goal)
342}
343
344pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
345    for _ in 0..times {
346        let mut new_point = point;
347        *new_point.column_mut() += 1;
348        let new_point = map.clip_point(new_point, Bias::Right);
349        if point == new_point {
350            break;
351        }
352        point = new_point;
353    }
354    point
355}
356
357pub(crate) fn next_word_start(
358    map: &DisplaySnapshot,
359    mut point: DisplayPoint,
360    ignore_punctuation: bool,
361    times: usize,
362) -> DisplayPoint {
363    for _ in 0..times {
364        let mut crossed_newline = false;
365        point = movement::find_boundary(map, point, |left, right| {
366            let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
367            let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
368            let at_newline = right == '\n';
369
370            let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
371                || at_newline && crossed_newline
372                || at_newline && left == '\n'; // Prevents skipping repeated empty lines
373
374            crossed_newline |= at_newline;
375            found
376        })
377    }
378    point
379}
380
381fn next_word_end(
382    map: &DisplaySnapshot,
383    mut point: DisplayPoint,
384    ignore_punctuation: bool,
385    times: usize,
386) -> DisplayPoint {
387    for _ in 0..times {
388        *point.column_mut() += 1;
389        point = movement::find_boundary(map, point, |left, right| {
390            let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
391            let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
392
393            left_kind != right_kind && left_kind != CharKind::Whitespace
394        });
395
396        // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
397        // we have backtracked already
398        if !map
399            .chars_at(point)
400            .nth(1)
401            .map(|(c, _)| c == '\n')
402            .unwrap_or(true)
403        {
404            *point.column_mut() = point.column().saturating_sub(1);
405        }
406        point = map.clip_point(point, Bias::Left);
407    }
408    point
409}
410
411fn previous_word_start(
412    map: &DisplaySnapshot,
413    mut point: DisplayPoint,
414    ignore_punctuation: bool,
415    times: usize,
416) -> DisplayPoint {
417    for _ in 0..times {
418        // This works even though find_preceding_boundary is called for every character in the line containing
419        // cursor because the newline is checked only once.
420        point = movement::find_preceding_boundary(map, point, |left, right| {
421            let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
422            let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
423
424            (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
425        });
426    }
427    point
428}
429
430fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
431    let mut last_point = DisplayPoint::new(from.row(), 0);
432    for (ch, point) in map.chars_at(last_point) {
433        if ch == '\n' {
434            return from;
435        }
436
437        last_point = point;
438
439        if char_kind(ch) != CharKind::Whitespace {
440            break;
441        }
442    }
443
444    map.clip_point(last_point, Bias::Left)
445}
446
447fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
448    map.prev_line_boundary(point.to_point(map)).1
449}
450
451fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
452    map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
453}
454
455fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
456    let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
457    *new_point.column_mut() = point.column();
458    map.clip_point(new_point, Bias::Left)
459}
460
461fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
462    let mut new_point = if line == 1 {
463        map.max_point()
464    } else {
465        Point::new((line - 1) as u32, 0).to_display_point(map)
466    };
467    *new_point.column_mut() = point.column();
468    map.clip_point(new_point, Bias::Left)
469}
470
471fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
472    let offset = point.to_offset(map, Bias::Left);
473    if let Some((open_range, close_range)) =
474        map.buffer_snapshot.enclosing_bracket_ranges(offset..offset)
475    {
476        if open_range.contains(&offset) {
477            close_range.start.to_display_point(map)
478        } else {
479            open_range.start.to_display_point(map)
480        }
481    } else {
482        point
483    }
484}
485
486fn find_forward(
487    map: &DisplaySnapshot,
488    from: DisplayPoint,
489    before: bool,
490    target: char,
491    mut times: usize,
492) -> DisplayPoint {
493    let mut previous_point = from;
494
495    for (ch, point) in map.chars_at(from) {
496        if ch == target && point != from {
497            times -= 1;
498            if times == 0 {
499                return if before { previous_point } else { point };
500            }
501        } else if ch == '\n' {
502            break;
503        }
504        previous_point = point;
505    }
506
507    from
508}
509
510fn find_backward(
511    map: &DisplaySnapshot,
512    from: DisplayPoint,
513    after: bool,
514    target: char,
515    mut times: usize,
516) -> DisplayPoint {
517    let mut previous_point = from;
518    for (ch, point) in map.reverse_chars_at(from) {
519        if ch == target && point != from {
520            times -= 1;
521            if times == 0 {
522                return if after { previous_point } else { point };
523            }
524        } else if ch == '\n' {
525            break;
526        }
527        previous_point = point;
528    }
529
530    from
531}