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::{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)]
 19pub enum Motion {
 20    Left,
 21    Down,
 22    Up,
 23    Right,
 24    NextWordStart { ignore_punctuation: bool },
 25    NextWordEnd { ignore_punctuation: bool },
 26    PreviousWordStart { ignore_punctuation: bool },
 27    FirstNonWhitespace,
 28    CurrentLine,
 29    StartOfLine,
 30    EndOfLine,
 31    StartOfDocument,
 32    EndOfDocument,
 33}
 34
 35#[derive(Clone, Deserialize)]
 36#[serde(rename_all = "camelCase")]
 37struct NextWordStart {
 38    #[serde(default)]
 39    ignore_punctuation: bool,
 40}
 41
 42#[derive(Clone, Deserialize)]
 43#[serde(rename_all = "camelCase")]
 44struct NextWordEnd {
 45    #[serde(default)]
 46    ignore_punctuation: bool,
 47}
 48
 49#[derive(Clone, Deserialize)]
 50#[serde(rename_all = "camelCase")]
 51struct PreviousWordStart {
 52    #[serde(default)]
 53    ignore_punctuation: bool,
 54}
 55
 56actions!(
 57    vim,
 58    [
 59        Left,
 60        Down,
 61        Up,
 62        Right,
 63        FirstNonWhitespace,
 64        StartOfLine,
 65        EndOfLine,
 66        CurrentLine,
 67        StartOfDocument,
 68        EndOfDocument
 69    ]
 70);
 71impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]);
 72
 73pub fn init(cx: &mut MutableAppContext) {
 74    cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
 75    cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
 76    cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
 77    cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
 78    cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
 79        motion(Motion::FirstNonWhitespace, cx)
 80    });
 81    cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
 82    cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
 83    cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
 84    cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
 85        motion(Motion::StartOfDocument, cx)
 86    });
 87    cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
 88
 89    cx.add_action(
 90        |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
 91            motion(Motion::NextWordStart { ignore_punctuation }, cx)
 92        },
 93    );
 94    cx.add_action(
 95        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
 96            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
 97        },
 98    );
 99    cx.add_action(
100        |_: &mut Workspace,
101         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
102         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
103    );
104}
105
106fn motion(motion: Motion, cx: &mut MutableAppContext) {
107    Vim::update(cx, |vim, cx| {
108        if let Some(Operator::Namespace(_)) = vim.active_operator() {
109            vim.pop_operator(cx);
110        }
111    });
112    match Vim::read(cx).state.mode {
113        Mode::Normal => normal_motion(motion, cx),
114        Mode::Visual => visual_motion(motion, cx),
115        Mode::VisualLine => visual_motion(motion, cx),
116        Mode::Insert => {
117            // Shouldn't execute a motion in insert mode. Ignoring
118        }
119    }
120}
121
122// Motion handling is specified here:
123// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
124impl Motion {
125    pub fn linewise(self) -> bool {
126        use Motion::*;
127        match self {
128            Down | Up | StartOfDocument | EndOfDocument | CurrentLine => true,
129            _ => false,
130        }
131    }
132
133    pub fn inclusive(self) -> bool {
134        use Motion::*;
135        if self.linewise() {
136            return true;
137        }
138
139        match self {
140            EndOfLine | NextWordEnd { .. } => true,
141            Left | Right | StartOfLine | NextWordStart { .. } | PreviousWordStart { .. } => false,
142            _ => panic!("Exclusivity not defined for {self:?}"),
143        }
144    }
145
146    pub fn move_point(
147        self,
148        map: &DisplaySnapshot,
149        point: DisplayPoint,
150        goal: SelectionGoal,
151    ) -> (DisplayPoint, SelectionGoal) {
152        use Motion::*;
153        match self {
154            Left => (left(map, point), SelectionGoal::None),
155            Down => movement::down(map, point, goal, true),
156            Up => movement::up(map, point, goal, true),
157            Right => (right(map, point), SelectionGoal::None),
158            NextWordStart { ignore_punctuation } => (
159                next_word_start(map, point, ignore_punctuation),
160                SelectionGoal::None,
161            ),
162            NextWordEnd { ignore_punctuation } => (
163                next_word_end(map, point, ignore_punctuation),
164                SelectionGoal::None,
165            ),
166            PreviousWordStart { ignore_punctuation } => (
167                previous_word_start(map, point, ignore_punctuation),
168                SelectionGoal::None,
169            ),
170            FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
171            StartOfLine => (start_of_line(map, point), SelectionGoal::None),
172            EndOfLine => (end_of_line(map, point), SelectionGoal::None),
173            CurrentLine => (end_of_line(map, point), SelectionGoal::None),
174            StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
175            EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
176        }
177    }
178
179    // Expands a selection using self motion for an operator
180    pub fn expand_selection(
181        self,
182        map: &DisplaySnapshot,
183        selection: &mut Selection<DisplayPoint>,
184        expand_to_surrounding_newline: bool,
185    ) {
186        let (head, goal) = self.move_point(map, selection.head(), selection.goal);
187        selection.set_head(head, goal);
188
189        if self.linewise() {
190            selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
191
192            if expand_to_surrounding_newline {
193                if selection.end.row() < map.max_point().row() {
194                    *selection.end.row_mut() += 1;
195                    *selection.end.column_mut() = 0;
196                    selection.end = map.clip_point(selection.end, Bias::Right);
197                    // Don't reset the end here
198                    return;
199                } else if selection.start.row() > 0 {
200                    *selection.start.row_mut() -= 1;
201                    *selection.start.column_mut() = map.line_len(selection.start.row());
202                    selection.start = map.clip_point(selection.start, Bias::Left);
203                }
204            }
205
206            selection.end = map.next_line_boundary(selection.end.to_point(map)).1;
207        } else {
208            // If the motion is exclusive and the end of the motion is in column 1, the
209            // end of the motion is moved to the end of the previous line and the motion
210            // becomes inclusive. Example: "}" moves to the first line after a paragraph,
211            // but "d}" will not include that line.
212            let mut inclusive = self.inclusive();
213            if !inclusive
214                && selection.end.row() > selection.start.row()
215                && selection.end.column() == 0
216                && selection.end.row() > 0
217            {
218                inclusive = true;
219                *selection.end.row_mut() -= 1;
220                *selection.end.column_mut() = 0;
221                selection.end = map.clip_point(
222                    map.next_line_boundary(selection.end.to_point(map)).1,
223                    Bias::Left,
224                );
225            }
226
227            if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
228                *selection.end.column_mut() += 1;
229            }
230        }
231    }
232}
233
234fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
235    *point.column_mut() = point.column().saturating_sub(1);
236    map.clip_point(point, Bias::Left)
237}
238
239fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
240    *point.column_mut() += 1;
241    map.clip_point(point, Bias::Right)
242}
243
244fn next_word_start(
245    map: &DisplaySnapshot,
246    point: DisplayPoint,
247    ignore_punctuation: bool,
248) -> DisplayPoint {
249    let mut crossed_newline = false;
250    movement::find_boundary(map, point, |left, right| {
251        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
252        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
253        let at_newline = right == '\n';
254
255        let found = (left_kind != right_kind && !right.is_whitespace())
256            || at_newline && crossed_newline
257            || at_newline && left == '\n'; // Prevents skipping repeated empty lines
258
259        if at_newline {
260            crossed_newline = true;
261        }
262        found
263    })
264}
265
266fn next_word_end(
267    map: &DisplaySnapshot,
268    mut point: DisplayPoint,
269    ignore_punctuation: bool,
270) -> DisplayPoint {
271    *point.column_mut() += 1;
272    point = movement::find_boundary(map, point, |left, right| {
273        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
274        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
275
276        left_kind != right_kind && !left.is_whitespace()
277    });
278    // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
279    // we have backtraced already
280    if !map
281        .chars_at(point)
282        .skip(1)
283        .next()
284        .map(|c| c == '\n')
285        .unwrap_or(true)
286    {
287        *point.column_mut() = point.column().saturating_sub(1);
288    }
289    map.clip_point(point, Bias::Left)
290}
291
292fn previous_word_start(
293    map: &DisplaySnapshot,
294    mut point: DisplayPoint,
295    ignore_punctuation: bool,
296) -> DisplayPoint {
297    // This works even though find_preceding_boundary is called for every character in the line containing
298    // cursor because the newline is checked only once.
299    point = movement::find_preceding_boundary(map, point, |left, right| {
300        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
301        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
302
303        (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
304    });
305    point
306}
307
308fn first_non_whitespace(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
309    let mut column = 0;
310    for ch in map.chars_at(DisplayPoint::new(point.row(), 0)) {
311        if ch == '\n' {
312            return point;
313        }
314
315        if char_kind(ch) != CharKind::Whitespace {
316            break;
317        }
318
319        column += ch.len_utf8() as u32;
320    }
321
322    *point.column_mut() = column;
323    map.clip_point(point, Bias::Left)
324}
325
326fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
327    map.prev_line_boundary(point.to_point(map)).1
328}
329
330fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
331    map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
332}
333
334fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
335    let mut new_point = 0usize.to_display_point(map);
336    *new_point.column_mut() = point.column();
337    map.clip_point(new_point, Bias::Left)
338}
339
340fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
341    let mut new_point = map.max_point();
342    *new_point.column_mut() = point.column();
343    map.clip_point(new_point, Bias::Left)
344}