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