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