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 dbg!(&motion);
121 if let Some(Operator::Namespace(_))
122 | Some(Operator::FindForward { .. })
123 | Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator()
124 {
125 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
126 }
127
128 let times = Vim::update(cx, |vim, cx| vim.pop_number_operator(cx));
129 let operator = Vim::read(cx).active_operator();
130 match Vim::read(cx).state.mode {
131 Mode::Normal => normal_motion(motion, operator, times, cx),
132 Mode::Visual { .. } => visual_motion(motion, times, cx),
133 Mode::Insert => {
134 // Shouldn't execute a motion in insert mode. Ignoring
135 }
136 }
137 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
138}
139
140// Motion handling is specified here:
141// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
142impl Motion {
143 pub fn linewise(&self) -> bool {
144 use Motion::*;
145 match self {
146 Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
147 EndOfLine
148 | NextWordEnd { .. }
149 | Matching
150 | FindForward { .. }
151 | Left
152 | Backspace
153 | Right
154 | StartOfLine
155 | NextWordStart { .. }
156 | PreviousWordStart { .. }
157 | FirstNonWhitespace
158 | FindBackward { .. } => false,
159 }
160 }
161
162 pub fn infallible(&self) -> bool {
163 use Motion::*;
164 match self {
165 StartOfDocument | EndOfDocument | CurrentLine => true,
166 Down
167 | Up
168 | EndOfLine
169 | NextWordEnd { .. }
170 | Matching
171 | FindForward { .. }
172 | Left
173 | Backspace
174 | Right
175 | StartOfLine
176 | NextWordStart { .. }
177 | PreviousWordStart { .. }
178 | FirstNonWhitespace
179 | FindBackward { .. }
180 | NextLineStart => false,
181 }
182 }
183
184 pub fn inclusive(&self) -> bool {
185 use Motion::*;
186 match self {
187 Down
188 | Up
189 | StartOfDocument
190 | EndOfDocument
191 | CurrentLine
192 | EndOfLine
193 | NextWordEnd { .. }
194 | Matching
195 | FindForward { .. }
196 | NextLineStart => true,
197 Left
198 | Backspace
199 | Right
200 | StartOfLine
201 | NextWordStart { .. }
202 | PreviousWordStart { .. }
203 | FirstNonWhitespace
204 | FindBackward { .. } => false,
205 }
206 }
207
208 pub fn move_point(
209 &self,
210 map: &DisplaySnapshot,
211 point: DisplayPoint,
212 goal: SelectionGoal,
213 times: usize,
214 ) -> Option<(DisplayPoint, SelectionGoal)> {
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 => (end_of_document(map, point, times), SelectionGoal::None),
241 Matching => (matching(map, point), SelectionGoal::None),
242 FindForward { before, text } => (
243 find_forward(map, point, *before, text.clone(), times),
244 SelectionGoal::None,
245 ),
246 FindBackward { after, text } => (
247 find_backward(map, point, *after, text.clone(), times),
248 SelectionGoal::None,
249 ),
250 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
251 };
252
253 (new_point != point || infallible).then_some((new_point, goal))
254 }
255
256 // Expands a selection using self motion for an operator
257 pub fn expand_selection(
258 &self,
259 map: &DisplaySnapshot,
260 selection: &mut Selection<DisplayPoint>,
261 times: usize,
262 expand_to_surrounding_newline: bool,
263 ) -> bool {
264 if let Some((new_head, goal)) =
265 self.move_point(map, selection.head(), selection.goal, times)
266 {
267 selection.set_head(new_head, goal);
268
269 if self.linewise() {
270 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
271
272 if expand_to_surrounding_newline {
273 if selection.end.row() < map.max_point().row() {
274 *selection.end.row_mut() += 1;
275 *selection.end.column_mut() = 0;
276 selection.end = map.clip_point(selection.end, Bias::Right);
277 // Don't reset the end here
278 return true;
279 } else if selection.start.row() > 0 {
280 *selection.start.row_mut() -= 1;
281 *selection.start.column_mut() = map.line_len(selection.start.row());
282 selection.start = map.clip_point(selection.start, Bias::Left);
283 }
284 }
285
286 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
287 } else {
288 // If the motion is exclusive and the end of the motion is in column 1, the
289 // end of the motion is moved to the end of the previous line and the motion
290 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
291 // but "d}" will not include that line.
292 let mut inclusive = self.inclusive();
293 if !inclusive
294 && self != &Motion::Backspace
295 && selection.end.row() > selection.start.row()
296 && selection.end.column() == 0
297 {
298 inclusive = true;
299 *selection.end.row_mut() -= 1;
300 *selection.end.column_mut() = 0;
301 selection.end = map.clip_point(
302 map.next_line_boundary(selection.end.to_point(map)).1,
303 Bias::Left,
304 );
305 }
306
307 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
308 *selection.end.column_mut() += 1;
309 }
310 }
311 true
312 } else {
313 false
314 }
315 }
316}
317
318fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
319 for _ in 0..times {
320 *point.column_mut() = point.column().saturating_sub(1);
321 point = map.clip_point(point, Bias::Left);
322 if point.column() == 0 {
323 break;
324 }
325 }
326 point
327}
328
329fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
330 for _ in 0..times {
331 point = movement::left(map, point);
332 }
333 point
334}
335
336fn down(
337 map: &DisplaySnapshot,
338 mut point: DisplayPoint,
339 mut goal: SelectionGoal,
340 times: usize,
341) -> (DisplayPoint, SelectionGoal) {
342 for _ in 0..times {
343 (point, goal) = movement::down(map, point, goal, true);
344 }
345 (point, goal)
346}
347
348fn up(
349 map: &DisplaySnapshot,
350 mut point: DisplayPoint,
351 mut goal: SelectionGoal,
352 times: usize,
353) -> (DisplayPoint, SelectionGoal) {
354 for _ in 0..times {
355 (point, goal) = movement::up(map, point, goal, true);
356 }
357 (point, goal)
358}
359
360pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
361 for _ in 0..times {
362 let mut new_point = point;
363 *new_point.column_mut() += 1;
364 let new_point = map.clip_point(new_point, Bias::Right);
365 if point == new_point {
366 break;
367 }
368 point = new_point;
369 }
370 point
371}
372
373pub(crate) fn next_word_start(
374 map: &DisplaySnapshot,
375 mut point: DisplayPoint,
376 ignore_punctuation: bool,
377 times: usize,
378) -> DisplayPoint {
379 for _ in 0..times {
380 let mut crossed_newline = false;
381 point = movement::find_boundary(map, point, |left, right| {
382 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
383 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
384 let at_newline = right == '\n';
385
386 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
387 || at_newline && crossed_newline
388 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
389
390 crossed_newline |= at_newline;
391 found
392 })
393 }
394 point
395}
396
397fn next_word_end(
398 map: &DisplaySnapshot,
399 mut point: DisplayPoint,
400 ignore_punctuation: bool,
401 times: usize,
402) -> DisplayPoint {
403 for _ in 0..times {
404 *point.column_mut() += 1;
405 point = movement::find_boundary(map, point, |left, right| {
406 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
407 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
408
409 left_kind != right_kind && left_kind != CharKind::Whitespace
410 });
411
412 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
413 // we have backtracked already
414 if !map
415 .chars_at(point)
416 .nth(1)
417 .map(|(c, _)| c == '\n')
418 .unwrap_or(true)
419 {
420 *point.column_mut() = point.column().saturating_sub(1);
421 }
422 point = map.clip_point(point, Bias::Left);
423 }
424 point
425}
426
427fn previous_word_start(
428 map: &DisplaySnapshot,
429 mut point: DisplayPoint,
430 ignore_punctuation: bool,
431 times: usize,
432) -> DisplayPoint {
433 for _ in 0..times {
434 // This works even though find_preceding_boundary is called for every character in the line containing
435 // cursor because the newline is checked only once.
436 point = movement::find_preceding_boundary(map, point, |left, right| {
437 let left_kind = char_kind(left).coerce_punctuation(ignore_punctuation);
438 let right_kind = char_kind(right).coerce_punctuation(ignore_punctuation);
439
440 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
441 });
442 }
443 point
444}
445
446fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint {
447 let mut last_point = DisplayPoint::new(from.row(), 0);
448 for (ch, point) in map.chars_at(last_point) {
449 if ch == '\n' {
450 return from;
451 }
452
453 last_point = point;
454
455 if char_kind(ch) != CharKind::Whitespace {
456 break;
457 }
458 }
459
460 map.clip_point(last_point, Bias::Left)
461}
462
463fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
464 map.prev_line_boundary(point.to_point(map)).1
465}
466
467fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
468 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
469}
470
471fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
472 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
473 *new_point.column_mut() = point.column();
474 map.clip_point(new_point, Bias::Left)
475}
476
477fn end_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
478 let mut new_point = if line == 1 {
479 map.max_point()
480 } else {
481 Point::new((line - 1) as u32, 0).to_display_point(map)
482 };
483 *new_point.column_mut() = point.column();
484 map.clip_point(new_point, Bias::Left)
485}
486
487fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
488 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
489 let point = display_point.to_point(map);
490 let offset = point.to_offset(&map.buffer_snapshot);
491
492 // Ensure the range is contained by the current line.
493 let mut line_end = map.next_line_boundary(point).0;
494 if line_end == point {
495 line_end = map.max_point().to_point(map);
496 }
497 line_end.column = line_end.column.saturating_sub(1);
498
499 let line_range = map.prev_line_boundary(point).0..line_end;
500 let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone());
501 if let Some(ranges) = ranges {
502 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
503 ..line_range.end.to_offset(&map.buffer_snapshot);
504 let mut closest_pair_destination = None;
505 let mut closest_distance = usize::MAX;
506
507 for (open_range, close_range) in ranges {
508 if open_range.start >= offset && line_range.contains(&open_range.start) {
509 let distance = open_range.start - offset;
510 if distance < closest_distance {
511 closest_pair_destination = Some(close_range.start);
512 closest_distance = distance;
513 continue;
514 }
515 }
516
517 if close_range.start >= offset && line_range.contains(&close_range.start) {
518 let distance = close_range.start - offset;
519 if distance < closest_distance {
520 closest_pair_destination = Some(open_range.start);
521 closest_distance = distance;
522 continue;
523 }
524 }
525
526 continue;
527 }
528
529 closest_pair_destination
530 .map(|destination| destination.to_display_point(map))
531 .unwrap_or(display_point)
532 } else {
533 display_point
534 }
535}
536
537fn find_forward(
538 map: &DisplaySnapshot,
539 from: DisplayPoint,
540 before: bool,
541 target: Arc<str>,
542 times: usize,
543) -> DisplayPoint {
544 map.find_while(from, target.as_ref(), |ch, _| ch != '\n')
545 .skip_while(|found_at| found_at == &from)
546 .nth(times - 1)
547 .map(|mut found| {
548 if before {
549 *found.column_mut() -= 1;
550 found = map.clip_point(found, Bias::Right);
551 found
552 } else {
553 found
554 }
555 })
556 .unwrap_or(from)
557}
558
559fn find_backward(
560 map: &DisplaySnapshot,
561 from: DisplayPoint,
562 after: bool,
563 target: Arc<str>,
564 times: usize,
565) -> DisplayPoint {
566 map.reverse_find_while(from, target.as_ref(), |ch, _| ch != '\n')
567 .skip_while(|found_at| found_at == &from)
568 .nth(times - 1)
569 .map(|mut found| {
570 if after {
571 *found.column_mut() += 1;
572 found = map.clip_point(found, Bias::Left);
573 found
574 } else {
575 found
576 }
577 })
578 .unwrap_or(from)
579}
580
581fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
582 let new_row = (point.row() + times as u32).min(map.max_buffer_row());
583 map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
584}