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::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 => {
126            // Shouldn't execute a motion in insert mode. Ignoring
127        }
128    }
129}
130
131impl Motion {
132    pub fn move_point(
133        self,
134        map: &DisplaySnapshot,
135        point: DisplayPoint,
136        goal: SelectionGoal,
137        block_cursor_positioning: bool,
138    ) -> (DisplayPoint, SelectionGoal) {
139        use Motion::*;
140        match self {
141            Left => (left(map, point), SelectionGoal::None),
142            Down => movement::down(map, point, goal),
143            Up => movement::up(map, point, goal),
144            Right => (right(map, point), SelectionGoal::None),
145            NextWordStart {
146                ignore_punctuation,
147                stop_at_newline,
148            } => (
149                next_word_start(map, point, ignore_punctuation, stop_at_newline),
150                SelectionGoal::None,
151            ),
152            NextWordEnd { ignore_punctuation } => (
153                next_word_end(map, point, ignore_punctuation, block_cursor_positioning),
154                SelectionGoal::None,
155            ),
156            PreviousWordStart { ignore_punctuation } => (
157                previous_word_start(map, point, ignore_punctuation),
158                SelectionGoal::None,
159            ),
160            StartOfLine => (start_of_line(map, point), SelectionGoal::None),
161            EndOfLine => (end_of_line(map, point), SelectionGoal::None),
162            StartOfDocument => (start_of_document(map, point), SelectionGoal::None),
163            EndOfDocument => (end_of_document(map, point), SelectionGoal::None),
164        }
165    }
166
167    pub fn line_wise(self) -> bool {
168        use Motion::*;
169        match self {
170            Down | Up | StartOfDocument | EndOfDocument => true,
171            _ => false,
172        }
173    }
174}
175
176fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
177    *point.column_mut() = point.column().saturating_sub(1);
178    map.clip_point(point, Bias::Left)
179}
180
181fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
182    *point.column_mut() += 1;
183    map.clip_point(point, Bias::Right)
184}
185
186fn next_word_start(
187    map: &DisplaySnapshot,
188    point: DisplayPoint,
189    ignore_punctuation: bool,
190    stop_at_newline: bool,
191) -> DisplayPoint {
192    let mut crossed_newline = false;
193    movement::find_boundary(map, point, |left, right| {
194        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
195        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
196        let at_newline = right == '\n';
197
198        let found = (left_kind != right_kind && !right.is_whitespace())
199            || (at_newline && (crossed_newline || stop_at_newline))
200            || (at_newline && left == '\n'); // Prevents skipping repeated empty lines
201
202        if at_newline {
203            crossed_newline = true;
204        }
205        found
206    })
207}
208
209fn next_word_end(
210    map: &DisplaySnapshot,
211    mut point: DisplayPoint,
212    ignore_punctuation: bool,
213    before_end_character: bool,
214) -> DisplayPoint {
215    *point.column_mut() += 1;
216    point = movement::find_boundary(map, point, |left, right| {
217        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
218        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
219
220        left_kind != right_kind && !left.is_whitespace()
221    });
222    // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
223    // we have backtraced already
224    if before_end_character
225        && !map
226            .chars_at(point)
227            .skip(1)
228            .next()
229            .map(|c| c == '\n')
230            .unwrap_or(true)
231    {
232        *point.column_mut() = point.column().saturating_sub(1);
233    }
234    map.clip_point(point, Bias::Left)
235}
236
237fn previous_word_start(
238    map: &DisplaySnapshot,
239    mut point: DisplayPoint,
240    ignore_punctuation: bool,
241) -> DisplayPoint {
242    // This works even though find_preceding_boundary is called for every character in the line containing
243    // cursor because the newline is checked only once.
244    point = movement::find_preceding_boundary(map, point, |left, right| {
245        let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
246        let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
247
248        (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
249    });
250    point
251}
252
253fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
254    map.prev_line_boundary(point.to_point(map)).1
255}
256
257fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
258    map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
259}
260
261fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
262    let mut new_point = 0usize.to_display_point(map);
263    *new_point.column_mut() = point.column();
264    map.clip_point(new_point, Bias::Left)
265}
266
267fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
268    let mut new_point = map.max_point();
269    *new_point.column_mut() = point.column();
270    map.clip_point(new_point, Bias::Left)
271}