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