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