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