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)]
 18pub enum Motion {
 19    Left,
 20    Down,
 21    Up,
 22    Right,
 23    NextWordStart {
 24        ignore_punctuation: bool,
 25        stop_at_newline: bool,
 26    },
 27    NextWordEnd {
 28        ignore_punctuation: bool,
 29    },
 30    PreviousWordStart {
 31        ignore_punctuation: bool,
 32    },
 33    StartOfLine,
 34    EndOfLine,
 35    StartOfDocument,
 36    EndOfDocument,
 37}
 38
 39#[derive(Clone, Deserialize)]
 40#[serde(rename_all = "camelCase")]
 41struct NextWordStart {
 42    #[serde(default)]
 43    ignore_punctuation: bool,
 44    #[serde(default)]
 45    stop_at_newline: bool,
 46}
 47
 48#[derive(Clone, Deserialize)]
 49#[serde(rename_all = "camelCase")]
 50struct NextWordEnd {
 51    #[serde(default)]
 52    ignore_punctuation: bool,
 53}
 54
 55#[derive(Clone, Deserialize)]
 56#[serde(rename_all = "camelCase")]
 57struct PreviousWordStart {
 58    #[serde(default)]
 59    ignore_punctuation: bool,
 60}
 61
 62actions!(
 63    vim,
 64    [
 65        Left,
 66        Down,
 67        Up,
 68        Right,
 69        StartOfLine,
 70        EndOfLine,
 71        StartOfDocument,
 72        EndOfDocument
 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, _: &Down, cx: _| motion(Motion::Down, cx));
 80    cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
 81    cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
 82    cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
 83    cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, 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,
 91         &NextWordStart {
 92             ignore_punctuation,
 93             stop_at_newline,
 94         }: &NextWordStart,
 95         cx: _| {
 96            motion(
 97                Motion::NextWordStart {
 98                    ignore_punctuation,
 99                    stop_at_newline,
100                },
101                cx,
102            )
103        },
104    );
105    cx.add_action(
106        |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
107            motion(Motion::NextWordEnd { ignore_punctuation }, cx)
108        },
109    );
110    cx.add_action(
111        |_: &mut Workspace,
112         &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
113         cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
114    );
115}
116
117fn motion(motion: Motion, cx: &mut MutableAppContext) {
118    Vim::update(cx, |vim, cx| {
119        if let Some(Operator::Namespace(_)) = vim.active_operator() {
120            vim.pop_operator(cx);
121        }
122    });
123    match Vim::read(cx).state.mode {
124        Mode::Normal => normal_motion(motion, cx),
125        Mode::Insert => panic!("motion bindings in insert mode interfere with normal typing"),
126    }
127}
128
129impl Motion {
130    pub fn move_point(
131        self,
132        map: &DisplaySnapshot,
133        point: DisplayPoint,
134        goal: SelectionGoal,
135    ) -> (DisplayPoint, SelectionGoal) {
136        use Motion::*;
137        match self {
138            Left => (left(map, point), SelectionGoal::None),
139            Down => movement::down(map, point, goal),
140            Up => movement::up(map, point, goal),
141            Right => (right(map, point), SelectionGoal::None),
142            NextWordStart {
143                ignore_punctuation,
144                stop_at_newline,
145            } => (
146                next_word_start(map, point, ignore_punctuation, stop_at_newline),
147                SelectionGoal::None,
148            ),
149            NextWordEnd { ignore_punctuation } => (
150                next_word_end(map, point, ignore_punctuation, true),
151                SelectionGoal::None,
152            ),
153            PreviousWordStart { ignore_punctuation } => (
154                previous_word_start(map, point, ignore_punctuation),
155                SelectionGoal::None,
156            ),
157            StartOfLine => (
158                movement::line_beginning(map, point, false),
159                SelectionGoal::None,
160            ),
161            EndOfLine => (
162                map.clip_point(movement::line_end(map, point, false), Bias::Left),
163                SelectionGoal::None,
164            ),
165            StartOfDocument => (start_of_document(map), SelectionGoal::None),
166            EndOfDocument => (end_of_document(map), SelectionGoal::None),
167        }
168    }
169
170    pub fn expand_selection(self, map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
171        use Motion::*;
172        match self {
173            Up => {
174                let (start, _) = Up.move_point(map, selection.start, SelectionGoal::None);
175                // Cursor at top of file. Return early rather
176                if start == selection.start {
177                    return;
178                }
179                let (start, _) = StartOfLine.move_point(map, start, SelectionGoal::None);
180                let (end, _) = EndOfLine.move_point(map, selection.end, SelectionGoal::None);
181                selection.start = start;
182                selection.end = end;
183                // TODO: Make sure selection goal is correct here
184                selection.goal = SelectionGoal::None;
185            }
186            Down => {
187                let (end, _) = Down.move_point(map, selection.end, SelectionGoal::None);
188                // Cursor at top of file. Return early rather
189                if end == selection.start {
190                    return;
191                }
192                let (start, _) = StartOfLine.move_point(map, selection.start, SelectionGoal::None);
193                let (end, _) = EndOfLine.move_point(map, end, SelectionGoal::None);
194                selection.start = start;
195                selection.end = end;
196                // TODO: Make sure selection goal is correct here
197                selection.goal = SelectionGoal::None;
198            }
199            NextWordEnd { ignore_punctuation } => {
200                selection.set_head(
201                    next_word_end(map, selection.head(), ignore_punctuation, false),
202                    SelectionGoal::None,
203                );
204            }
205            _ => {
206                let (head, goal) = self.move_point(map, selection.head(), selection.goal);
207                selection.set_head(head, goal);
208            }
209        }
210    }
211}
212
213fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
214    *point.column_mut() = point.column().saturating_sub(1);
215    map.clip_point(point, Bias::Left)
216}
217
218fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
219    *point.column_mut() += 1;
220    map.clip_point(point, Bias::Right)
221}
222
223fn next_word_start(
224    map: &DisplaySnapshot,
225    point: DisplayPoint,
226    ignore_punctuation: bool,
227    stop_at_newline: bool,
228) -> DisplayPoint {
229    let mut crossed_newline = false;
230    movement::find_boundary(map, point, |left, right| {
231        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
232        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
233        let at_newline = right == '\n';
234
235        let found = (left_kind != right_kind && !right.is_whitespace())
236            || (at_newline && (crossed_newline || stop_at_newline))
237            || (at_newline && left == '\n'); // Prevents skipping repeated empty lines
238
239        if at_newline {
240            crossed_newline = true;
241        }
242        found
243    })
244}
245
246fn next_word_end(
247    map: &DisplaySnapshot,
248    mut point: DisplayPoint,
249    ignore_punctuation: bool,
250    before_end_character: bool,
251) -> DisplayPoint {
252    *point.column_mut() += 1;
253    point = movement::find_boundary(map, point, |left, right| {
254        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
255        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
256
257        left_kind != right_kind && !left.is_whitespace()
258    });
259    // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
260    // we have backtraced already
261    if before_end_character
262        && !map
263            .chars_at(point)
264            .skip(1)
265            .next()
266            .map(|c| c == '\n')
267            .unwrap_or(true)
268    {
269        *point.column_mut() = point.column().saturating_sub(1);
270    }
271    map.clip_point(point, Bias::Left)
272}
273
274fn previous_word_start(
275    map: &DisplaySnapshot,
276    mut point: DisplayPoint,
277    ignore_punctuation: bool,
278) -> DisplayPoint {
279    // This works even though find_preceding_boundary is called for every character in the line containing
280    // cursor because the newline is checked only once.
281    point = movement::find_preceding_boundary(map, point, |left, right| {
282        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
283        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
284
285        (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
286    });
287    point
288}
289
290fn start_of_document(map: &DisplaySnapshot) -> DisplayPoint {
291    0usize.to_display_point(map)
292}
293
294fn end_of_document(map: &DisplaySnapshot) -> DisplayPoint {
295    map.clip_point(map.max_point(), Bias::Left)
296}