motion.rs

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