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