motion.rs

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