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