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