motion.rs

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