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