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