motion.rs

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