1use editor::{
2 char_kind,
3 display_map::{DisplaySnapshot, ToDisplayPoint},
4 movement, Bias, CharKind, DisplayPoint,
5};
6use gpui::{actions, impl_actions, MutableAppContext};
7use language::{Point, 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, PartialEq, Eq)]
19pub enum Motion {
20 Left,
21 Backspace,
22 Down,
23 Up,
24 Right,
25 NextWordStart { ignore_punctuation: bool },
26 NextWordEnd { ignore_punctuation: bool },
27 PreviousWordStart { ignore_punctuation: bool },
28 FirstNonWhitespace,
29 CurrentLine,
30 StartOfLine,
31 EndOfLine,
32 StartOfDocument,
33 EndOfDocument,
34 Matching,
35}
36
37#[derive(Clone, Deserialize, PartialEq)]
38#[serde(rename_all = "camelCase")]
39struct NextWordStart {
40 #[serde(default)]
41 ignore_punctuation: bool,
42}
43
44#[derive(Clone, Deserialize, PartialEq)]
45#[serde(rename_all = "camelCase")]
46struct NextWordEnd {
47 #[serde(default)]
48 ignore_punctuation: bool,
49}
50
51#[derive(Clone, Deserialize, PartialEq)]
52#[serde(rename_all = "camelCase")]
53struct PreviousWordStart {
54 #[serde(default)]
55 ignore_punctuation: bool,
56}
57
58actions!(
59 vim,
60 [
61 Left,
62 Backspace,
63 Down,
64 Up,
65 Right,
66 FirstNonWhitespace,
67 StartOfLine,
68 EndOfLine,
69 CurrentLine,
70 StartOfDocument,
71 EndOfDocument,
72 Matching,
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, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
80 cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx));
81 cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx));
82 cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
83 cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| {
84 motion(Motion::FirstNonWhitespace, cx)
85 });
86 cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
87 cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
88 cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
89 cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
90 motion(Motion::StartOfDocument, cx)
91 });
92 cx.add_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| motion(Motion::EndOfDocument, cx));
93 cx.add_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
94
95 cx.add_action(
96 |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
97 motion(Motion::NextWordStart { ignore_punctuation }, cx)
98 },
99 );
100 cx.add_action(
101 |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
102 motion(Motion::NextWordEnd { ignore_punctuation }, cx)
103 },
104 );
105 cx.add_action(
106 |_: &mut Workspace,
107 &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
108 cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
109 );
110}
111
112pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
113 if let Some(Operator::Namespace(_)) = Vim::read(cx).active_operator() {
114 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
115 }
116
117 let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
118 let operator = Vim::read(cx).active_operator();
119 match Vim::read(cx).state.mode {
120 Mode::Normal => normal_motion(motion, operator, times, cx),
121 Mode::Visual { .. } => visual_motion(motion, times, cx),
122 Mode::Insert => {
123 // Shouldn't execute a motion in insert mode. Ignoring
124 }
125 }
126 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
127}
128
129// Motion handling is specified here:
130// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
131impl Motion {
132 pub fn linewise(self) -> bool {
133 use Motion::*;
134 matches!(
135 self,
136 Down | Up | StartOfDocument | EndOfDocument | CurrentLine
137 )
138 }
139
140 pub fn infallible(self) -> bool {
141 use Motion::*;
142 matches!(self, StartOfDocument | CurrentLine | EndOfDocument)
143 }
144
145 pub fn inclusive(self) -> bool {
146 use Motion::*;
147 match self {
148 Down
149 | Up
150 | StartOfDocument
151 | EndOfDocument
152 | CurrentLine
153 | EndOfLine
154 | NextWordEnd { .. }
155 | Matching => true,
156 Left
157 | Backspace
158 | Right
159 | StartOfLine
160 | NextWordStart { .. }
161 | PreviousWordStart { .. }
162 | FirstNonWhitespace => false,
163 }
164 }
165
166 pub fn move_point(
167 self,
168 map: &DisplaySnapshot,
169 point: DisplayPoint,
170 goal: SelectionGoal,
171 times: usize,
172 ) -> Option<(DisplayPoint, SelectionGoal)> {
173 use Motion::*;
174 let (new_point, goal) = match self {
175 Left => (left(map, point, times), SelectionGoal::None),
176 Backspace => (backspace(map, point, times), SelectionGoal::None),
177 Down => down(map, point, goal, times),
178 Up => up(map, point, goal, times),
179 Right => (right(map, point, times), SelectionGoal::None),
180 NextWordStart { ignore_punctuation } => (
181 next_word_start(map, point, ignore_punctuation, times),
182 SelectionGoal::None,
183 ),
184 NextWordEnd { ignore_punctuation } => (
185 next_word_end(map, point, ignore_punctuation, times),
186 SelectionGoal::None,
187 ),
188 PreviousWordStart { ignore_punctuation } => (
189 previous_word_start(map, point, ignore_punctuation, times),
190 SelectionGoal::None,
191 ),
192 FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
193 StartOfLine => (start_of_line(map, point), SelectionGoal::None),
194 EndOfLine => (end_of_line(map, point), SelectionGoal::None),
195 CurrentLine => (end_of_line(map, point), SelectionGoal::None),
196 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
197 EndOfDocument => (end_of_document(map, point, times), SelectionGoal::None),
198 Matching => (matching(map, point), SelectionGoal::None),
199 };
200
201 (new_point != point || self.infallible()).then_some((new_point, goal))
202 }
203
204 // Expands a selection using self motion for an operator
205 pub fn expand_selection(
206 self,
207 map: &DisplaySnapshot,
208 selection: &mut Selection<DisplayPoint>,
209 times: usize,
210 expand_to_surrounding_newline: bool,
211 ) -> bool {
212 if let Some((new_head, goal)) =
213 self.move_point(map, selection.head(), selection.goal, times)
214 {
215 selection.set_head(new_head, goal);
216
217 if self.linewise() {
218 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
219
220 if expand_to_surrounding_newline {
221 if selection.end.row() < map.max_point().row() {
222 *selection.end.row_mut() += 1;
223 *selection.end.column_mut() = 0;
224 selection.end = map.clip_point(selection.end, Bias::Right);
225 // Don't reset the end here
226 return true;
227 } else if selection.start.row() > 0 {
228 *selection.start.row_mut() -= 1;
229 *selection.start.column_mut() = map.line_len(selection.start.row());
230 selection.start = map.clip_point(selection.start, Bias::Left);
231 }
232 }
233
234 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
235 } else {
236 // If the motion is exclusive and the end of the motion is in column 1, the
237 // end of the motion is moved to the end of the previous line and the motion
238 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
239 // but "d}" will not include that line.
240 let mut inclusive = self.inclusive();
241 if !inclusive
242 && self != Motion::Backspace
243 && selection.end.row() > selection.start.row()
244 && selection.end.column() == 0
245 {
246 inclusive = true;
247 *selection.end.row_mut() -= 1;
248 *selection.end.column_mut() = 0;
249 selection.end = map.clip_point(
250 map.next_line_boundary(selection.end.to_point(map)).1,
251 Bias::Left,
252 );
253 }
254
255 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
256 *selection.end.column_mut() += 1;
257 }
258 }
259 true
260 } else {
261 false
262 }
263 }
264}
265
266fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
267 for _ in 0..times {
268 *point.column_mut() = point.column().saturating_sub(1);
269 point = map.clip_point(point, Bias::Left);
270 if point.column() == 0 {
271 break;
272 }
273 }
274 point
275}
276
277fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
278 for _ in 0..times {
279 point = movement::left(map, point);
280 }
281 point
282}
283
284fn down(
285 map: &DisplaySnapshot,
286 mut point: DisplayPoint,
287 mut goal: SelectionGoal,
288 times: usize,
289) -> (DisplayPoint, SelectionGoal) {
290 for _ in 0..times {
291 (point, goal) = movement::down(map, point, goal, true);
292 }
293 (point, goal)
294}
295
296fn up(
297 map: &DisplaySnapshot,
298 mut point: DisplayPoint,
299 mut goal: SelectionGoal,
300 times: usize,
301) -> (DisplayPoint, SelectionGoal) {
302 for _ in 0..times {
303 (point, goal) = movement::up(map, point, goal, true);
304 }
305 (point, goal)
306}
307
308pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
309 for _ in 0..times {
310 let mut new_point = point;
311 *new_point.column_mut() += 1;
312 let new_point = map.clip_point(new_point, Bias::Right);
313 if point == new_point {
314 break;
315 }
316 point = new_point;
317 }
318 point
319}
320
321pub(crate) fn next_word_start(
322 map: &DisplaySnapshot,
323 mut point: DisplayPoint,
324 ignore_punctuation: bool,
325 times: usize,
326) -> DisplayPoint {
327 for _ in 0..times {
328 let mut crossed_newline = false;
329 point = movement::find_boundary(map, point, |left, right| {
330 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
331 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
332 let at_newline = right == '\n';
333
334 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
335 || at_newline && crossed_newline
336 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
337
338 crossed_newline |= at_newline;
339 found
340 })
341 }
342 point
343}
344
345fn next_word_end(
346 map: &DisplaySnapshot,
347 mut point: DisplayPoint,
348 ignore_punctuation: bool,
349 times: usize,
350) -> DisplayPoint {
351 for _ in 0..times {
352 *point.column_mut() += 1;
353 point = movement::find_boundary(map, point, |left, right| {
354 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
355 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
356
357 left_kind != right_kind && left_kind != CharKind::Whitespace
358 });
359
360 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
361 // we have backtracked already
362 if !map
363 .chars_at(point)
364 .nth(1)
365 .map(|(c, _)| c == '\n')
366 .unwrap_or(true)
367 {
368 *point.column_mut() = point.column().saturating_sub(1);
369 }
370 point = map.clip_point(point, Bias::Left);
371 }
372 point
373}
374
375fn previous_word_start(
376 map: &DisplaySnapshot,
377 mut point: DisplayPoint,
378 ignore_punctuation: bool,
379 times: usize,
380) -> DisplayPoint {
381 for _ in 0..times {
382 // This works even though find_preceding_boundary is called for every character in the line containing
383 // cursor because the newline is checked only once.
384 point = movement::find_preceding_boundary(map, point, |left, right| {
385 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
386 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
387
388 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
389 });
390 }
391 point
392}
393
394fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
395 let mut last_point = DisplayPoint::new(from.row(), 0);
396 for (ch, point) in map.chars_at(last_point) {
397 if ch == '\n' {
398 return from;
399 }
400
401 last_point = point;
402
403 if char_kind(ch) != CharKind::Whitespace {
404 break;
405 }
406 }
407
408 map.clip_point(last_point, Bias::Left)
409}
410
411fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
412 map.prev_line_boundary(point.to_point(map)).1
413}
414
415fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
416 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
417}
418
419fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
420 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
421 *new_point.column_mut() = point.column();
422 map.clip_point(new_point, Bias::Left)
423}
424
425fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
426 let mut new_point = if line == 1 {
427 map.max_point()
428 } else {
429 Point::new((line - 1) as u32, 0).to_display_point(map)
430 };
431 *new_point.column_mut() = point.column();
432 map.clip_point(new_point, Bias::Left)
433}
434
435fn matching(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
436 let offset = point.to_offset(map, Bias::Left);
437 if let Some((open_range, close_range)) =
438 map.buffer_snapshot.enclosing_bracket_ranges(offset..offset)
439 {
440 if open_range.contains(&offset) {
441 close_range.start.to_display_point(map)
442 } else {
443 open_range.start.to_display_point(map)
444 }
445 } else {
446 point
447 }
448}