1use editor::{
2 display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint},
3 movement::{self, find_boundary, find_preceding_boundary, FindRange, TextLayoutDetails},
4 Bias, DisplayPoint, ToOffset,
5};
6use gpui::{actions, impl_actions, px, ViewContext, WindowContext};
7use language::{char_kind, CharKind, Point, Selection, SelectionGoal};
8use serde::Deserialize;
9use workspace::Workspace;
10
11use crate::{
12 normal::normal_motion,
13 state::{Mode, Operator},
14 utils::coerce_punctuation,
15 visual::visual_motion,
16 Vim,
17};
18
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum Motion {
21 Left,
22 Backspace,
23 Down { display_lines: bool },
24 Up { display_lines: bool },
25 Right,
26 NextWordStart { ignore_punctuation: bool },
27 NextWordEnd { ignore_punctuation: bool },
28 PreviousWordStart { ignore_punctuation: bool },
29 FirstNonWhitespace { display_lines: bool },
30 CurrentLine,
31 StartOfLine { display_lines: bool },
32 EndOfLine { display_lines: bool },
33 StartOfParagraph,
34 EndOfParagraph,
35 StartOfDocument,
36 EndOfDocument,
37 Matching,
38 FindForward { before: bool, char: char },
39 FindBackward { after: bool, char: char },
40 NextLineStart,
41 StartOfLineDownward,
42 EndOfLineDownward,
43 GoToColumn,
44 WindowTop,
45 WindowMiddle,
46 WindowBottom,
47}
48
49#[derive(Clone, Deserialize, PartialEq)]
50#[serde(rename_all = "camelCase")]
51struct NextWordStart {
52 #[serde(default)]
53 ignore_punctuation: bool,
54}
55
56#[derive(Clone, Deserialize, PartialEq)]
57#[serde(rename_all = "camelCase")]
58struct NextWordEnd {
59 #[serde(default)]
60 ignore_punctuation: bool,
61}
62
63#[derive(Clone, Deserialize, PartialEq)]
64#[serde(rename_all = "camelCase")]
65struct PreviousWordStart {
66 #[serde(default)]
67 ignore_punctuation: bool,
68}
69
70#[derive(Clone, Deserialize, PartialEq)]
71#[serde(rename_all = "camelCase")]
72pub(crate) struct Up {
73 #[serde(default)]
74 pub(crate) display_lines: bool,
75}
76
77#[derive(Clone, Deserialize, PartialEq)]
78#[serde(rename_all = "camelCase")]
79pub(crate) struct Down {
80 #[serde(default)]
81 pub(crate) display_lines: bool,
82}
83
84#[derive(Clone, Deserialize, PartialEq)]
85#[serde(rename_all = "camelCase")]
86struct FirstNonWhitespace {
87 #[serde(default)]
88 display_lines: bool,
89}
90
91#[derive(Clone, Deserialize, PartialEq)]
92#[serde(rename_all = "camelCase")]
93struct EndOfLine {
94 #[serde(default)]
95 display_lines: bool,
96}
97
98#[derive(Clone, Deserialize, PartialEq)]
99#[serde(rename_all = "camelCase")]
100pub struct StartOfLine {
101 #[serde(default)]
102 pub(crate) display_lines: bool,
103}
104
105#[derive(Clone, Deserialize, PartialEq)]
106struct RepeatFind {
107 #[serde(default)]
108 backwards: bool,
109}
110
111impl_actions!(
112 vim,
113 [
114 RepeatFind,
115 StartOfLine,
116 EndOfLine,
117 FirstNonWhitespace,
118 Down,
119 Up,
120 PreviousWordStart,
121 NextWordEnd,
122 NextWordStart
123 ]
124);
125
126actions!(
127 vim,
128 [
129 Left,
130 Backspace,
131 Right,
132 CurrentLine,
133 StartOfParagraph,
134 EndOfParagraph,
135 StartOfDocument,
136 EndOfDocument,
137 Matching,
138 NextLineStart,
139 StartOfLineDownward,
140 EndOfLineDownward,
141 GoToColumn,
142 WindowTop,
143 WindowMiddle,
144 WindowBottom,
145 ]
146);
147
148pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
149 workspace.register_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx));
150 workspace
151 .register_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx));
152 workspace.register_action(|_: &mut Workspace, action: &Down, cx: _| {
153 motion(
154 Motion::Down {
155 display_lines: action.display_lines,
156 },
157 cx,
158 )
159 });
160 workspace.register_action(|_: &mut Workspace, action: &Up, cx: _| {
161 motion(
162 Motion::Up {
163 display_lines: action.display_lines,
164 },
165 cx,
166 )
167 });
168 workspace.register_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx));
169 workspace.register_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| {
170 motion(
171 Motion::FirstNonWhitespace {
172 display_lines: action.display_lines,
173 },
174 cx,
175 )
176 });
177 workspace.register_action(|_: &mut Workspace, action: &StartOfLine, cx: _| {
178 motion(
179 Motion::StartOfLine {
180 display_lines: action.display_lines,
181 },
182 cx,
183 )
184 });
185 workspace.register_action(|_: &mut Workspace, action: &EndOfLine, cx: _| {
186 motion(
187 Motion::EndOfLine {
188 display_lines: action.display_lines,
189 },
190 cx,
191 )
192 });
193 workspace.register_action(|_: &mut Workspace, _: &CurrentLine, cx: _| {
194 motion(Motion::CurrentLine, cx)
195 });
196 workspace.register_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
197 motion(Motion::StartOfParagraph, cx)
198 });
199 workspace.register_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
200 motion(Motion::EndOfParagraph, cx)
201 });
202 workspace.register_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
203 motion(Motion::StartOfDocument, cx)
204 });
205 workspace.register_action(|_: &mut Workspace, _: &EndOfDocument, cx: _| {
206 motion(Motion::EndOfDocument, cx)
207 });
208 workspace
209 .register_action(|_: &mut Workspace, _: &Matching, cx: _| motion(Motion::Matching, cx));
210
211 workspace.register_action(
212 |_: &mut Workspace, &NextWordStart { ignore_punctuation }: &NextWordStart, cx: _| {
213 motion(Motion::NextWordStart { ignore_punctuation }, cx)
214 },
215 );
216 workspace.register_action(
217 |_: &mut Workspace, &NextWordEnd { ignore_punctuation }: &NextWordEnd, cx: _| {
218 motion(Motion::NextWordEnd { ignore_punctuation }, cx)
219 },
220 );
221 workspace.register_action(
222 |_: &mut Workspace,
223 &PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
224 cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
225 );
226 workspace.register_action(|_: &mut Workspace, &NextLineStart, cx: _| {
227 motion(Motion::NextLineStart, cx)
228 });
229 workspace.register_action(|_: &mut Workspace, &StartOfLineDownward, cx: _| {
230 motion(Motion::StartOfLineDownward, cx)
231 });
232 workspace.register_action(|_: &mut Workspace, &EndOfLineDownward, cx: _| {
233 motion(Motion::EndOfLineDownward, cx)
234 });
235 workspace
236 .register_action(|_: &mut Workspace, &GoToColumn, cx: _| motion(Motion::GoToColumn, cx));
237 workspace.register_action(|_: &mut Workspace, action: &RepeatFind, cx: _| {
238 repeat_motion(action.backwards, cx)
239 });
240 workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx));
241 workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| {
242 motion(Motion::WindowMiddle, cx)
243 });
244 workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| {
245 motion(Motion::WindowBottom, cx)
246 });
247}
248
249pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) {
250 if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) =
251 Vim::read(cx).active_operator()
252 {
253 Vim::update(cx, |vim, cx| vim.pop_operator(cx));
254 }
255
256 let count = Vim::update(cx, |vim, cx| vim.take_count(cx));
257 let operator = Vim::read(cx).active_operator();
258 match Vim::read(cx).state().mode {
259 Mode::Normal => normal_motion(motion, operator, count, cx),
260 Mode::Visual | Mode::VisualLine | Mode::VisualBlock => visual_motion(motion, count, cx),
261 Mode::Insert => {
262 // Shouldn't execute a motion in insert mode. Ignoring
263 }
264 }
265 Vim::update(cx, |vim, cx| vim.clear_operator(cx));
266}
267
268fn repeat_motion(backwards: bool, cx: &mut WindowContext) {
269 let find = match Vim::read(cx).workspace_state.last_find.clone() {
270 Some(Motion::FindForward { before, char }) => {
271 if backwards {
272 Motion::FindBackward {
273 after: before,
274 char,
275 }
276 } else {
277 Motion::FindForward { before, char }
278 }
279 }
280
281 Some(Motion::FindBackward { after, char }) => {
282 if backwards {
283 Motion::FindForward {
284 before: after,
285 char,
286 }
287 } else {
288 Motion::FindBackward { after, char }
289 }
290 }
291 _ => return,
292 };
293
294 motion(find, cx)
295}
296
297// Motion handling is specified here:
298// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt
299impl Motion {
300 pub fn linewise(&self) -> bool {
301 use Motion::*;
302 match self {
303 Down { .. }
304 | Up { .. }
305 | StartOfDocument
306 | EndOfDocument
307 | CurrentLine
308 | NextLineStart
309 | StartOfLineDownward
310 | StartOfParagraph
311 | WindowTop
312 | WindowMiddle
313 | WindowBottom
314 | EndOfParagraph => true,
315 EndOfLine { .. }
316 | NextWordEnd { .. }
317 | Matching
318 | FindForward { .. }
319 | Left
320 | Backspace
321 | Right
322 | StartOfLine { .. }
323 | EndOfLineDownward
324 | GoToColumn
325 | NextWordStart { .. }
326 | PreviousWordStart { .. }
327 | FirstNonWhitespace { .. }
328 | FindBackward { .. } => false,
329 }
330 }
331
332 pub fn infallible(&self) -> bool {
333 use Motion::*;
334 match self {
335 StartOfDocument | EndOfDocument | CurrentLine => true,
336 Down { .. }
337 | Up { .. }
338 | EndOfLine { .. }
339 | NextWordEnd { .. }
340 | Matching
341 | FindForward { .. }
342 | Left
343 | Backspace
344 | Right
345 | StartOfLine { .. }
346 | StartOfParagraph
347 | EndOfParagraph
348 | StartOfLineDownward
349 | EndOfLineDownward
350 | GoToColumn
351 | NextWordStart { .. }
352 | PreviousWordStart { .. }
353 | FirstNonWhitespace { .. }
354 | FindBackward { .. }
355 | WindowTop
356 | WindowMiddle
357 | WindowBottom
358 | NextLineStart => false,
359 }
360 }
361
362 pub fn inclusive(&self) -> bool {
363 use Motion::*;
364 match self {
365 Down { .. }
366 | Up { .. }
367 | StartOfDocument
368 | EndOfDocument
369 | CurrentLine
370 | EndOfLine { .. }
371 | EndOfLineDownward
372 | NextWordEnd { .. }
373 | Matching
374 | FindForward { .. }
375 | WindowTop
376 | WindowMiddle
377 | WindowBottom
378 | NextLineStart => true,
379 Left
380 | Backspace
381 | Right
382 | StartOfLine { .. }
383 | StartOfLineDownward
384 | StartOfParagraph
385 | EndOfParagraph
386 | GoToColumn
387 | NextWordStart { .. }
388 | PreviousWordStart { .. }
389 | FirstNonWhitespace { .. }
390 | FindBackward { .. } => false,
391 }
392 }
393
394 pub fn move_point(
395 &self,
396 map: &DisplaySnapshot,
397 point: DisplayPoint,
398 goal: SelectionGoal,
399 maybe_times: Option<usize>,
400 text_layout_details: &TextLayoutDetails,
401 ) -> Option<(DisplayPoint, SelectionGoal)> {
402 let times = maybe_times.unwrap_or(1);
403 use Motion::*;
404 let infallible = self.infallible();
405 let (new_point, goal) = match self {
406 Left => (left(map, point, times), SelectionGoal::None),
407 Backspace => (backspace(map, point, times), SelectionGoal::None),
408 Down {
409 display_lines: false,
410 } => up_down_buffer_rows(map, point, goal, times as isize, &text_layout_details),
411 Down {
412 display_lines: true,
413 } => down_display(map, point, goal, times, &text_layout_details),
414 Up {
415 display_lines: false,
416 } => up_down_buffer_rows(map, point, goal, 0 - times as isize, &text_layout_details),
417 Up {
418 display_lines: true,
419 } => up_display(map, point, goal, times, &text_layout_details),
420 Right => (right(map, point, times), SelectionGoal::None),
421 NextWordStart { ignore_punctuation } => (
422 next_word_start(map, point, *ignore_punctuation, times),
423 SelectionGoal::None,
424 ),
425 NextWordEnd { ignore_punctuation } => (
426 next_word_end(map, point, *ignore_punctuation, times),
427 SelectionGoal::None,
428 ),
429 PreviousWordStart { ignore_punctuation } => (
430 previous_word_start(map, point, *ignore_punctuation, times),
431 SelectionGoal::None,
432 ),
433 FirstNonWhitespace { display_lines } => (
434 first_non_whitespace(map, *display_lines, point),
435 SelectionGoal::None,
436 ),
437 StartOfLine { display_lines } => (
438 start_of_line(map, *display_lines, point),
439 SelectionGoal::None,
440 ),
441 EndOfLine { display_lines } => {
442 (end_of_line(map, *display_lines, point), SelectionGoal::None)
443 }
444 StartOfParagraph => (
445 movement::start_of_paragraph(map, point, times),
446 SelectionGoal::None,
447 ),
448 EndOfParagraph => (
449 map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
450 SelectionGoal::None,
451 ),
452 CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
453 StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
454 EndOfDocument => (
455 end_of_document(map, point, maybe_times),
456 SelectionGoal::None,
457 ),
458 Matching => (matching(map, point), SelectionGoal::None),
459 FindForward { before, char } => {
460 if let Some(new_point) = find_forward(map, point, *before, *char, times) {
461 return Some((new_point, SelectionGoal::None));
462 } else {
463 return None;
464 }
465 }
466 FindBackward { after, char } => (
467 find_backward(map, point, *after, *char, times),
468 SelectionGoal::None,
469 ),
470 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
471 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
472 EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
473 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
474 WindowTop => window_top(map, point, &text_layout_details),
475 WindowMiddle => window_middle(map, point, &text_layout_details),
476 WindowBottom => window_bottom(map, point, &text_layout_details),
477 };
478
479 (new_point != point || infallible).then_some((new_point, goal))
480 }
481
482 // Expands a selection using self motion for an operator
483 pub fn expand_selection(
484 &self,
485 map: &DisplaySnapshot,
486 selection: &mut Selection<DisplayPoint>,
487 times: Option<usize>,
488 expand_to_surrounding_newline: bool,
489 text_layout_details: &TextLayoutDetails,
490 ) -> bool {
491 if let Some((new_head, goal)) = self.move_point(
492 map,
493 selection.head(),
494 selection.goal,
495 times,
496 &text_layout_details,
497 ) {
498 selection.set_head(new_head, goal);
499
500 if self.linewise() {
501 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
502
503 if expand_to_surrounding_newline {
504 if selection.end.row() < map.max_point().row() {
505 *selection.end.row_mut() += 1;
506 *selection.end.column_mut() = 0;
507 selection.end = map.clip_point(selection.end, Bias::Right);
508 // Don't reset the end here
509 return true;
510 } else if selection.start.row() > 0 {
511 *selection.start.row_mut() -= 1;
512 *selection.start.column_mut() = map.line_len(selection.start.row());
513 selection.start = map.clip_point(selection.start, Bias::Left);
514 }
515 }
516
517 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
518 } else {
519 // Another special case: When using the "w" motion in combination with an
520 // operator and the last word moved over is at the end of a line, the end of
521 // that word becomes the end of the operated text, not the first word in the
522 // next line.
523 if let Motion::NextWordStart {
524 ignore_punctuation: _,
525 } = self
526 {
527 let start_row = selection.start.to_point(&map).row;
528 if selection.end.to_point(&map).row > start_row {
529 selection.end =
530 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
531 .to_display_point(&map)
532 }
533 }
534
535 // If the motion is exclusive and the end of the motion is in column 1, the
536 // end of the motion is moved to the end of the previous line and the motion
537 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
538 // but "d}" will not include that line.
539 let mut inclusive = self.inclusive();
540 if !inclusive
541 && self != &Motion::Backspace
542 && selection.end.row() > selection.start.row()
543 && selection.end.column() == 0
544 {
545 inclusive = true;
546 *selection.end.row_mut() -= 1;
547 *selection.end.column_mut() = 0;
548 selection.end = map.clip_point(
549 map.next_line_boundary(selection.end.to_point(map)).1,
550 Bias::Left,
551 );
552 }
553
554 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
555 *selection.end.column_mut() += 1;
556 }
557 }
558 true
559 } else {
560 false
561 }
562 }
563}
564
565fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
566 for _ in 0..times {
567 point = movement::saturating_left(map, point);
568 if point.column() == 0 {
569 break;
570 }
571 }
572 point
573}
574
575fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
576 for _ in 0..times {
577 point = movement::left(map, point);
578 }
579 point
580}
581
582pub(crate) fn start_of_relative_buffer_row(
583 map: &DisplaySnapshot,
584 point: DisplayPoint,
585 times: isize,
586) -> DisplayPoint {
587 let start = map.display_point_to_fold_point(point, Bias::Left);
588 let target = start.row() as isize + times;
589 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
590
591 map.clip_point(
592 map.fold_point_to_display_point(
593 map.fold_snapshot
594 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
595 ),
596 Bias::Right,
597 )
598}
599
600fn up_down_buffer_rows(
601 map: &DisplaySnapshot,
602 point: DisplayPoint,
603 mut goal: SelectionGoal,
604 times: isize,
605 text_layout_details: &TextLayoutDetails,
606) -> (DisplayPoint, SelectionGoal) {
607 let start = map.display_point_to_fold_point(point, Bias::Left);
608 let begin_folded_line = map.fold_point_to_display_point(
609 map.fold_snapshot
610 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
611 );
612 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
613
614 let (goal_wrap, goal_x) = match goal {
615 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
616 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
617 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
618 _ => {
619 let x = map.x_for_display_point(point, text_layout_details);
620 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
621 (select_nth_wrapped_row, x.0)
622 }
623 };
624
625 let target = start.row() as isize + times;
626 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
627
628 let mut begin_folded_line = map.fold_point_to_display_point(
629 map.fold_snapshot
630 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
631 );
632
633 let mut i = 0;
634 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
635 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
636 if map
637 .display_point_to_fold_point(next_folded_line, Bias::Right)
638 .row()
639 == new_row
640 {
641 i += 1;
642 begin_folded_line = next_folded_line;
643 } else {
644 break;
645 }
646 }
647
648 let new_col = if i == goal_wrap {
649 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
650 } else {
651 map.line_len(begin_folded_line.row())
652 };
653
654 (
655 map.clip_point(
656 DisplayPoint::new(begin_folded_line.row(), new_col),
657 Bias::Left,
658 ),
659 goal,
660 )
661}
662
663fn down_display(
664 map: &DisplaySnapshot,
665 mut point: DisplayPoint,
666 mut goal: SelectionGoal,
667 times: usize,
668 text_layout_details: &TextLayoutDetails,
669) -> (DisplayPoint, SelectionGoal) {
670 for _ in 0..times {
671 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
672 }
673
674 (point, goal)
675}
676
677fn up_display(
678 map: &DisplaySnapshot,
679 mut point: DisplayPoint,
680 mut goal: SelectionGoal,
681 times: usize,
682 text_layout_details: &TextLayoutDetails,
683) -> (DisplayPoint, SelectionGoal) {
684 for _ in 0..times {
685 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
686 }
687
688 (point, goal)
689}
690
691pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
692 for _ in 0..times {
693 let new_point = movement::saturating_right(map, point);
694 if point == new_point {
695 break;
696 }
697 point = new_point;
698 }
699 point
700}
701
702pub(crate) fn next_word_start(
703 map: &DisplaySnapshot,
704 mut point: DisplayPoint,
705 ignore_punctuation: bool,
706 times: usize,
707) -> DisplayPoint {
708 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
709 for _ in 0..times {
710 let mut crossed_newline = false;
711 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
712 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
713 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
714 let at_newline = right == '\n';
715
716 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
717 || at_newline && crossed_newline
718 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
719
720 crossed_newline |= at_newline;
721 found
722 })
723 }
724 point
725}
726
727fn next_word_end(
728 map: &DisplaySnapshot,
729 mut point: DisplayPoint,
730 ignore_punctuation: bool,
731 times: usize,
732) -> DisplayPoint {
733 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
734 for _ in 0..times {
735 if point.column() < map.line_len(point.row()) {
736 *point.column_mut() += 1;
737 } else if point.row() < map.max_buffer_row() {
738 *point.row_mut() += 1;
739 *point.column_mut() = 0;
740 }
741 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
742 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
743 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
744
745 left_kind != right_kind && left_kind != CharKind::Whitespace
746 });
747
748 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
749 // we have backtracked already
750 if !map
751 .chars_at(point)
752 .nth(1)
753 .map(|(c, _)| c == '\n')
754 .unwrap_or(true)
755 {
756 *point.column_mut() = point.column().saturating_sub(1);
757 }
758 point = map.clip_point(point, Bias::Left);
759 }
760 point
761}
762
763fn previous_word_start(
764 map: &DisplaySnapshot,
765 mut point: DisplayPoint,
766 ignore_punctuation: bool,
767 times: usize,
768) -> DisplayPoint {
769 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
770 for _ in 0..times {
771 // This works even though find_preceding_boundary is called for every character in the line containing
772 // cursor because the newline is checked only once.
773 point =
774 movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
775 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
776 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
777
778 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
779 });
780 }
781 point
782}
783
784pub(crate) fn first_non_whitespace(
785 map: &DisplaySnapshot,
786 display_lines: bool,
787 from: DisplayPoint,
788) -> DisplayPoint {
789 let mut last_point = start_of_line(map, display_lines, from);
790 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
791 for (ch, point) in map.chars_at(last_point) {
792 if ch == '\n' {
793 return from;
794 }
795
796 last_point = point;
797
798 if char_kind(&scope, ch) != CharKind::Whitespace {
799 break;
800 }
801 }
802
803 map.clip_point(last_point, Bias::Left)
804}
805
806pub(crate) fn start_of_line(
807 map: &DisplaySnapshot,
808 display_lines: bool,
809 point: DisplayPoint,
810) -> DisplayPoint {
811 if display_lines {
812 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
813 } else {
814 map.prev_line_boundary(point.to_point(map)).1
815 }
816}
817
818pub(crate) fn end_of_line(
819 map: &DisplaySnapshot,
820 display_lines: bool,
821 point: DisplayPoint,
822) -> DisplayPoint {
823 if display_lines {
824 map.clip_point(
825 DisplayPoint::new(point.row(), map.line_len(point.row())),
826 Bias::Left,
827 )
828 } else {
829 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
830 }
831}
832
833fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
834 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
835 *new_point.column_mut() = point.column();
836 map.clip_point(new_point, Bias::Left)
837}
838
839fn end_of_document(
840 map: &DisplaySnapshot,
841 point: DisplayPoint,
842 line: Option<usize>,
843) -> DisplayPoint {
844 let new_row = if let Some(line) = line {
845 (line - 1) as u32
846 } else {
847 map.max_buffer_row()
848 };
849
850 let new_point = Point::new(new_row, point.column());
851 map.clip_point(new_point.to_display_point(map), Bias::Left)
852}
853
854fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
855 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
856 let point = display_point.to_point(map);
857 let offset = point.to_offset(&map.buffer_snapshot);
858
859 // Ensure the range is contained by the current line.
860 let mut line_end = map.next_line_boundary(point).0;
861 if line_end == point {
862 line_end = map.max_point().to_point(map);
863 }
864
865 let line_range = map.prev_line_boundary(point).0..line_end;
866 let visible_line_range =
867 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
868 let ranges = map
869 .buffer_snapshot
870 .bracket_ranges(visible_line_range.clone());
871 if let Some(ranges) = ranges {
872 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
873 ..line_range.end.to_offset(&map.buffer_snapshot);
874 let mut closest_pair_destination = None;
875 let mut closest_distance = usize::MAX;
876
877 for (open_range, close_range) in ranges {
878 if open_range.start >= offset && line_range.contains(&open_range.start) {
879 let distance = open_range.start - offset;
880 if distance < closest_distance {
881 closest_pair_destination = Some(close_range.start);
882 closest_distance = distance;
883 continue;
884 }
885 }
886
887 if close_range.start >= offset && line_range.contains(&close_range.start) {
888 let distance = close_range.start - offset;
889 if distance < closest_distance {
890 closest_pair_destination = Some(open_range.start);
891 closest_distance = distance;
892 continue;
893 }
894 }
895
896 continue;
897 }
898
899 closest_pair_destination
900 .map(|destination| destination.to_display_point(map))
901 .unwrap_or(display_point)
902 } else {
903 display_point
904 }
905}
906
907fn find_forward(
908 map: &DisplaySnapshot,
909 from: DisplayPoint,
910 before: bool,
911 target: char,
912 times: usize,
913) -> Option<DisplayPoint> {
914 let mut to = from;
915 let mut found = false;
916
917 for _ in 0..times {
918 found = false;
919 to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
920 found = right == target;
921 found
922 });
923 }
924
925 if found {
926 if before && to.column() > 0 {
927 *to.column_mut() -= 1;
928 Some(map.clip_point(to, Bias::Left))
929 } else {
930 Some(to)
931 }
932 } else {
933 None
934 }
935}
936
937fn find_backward(
938 map: &DisplaySnapshot,
939 from: DisplayPoint,
940 after: bool,
941 target: char,
942 times: usize,
943) -> DisplayPoint {
944 let mut to = from;
945
946 for _ in 0..times {
947 to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
948 }
949
950 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
951 if after {
952 *to.column_mut() += 1;
953 map.clip_point(to, Bias::Right)
954 } else {
955 to
956 }
957 } else {
958 from
959 }
960}
961
962fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
963 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
964 first_non_whitespace(map, false, correct_line)
965}
966
967fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
968 let correct_line = start_of_relative_buffer_row(map, point, 0);
969 right(map, correct_line, times.saturating_sub(1))
970}
971
972pub(crate) fn next_line_end(
973 map: &DisplaySnapshot,
974 mut point: DisplayPoint,
975 times: usize,
976) -> DisplayPoint {
977 if times > 1 {
978 point = start_of_relative_buffer_row(map, point, times as isize - 1);
979 }
980 end_of_line(map, false, point)
981}
982
983fn window_top(
984 map: &DisplaySnapshot,
985 point: DisplayPoint,
986 text_layout_details: &TextLayoutDetails,
987) -> (DisplayPoint, SelectionGoal) {
988 let first_visible_line = text_layout_details.anchor.to_display_point(map);
989 let new_col = point.column().min(map.line_len(first_visible_line.row()));
990 let new_point = DisplayPoint::new(first_visible_line.row(), new_col);
991 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
992}
993
994fn window_middle(
995 map: &DisplaySnapshot,
996 point: DisplayPoint,
997 text_layout_details: &TextLayoutDetails,
998) -> (DisplayPoint, SelectionGoal) {
999 if let Some(visible_rows) = text_layout_details.visible_rows {
1000 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1001 let max_rows = (visible_rows as u32).min(map.max_buffer_row());
1002 let new_row = first_visible_line.row() + (max_rows.div_euclid(2));
1003 let new_col = point.column().min(map.line_len(new_row));
1004 let new_point = DisplayPoint::new(new_row, new_col);
1005 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1006 } else {
1007 (point, SelectionGoal::None)
1008 }
1009}
1010
1011fn window_bottom(
1012 map: &DisplaySnapshot,
1013 point: DisplayPoint,
1014 text_layout_details: &TextLayoutDetails,
1015) -> (DisplayPoint, SelectionGoal) {
1016 if let Some(visible_rows) = text_layout_details.visible_rows {
1017 let first_visible_line = text_layout_details.anchor.to_display_point(map);
1018 let bottom_row = first_visible_line.row() + (visible_rows) as u32;
1019 let bottom_row_capped = bottom_row.min(map.max_buffer_row());
1020 let new_col = point.column().min(map.line_len(bottom_row_capped));
1021 let new_point = DisplayPoint::new(bottom_row_capped, new_col);
1022 (map.clip_point(new_point, Bias::Left), SelectionGoal::None)
1023 } else {
1024 (point, SelectionGoal::None)
1025 }
1026}
1027
1028#[cfg(test)]
1029mod test {
1030
1031 use crate::test::NeovimBackedTestContext;
1032 use indoc::indoc;
1033
1034 #[gpui::test]
1035 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
1036 let mut cx = NeovimBackedTestContext::new(cx).await;
1037
1038 let initial_state = indoc! {r"ˇabc
1039 def
1040
1041 paragraph
1042 the second
1043
1044
1045
1046 third and
1047 final"};
1048
1049 // goes down once
1050 cx.set_shared_state(initial_state).await;
1051 cx.simulate_shared_keystrokes(["}"]).await;
1052 cx.assert_shared_state(indoc! {r"abc
1053 def
1054 ˇ
1055 paragraph
1056 the second
1057
1058
1059
1060 third and
1061 final"})
1062 .await;
1063
1064 // goes up once
1065 cx.simulate_shared_keystrokes(["{"]).await;
1066 cx.assert_shared_state(initial_state).await;
1067
1068 // goes down twice
1069 cx.simulate_shared_keystrokes(["2", "}"]).await;
1070 cx.assert_shared_state(indoc! {r"abc
1071 def
1072
1073 paragraph
1074 the second
1075 ˇ
1076
1077
1078 third and
1079 final"})
1080 .await;
1081
1082 // goes down over multiple blanks
1083 cx.simulate_shared_keystrokes(["}"]).await;
1084 cx.assert_shared_state(indoc! {r"abc
1085 def
1086
1087 paragraph
1088 the second
1089
1090
1091
1092 third and
1093 finaˇl"})
1094 .await;
1095
1096 // goes up twice
1097 cx.simulate_shared_keystrokes(["2", "{"]).await;
1098 cx.assert_shared_state(indoc! {r"abc
1099 def
1100 ˇ
1101 paragraph
1102 the second
1103
1104
1105
1106 third and
1107 final"})
1108 .await
1109 }
1110
1111 #[gpui::test]
1112 async fn test_matching(cx: &mut gpui::TestAppContext) {
1113 let mut cx = NeovimBackedTestContext::new(cx).await;
1114
1115 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1116 do(something(with<Types>.and_arrays[0, 2]))
1117 }"})
1118 .await;
1119 cx.simulate_shared_keystrokes(["%"]).await;
1120 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1121 do(something(with<Types>.and_arrays[0, 2]))
1122 }"})
1123 .await;
1124
1125 // test it works on the last character of the line
1126 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1127 do(something(with<Types>.and_arrays[0, 2]))
1128 }"})
1129 .await;
1130 cx.simulate_shared_keystrokes(["%"]).await;
1131 cx.assert_shared_state(indoc! {r"func (a string) {
1132 do(something(with<Types>.and_arrays[0, 2]))
1133 ˇ}"})
1134 .await;
1135
1136 // test it works on immediate nesting
1137 cx.set_shared_state("ˇ{()}").await;
1138 cx.simulate_shared_keystrokes(["%"]).await;
1139 cx.assert_shared_state("{()ˇ}").await;
1140 cx.simulate_shared_keystrokes(["%"]).await;
1141 cx.assert_shared_state("ˇ{()}").await;
1142
1143 // test it works on immediate nesting inside braces
1144 cx.set_shared_state("{\n ˇ{()}\n}").await;
1145 cx.simulate_shared_keystrokes(["%"]).await;
1146 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1147
1148 // test it jumps to the next paren on a line
1149 cx.set_shared_state("func ˇboop() {\n}").await;
1150 cx.simulate_shared_keystrokes(["%"]).await;
1151 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1152 }
1153
1154 #[gpui::test]
1155 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1156 let mut cx = NeovimBackedTestContext::new(cx).await;
1157
1158 cx.set_shared_state("ˇone two three four").await;
1159 cx.simulate_shared_keystrokes(["f", "o"]).await;
1160 cx.assert_shared_state("one twˇo three four").await;
1161 cx.simulate_shared_keystrokes([","]).await;
1162 cx.assert_shared_state("ˇone two three four").await;
1163 cx.simulate_shared_keystrokes(["2", ";"]).await;
1164 cx.assert_shared_state("one two three fˇour").await;
1165 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1166 cx.assert_shared_state("one two threeˇ four").await;
1167 cx.simulate_shared_keystrokes(["3", ";"]).await;
1168 cx.assert_shared_state("oneˇ two three four").await;
1169 cx.simulate_shared_keystrokes([","]).await;
1170 cx.assert_shared_state("one two thˇree four").await;
1171 }
1172
1173 #[gpui::test]
1174 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1175 let mut cx = NeovimBackedTestContext::new(cx).await;
1176 cx.set_shared_state("ˇone\n two\nthree").await;
1177 cx.simulate_shared_keystrokes(["enter"]).await;
1178 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1179 }
1180
1181 #[gpui::test]
1182 async fn test_window_top(cx: &mut gpui::TestAppContext) {
1183 let mut cx = NeovimBackedTestContext::new(cx).await;
1184 let initial_state = indoc! {r"abc
1185 def
1186 paragraph
1187 the second
1188 third ˇand
1189 final"};
1190
1191 cx.set_shared_state(initial_state).await;
1192 cx.simulate_shared_keystrokes(["shift-h"]).await;
1193 cx.assert_shared_state(indoc! {r"abˇc
1194 def
1195 paragraph
1196 the second
1197 third and
1198 final"})
1199 .await;
1200
1201 // clip point
1202 cx.set_shared_state(indoc! {r"
1203 1 2 3
1204 4 5 6
1205 7 8 ˇ9
1206 "})
1207 .await;
1208 cx.simulate_shared_keystrokes(["shift-h"]).await;
1209 cx.assert_shared_state(indoc! {r"
1210 1 2 ˇ3
1211 4 5 6
1212 7 8 9
1213 "})
1214 .await;
1215
1216 cx.set_shared_state(indoc! {r"
1217 1 2 3
1218 4 5 6
1219 ˇ7 8 9
1220 "})
1221 .await;
1222 cx.simulate_shared_keystrokes(["shift-h"]).await;
1223 cx.assert_shared_state(indoc! {r"
1224 ˇ1 2 3
1225 4 5 6
1226 7 8 9
1227 "})
1228 .await;
1229 }
1230
1231 #[gpui::test]
1232 async fn test_window_middle(cx: &mut gpui::TestAppContext) {
1233 let mut cx = NeovimBackedTestContext::new(cx).await;
1234 let initial_state = indoc! {r"abˇc
1235 def
1236 paragraph
1237 the second
1238 third and
1239 final"};
1240
1241 cx.set_shared_state(initial_state).await;
1242 cx.simulate_shared_keystrokes(["shift-m"]).await;
1243 cx.assert_shared_state(indoc! {r"abc
1244 def
1245 paˇragraph
1246 the second
1247 third and
1248 final"})
1249 .await;
1250
1251 cx.set_shared_state(indoc! {r"
1252 1 2 3
1253 4 5 6
1254 7 8 ˇ9
1255 "})
1256 .await;
1257 cx.simulate_shared_keystrokes(["shift-m"]).await;
1258 cx.assert_shared_state(indoc! {r"
1259 1 2 3
1260 4 5 ˇ6
1261 7 8 9
1262 "})
1263 .await;
1264 cx.set_shared_state(indoc! {r"
1265 1 2 3
1266 4 5 6
1267 ˇ7 8 9
1268 "})
1269 .await;
1270 cx.simulate_shared_keystrokes(["shift-m"]).await;
1271 cx.assert_shared_state(indoc! {r"
1272 1 2 3
1273 ˇ4 5 6
1274 7 8 9
1275 "})
1276 .await;
1277 cx.set_shared_state(indoc! {r"
1278 ˇ1 2 3
1279 4 5 6
1280 7 8 9
1281 "})
1282 .await;
1283 cx.simulate_shared_keystrokes(["shift-m"]).await;
1284 cx.assert_shared_state(indoc! {r"
1285 1 2 3
1286 ˇ4 5 6
1287 7 8 9
1288 "})
1289 .await;
1290 cx.set_shared_state(indoc! {r"
1291 1 2 3
1292 ˇ4 5 6
1293 7 8 9
1294 "})
1295 .await;
1296 cx.simulate_shared_keystrokes(["shift-m"]).await;
1297 cx.assert_shared_state(indoc! {r"
1298 1 2 3
1299 ˇ4 5 6
1300 7 8 9
1301 "})
1302 .await;
1303 cx.set_shared_state(indoc! {r"
1304 1 2 3
1305 4 5 ˇ6
1306 7 8 9
1307 "})
1308 .await;
1309 cx.simulate_shared_keystrokes(["shift-m"]).await;
1310 cx.assert_shared_state(indoc! {r"
1311 1 2 3
1312 4 5 ˇ6
1313 7 8 9
1314 "})
1315 .await;
1316 }
1317
1318 #[gpui::test]
1319 async fn test_window_bottom(cx: &mut gpui::TestAppContext) {
1320 let mut cx = NeovimBackedTestContext::new(cx).await;
1321 let initial_state = indoc! {r"abc
1322 deˇf
1323 paragraph
1324 the second
1325 third and
1326 final"};
1327
1328 cx.set_shared_state(initial_state).await;
1329 cx.simulate_shared_keystrokes(["shift-l"]).await;
1330 cx.assert_shared_state(indoc! {r"abc
1331 def
1332 paragraph
1333 the second
1334 third and
1335 fiˇnal"})
1336 .await;
1337
1338 cx.set_shared_state(indoc! {r"
1339 1 2 3
1340 4 5 ˇ6
1341 7 8 9
1342 "})
1343 .await;
1344 cx.simulate_shared_keystrokes(["shift-l"]).await;
1345 cx.assert_shared_state(indoc! {r"
1346 1 2 3
1347 4 5 6
1348 7 8 9
1349 ˇ"})
1350 .await;
1351
1352 cx.set_shared_state(indoc! {r"
1353 1 2 3
1354 ˇ4 5 6
1355 7 8 9
1356 "})
1357 .await;
1358 cx.simulate_shared_keystrokes(["shift-l"]).await;
1359 cx.assert_shared_state(indoc! {r"
1360 1 2 3
1361 4 5 6
1362 7 8 9
1363 ˇ"})
1364 .await;
1365
1366 cx.set_shared_state(indoc! {r"
1367 1 2 ˇ3
1368 4 5 6
1369 7 8 9
1370 "})
1371 .await;
1372 cx.simulate_shared_keystrokes(["shift-l"]).await;
1373 cx.assert_shared_state(indoc! {r"
1374 1 2 3
1375 4 5 6
1376 7 8 9
1377 ˇ"})
1378 .await;
1379
1380 cx.set_shared_state(indoc! {r"
1381 ˇ1 2 3
1382 4 5 6
1383 7 8 9
1384 "})
1385 .await;
1386 cx.simulate_shared_keystrokes(["shift-l"]).await;
1387 cx.assert_shared_state(indoc! {r"
1388 1 2 3
1389 4 5 6
1390 7 8 9
1391 ˇ"})
1392 .await;
1393 }
1394}