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 if let Some(new_point) = find_forward(map, point, *before, *char, times) {
439 return Some((new_point, SelectionGoal::None));
440 } else {
441 return None;
442 }
443 }
444 FindBackward { after, char } => (
445 find_backward(map, point, *after, *char, times),
446 SelectionGoal::None,
447 ),
448 NextLineStart => (next_line_start(map, point, times), SelectionGoal::None),
449 StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None),
450 EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None),
451 GoToColumn => (go_to_column(map, point, times), SelectionGoal::None),
452 };
453
454 (new_point != point || infallible).then_some((new_point, goal))
455 }
456
457 // Expands a selection using self motion for an operator
458 pub fn expand_selection(
459 &self,
460 map: &DisplaySnapshot,
461 selection: &mut Selection<DisplayPoint>,
462 times: Option<usize>,
463 expand_to_surrounding_newline: bool,
464 text_layout_details: &TextLayoutDetails,
465 ) -> bool {
466 if let Some((new_head, goal)) = self.move_point(
467 map,
468 selection.head(),
469 selection.goal,
470 times,
471 &text_layout_details,
472 ) {
473 selection.set_head(new_head, goal);
474
475 if self.linewise() {
476 selection.start = map.prev_line_boundary(selection.start.to_point(map)).1;
477
478 if expand_to_surrounding_newline {
479 if selection.end.row() < map.max_point().row() {
480 *selection.end.row_mut() += 1;
481 *selection.end.column_mut() = 0;
482 selection.end = map.clip_point(selection.end, Bias::Right);
483 // Don't reset the end here
484 return true;
485 } else if selection.start.row() > 0 {
486 *selection.start.row_mut() -= 1;
487 *selection.start.column_mut() = map.line_len(selection.start.row());
488 selection.start = map.clip_point(selection.start, Bias::Left);
489 }
490 }
491
492 (_, selection.end) = map.next_line_boundary(selection.end.to_point(map));
493 } else {
494 // Another special case: When using the "w" motion in combination with an
495 // operator and the last word moved over is at the end of a line, the end of
496 // that word becomes the end of the operated text, not the first word in the
497 // next line.
498 if let Motion::NextWordStart {
499 ignore_punctuation: _,
500 } = self
501 {
502 let start_row = selection.start.to_point(&map).row;
503 if selection.end.to_point(&map).row > start_row {
504 selection.end =
505 Point::new(start_row, map.buffer_snapshot.line_len(start_row))
506 .to_display_point(&map)
507 }
508 }
509
510 // If the motion is exclusive and the end of the motion is in column 1, the
511 // end of the motion is moved to the end of the previous line and the motion
512 // becomes inclusive. Example: "}" moves to the first line after a paragraph,
513 // but "d}" will not include that line.
514 let mut inclusive = self.inclusive();
515 if !inclusive
516 && self != &Motion::Backspace
517 && selection.end.row() > selection.start.row()
518 && selection.end.column() == 0
519 {
520 inclusive = true;
521 *selection.end.row_mut() -= 1;
522 *selection.end.column_mut() = 0;
523 selection.end = map.clip_point(
524 map.next_line_boundary(selection.end.to_point(map)).1,
525 Bias::Left,
526 );
527 }
528
529 if inclusive && selection.end.column() < map.line_len(selection.end.row()) {
530 *selection.end.column_mut() += 1;
531 }
532 }
533 true
534 } else {
535 false
536 }
537 }
538}
539
540fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
541 for _ in 0..times {
542 point = movement::saturating_left(map, point);
543 if point.column() == 0 {
544 break;
545 }
546 }
547 point
548}
549
550fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
551 for _ in 0..times {
552 point = movement::left(map, point);
553 }
554 point
555}
556
557pub(crate) fn start_of_relative_buffer_row(
558 map: &DisplaySnapshot,
559 point: DisplayPoint,
560 times: isize,
561) -> DisplayPoint {
562 let start = map.display_point_to_fold_point(point, Bias::Left);
563 let target = start.row() as isize + times;
564 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
565
566 map.clip_point(
567 map.fold_point_to_display_point(
568 map.fold_snapshot
569 .clip_point(FoldPoint::new(new_row, 0), Bias::Right),
570 ),
571 Bias::Right,
572 )
573}
574
575fn up_down_buffer_rows(
576 map: &DisplaySnapshot,
577 point: DisplayPoint,
578 mut goal: SelectionGoal,
579 times: isize,
580 text_layout_details: &TextLayoutDetails,
581) -> (DisplayPoint, SelectionGoal) {
582 let start = map.display_point_to_fold_point(point, Bias::Left);
583 let begin_folded_line = map.fold_point_to_display_point(
584 map.fold_snapshot
585 .clip_point(FoldPoint::new(start.row(), 0), Bias::Left),
586 );
587 let select_nth_wrapped_row = point.row() - begin_folded_line.row();
588
589 let (goal_wrap, goal_x) = match goal {
590 SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x),
591 SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end),
592 SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x),
593 _ => {
594 let x = map.x_for_display_point(point, text_layout_details);
595 goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.0));
596 (select_nth_wrapped_row, x.0)
597 }
598 };
599
600 let target = start.row() as isize + times;
601 let new_row = (target.max(0) as u32).min(map.fold_snapshot.max_point().row());
602
603 let mut begin_folded_line = map.fold_point_to_display_point(
604 map.fold_snapshot
605 .clip_point(FoldPoint::new(new_row, 0), Bias::Left),
606 );
607
608 let mut i = 0;
609 while i < goal_wrap && begin_folded_line.row() < map.max_point().row() {
610 let next_folded_line = DisplayPoint::new(begin_folded_line.row() + 1, 0);
611 if map
612 .display_point_to_fold_point(next_folded_line, Bias::Right)
613 .row()
614 == new_row
615 {
616 i += 1;
617 begin_folded_line = next_folded_line;
618 } else {
619 break;
620 }
621 }
622
623 let new_col = if i == goal_wrap {
624 map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details)
625 } else {
626 map.line_len(begin_folded_line.row())
627 };
628
629 (
630 map.clip_point(
631 DisplayPoint::new(begin_folded_line.row(), new_col),
632 Bias::Left,
633 ),
634 goal,
635 )
636}
637
638fn down_display(
639 map: &DisplaySnapshot,
640 mut point: DisplayPoint,
641 mut goal: SelectionGoal,
642 times: usize,
643 text_layout_details: &TextLayoutDetails,
644) -> (DisplayPoint, SelectionGoal) {
645 for _ in 0..times {
646 (point, goal) = movement::down(map, point, goal, true, text_layout_details);
647 }
648
649 (point, goal)
650}
651
652fn up_display(
653 map: &DisplaySnapshot,
654 mut point: DisplayPoint,
655 mut goal: SelectionGoal,
656 times: usize,
657 text_layout_details: &TextLayoutDetails,
658) -> (DisplayPoint, SelectionGoal) {
659 for _ in 0..times {
660 (point, goal) = movement::up(map, point, goal, true, &text_layout_details);
661 }
662
663 (point, goal)
664}
665
666pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint {
667 for _ in 0..times {
668 let new_point = movement::saturating_right(map, point);
669 if point == new_point {
670 break;
671 }
672 point = new_point;
673 }
674 point
675}
676
677pub(crate) fn next_word_start(
678 map: &DisplaySnapshot,
679 mut point: DisplayPoint,
680 ignore_punctuation: bool,
681 times: usize,
682) -> DisplayPoint {
683 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
684 for _ in 0..times {
685 let mut crossed_newline = false;
686 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
687 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
688 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
689 let at_newline = right == '\n';
690
691 let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
692 || at_newline && crossed_newline
693 || at_newline && left == '\n'; // Prevents skipping repeated empty lines
694
695 crossed_newline |= at_newline;
696 found
697 })
698 }
699 point
700}
701
702fn next_word_end(
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 if point.column() < map.line_len(point.row()) {
711 *point.column_mut() += 1;
712 } else if point.row() < map.max_buffer_row() {
713 *point.row_mut() += 1;
714 *point.column_mut() = 0;
715 }
716 point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| {
717 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
718 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
719
720 left_kind != right_kind && left_kind != CharKind::Whitespace
721 });
722
723 // find_boundary clips, so if the character after the next character is a newline or at the end of the document, we know
724 // we have backtracked already
725 if !map
726 .chars_at(point)
727 .nth(1)
728 .map(|(c, _)| c == '\n')
729 .unwrap_or(true)
730 {
731 *point.column_mut() = point.column().saturating_sub(1);
732 }
733 point = map.clip_point(point, Bias::Left);
734 }
735 point
736}
737
738fn previous_word_start(
739 map: &DisplaySnapshot,
740 mut point: DisplayPoint,
741 ignore_punctuation: bool,
742 times: usize,
743) -> DisplayPoint {
744 let scope = map.buffer_snapshot.language_scope_at(point.to_point(map));
745 for _ in 0..times {
746 // This works even though find_preceding_boundary is called for every character in the line containing
747 // cursor because the newline is checked only once.
748 point =
749 movement::find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
750 let left_kind = coerce_punctuation(char_kind(&scope, left), ignore_punctuation);
751 let right_kind = coerce_punctuation(char_kind(&scope, right), ignore_punctuation);
752
753 (left_kind != right_kind && !right.is_whitespace()) || left == '\n'
754 });
755 }
756 point
757}
758
759pub(crate) fn first_non_whitespace(
760 map: &DisplaySnapshot,
761 display_lines: bool,
762 from: DisplayPoint,
763) -> DisplayPoint {
764 let mut last_point = start_of_line(map, display_lines, from);
765 let scope = map.buffer_snapshot.language_scope_at(from.to_point(map));
766 for (ch, point) in map.chars_at(last_point) {
767 if ch == '\n' {
768 return from;
769 }
770
771 last_point = point;
772
773 if char_kind(&scope, ch) != CharKind::Whitespace {
774 break;
775 }
776 }
777
778 map.clip_point(last_point, Bias::Left)
779}
780
781pub(crate) fn start_of_line(
782 map: &DisplaySnapshot,
783 display_lines: bool,
784 point: DisplayPoint,
785) -> DisplayPoint {
786 if display_lines {
787 map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right)
788 } else {
789 map.prev_line_boundary(point.to_point(map)).1
790 }
791}
792
793pub(crate) fn end_of_line(
794 map: &DisplaySnapshot,
795 display_lines: bool,
796 point: DisplayPoint,
797) -> DisplayPoint {
798 if display_lines {
799 map.clip_point(
800 DisplayPoint::new(point.row(), map.line_len(point.row())),
801 Bias::Left,
802 )
803 } else {
804 map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left)
805 }
806}
807
808fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint {
809 let mut new_point = Point::new((line - 1) as u32, 0).to_display_point(map);
810 *new_point.column_mut() = point.column();
811 map.clip_point(new_point, Bias::Left)
812}
813
814fn end_of_document(
815 map: &DisplaySnapshot,
816 point: DisplayPoint,
817 line: Option<usize>,
818) -> DisplayPoint {
819 let new_row = if let Some(line) = line {
820 (line - 1) as u32
821 } else {
822 map.max_buffer_row()
823 };
824
825 let new_point = Point::new(new_row, point.column());
826 map.clip_point(new_point.to_display_point(map), Bias::Left)
827}
828
829fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
830 // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200
831 let point = display_point.to_point(map);
832 let offset = point.to_offset(&map.buffer_snapshot);
833
834 // Ensure the range is contained by the current line.
835 let mut line_end = map.next_line_boundary(point).0;
836 if line_end == point {
837 line_end = map.max_point().to_point(map);
838 }
839
840 let line_range = map.prev_line_boundary(point).0..line_end;
841 let visible_line_range =
842 line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1));
843 let ranges = map
844 .buffer_snapshot
845 .bracket_ranges(visible_line_range.clone());
846 if let Some(ranges) = ranges {
847 let line_range = line_range.start.to_offset(&map.buffer_snapshot)
848 ..line_range.end.to_offset(&map.buffer_snapshot);
849 let mut closest_pair_destination = None;
850 let mut closest_distance = usize::MAX;
851
852 for (open_range, close_range) in ranges {
853 if open_range.start >= offset && line_range.contains(&open_range.start) {
854 let distance = open_range.start - offset;
855 if distance < closest_distance {
856 closest_pair_destination = Some(close_range.start);
857 closest_distance = distance;
858 continue;
859 }
860 }
861
862 if close_range.start >= offset && line_range.contains(&close_range.start) {
863 let distance = close_range.start - offset;
864 if distance < closest_distance {
865 closest_pair_destination = Some(open_range.start);
866 closest_distance = distance;
867 continue;
868 }
869 }
870
871 continue;
872 }
873
874 closest_pair_destination
875 .map(|destination| destination.to_display_point(map))
876 .unwrap_or(display_point)
877 } else {
878 display_point
879 }
880}
881
882fn find_forward(
883 map: &DisplaySnapshot,
884 from: DisplayPoint,
885 before: bool,
886 target: char,
887 times: usize,
888) -> Option<DisplayPoint> {
889 let mut to = from;
890 let mut found = false;
891
892 for _ in 0..times {
893 found = false;
894 to = find_boundary(map, to, FindRange::SingleLine, |_, right| {
895 found = right == target;
896 found
897 });
898 }
899
900 if found {
901 if before && to.column() > 0 {
902 *to.column_mut() -= 1;
903 Some(map.clip_point(to, Bias::Left))
904 } else {
905 Some(to)
906 }
907 } else {
908 None
909 }
910}
911
912fn find_backward(
913 map: &DisplaySnapshot,
914 from: DisplayPoint,
915 after: bool,
916 target: char,
917 times: usize,
918) -> DisplayPoint {
919 let mut to = from;
920
921 for _ in 0..times {
922 to = find_preceding_boundary(map, to, FindRange::SingleLine, |_, right| right == target);
923 }
924
925 if map.buffer_snapshot.chars_at(to.to_point(map)).next() == Some(target) {
926 if after {
927 *to.column_mut() += 1;
928 map.clip_point(to, Bias::Right)
929 } else {
930 to
931 }
932 } else {
933 from
934 }
935}
936
937fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
938 let correct_line = start_of_relative_buffer_row(map, point, times as isize);
939 first_non_whitespace(map, false, correct_line)
940}
941
942fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint {
943 let correct_line = start_of_relative_buffer_row(map, point, 0);
944 right(map, correct_line, times.saturating_sub(1))
945}
946
947pub(crate) fn next_line_end(
948 map: &DisplaySnapshot,
949 mut point: DisplayPoint,
950 times: usize,
951) -> DisplayPoint {
952 if times > 1 {
953 point = start_of_relative_buffer_row(map, point, times as isize - 1);
954 }
955 end_of_line(map, false, point)
956}
957
958#[cfg(test)]
959mod test {
960
961 use crate::test::NeovimBackedTestContext;
962 use indoc::indoc;
963
964 #[gpui::test]
965 async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
966 let mut cx = NeovimBackedTestContext::new(cx).await;
967
968 let initial_state = indoc! {r"ˇabc
969 def
970
971 paragraph
972 the second
973
974
975
976 third and
977 final"};
978
979 // goes down once
980 cx.set_shared_state(initial_state).await;
981 cx.simulate_shared_keystrokes(["}"]).await;
982 cx.assert_shared_state(indoc! {r"abc
983 def
984 ˇ
985 paragraph
986 the second
987
988
989
990 third and
991 final"})
992 .await;
993
994 // goes up once
995 cx.simulate_shared_keystrokes(["{"]).await;
996 cx.assert_shared_state(initial_state).await;
997
998 // goes down twice
999 cx.simulate_shared_keystrokes(["2", "}"]).await;
1000 cx.assert_shared_state(indoc! {r"abc
1001 def
1002
1003 paragraph
1004 the second
1005 ˇ
1006
1007
1008 third and
1009 final"})
1010 .await;
1011
1012 // goes down over multiple blanks
1013 cx.simulate_shared_keystrokes(["}"]).await;
1014 cx.assert_shared_state(indoc! {r"abc
1015 def
1016
1017 paragraph
1018 the second
1019
1020
1021
1022 third and
1023 finaˇl"})
1024 .await;
1025
1026 // goes up twice
1027 cx.simulate_shared_keystrokes(["2", "{"]).await;
1028 cx.assert_shared_state(indoc! {r"abc
1029 def
1030 ˇ
1031 paragraph
1032 the second
1033
1034
1035
1036 third and
1037 final"})
1038 .await
1039 }
1040
1041 #[gpui::test]
1042 async fn test_matching(cx: &mut gpui::TestAppContext) {
1043 let mut cx = NeovimBackedTestContext::new(cx).await;
1044
1045 cx.set_shared_state(indoc! {r"func ˇ(a string) {
1046 do(something(with<Types>.and_arrays[0, 2]))
1047 }"})
1048 .await;
1049 cx.simulate_shared_keystrokes(["%"]).await;
1050 cx.assert_shared_state(indoc! {r"func (a stringˇ) {
1051 do(something(with<Types>.and_arrays[0, 2]))
1052 }"})
1053 .await;
1054
1055 // test it works on the last character of the line
1056 cx.set_shared_state(indoc! {r"func (a string) ˇ{
1057 do(something(with<Types>.and_arrays[0, 2]))
1058 }"})
1059 .await;
1060 cx.simulate_shared_keystrokes(["%"]).await;
1061 cx.assert_shared_state(indoc! {r"func (a string) {
1062 do(something(with<Types>.and_arrays[0, 2]))
1063 ˇ}"})
1064 .await;
1065
1066 // test it works on immediate nesting
1067 cx.set_shared_state("ˇ{()}").await;
1068 cx.simulate_shared_keystrokes(["%"]).await;
1069 cx.assert_shared_state("{()ˇ}").await;
1070 cx.simulate_shared_keystrokes(["%"]).await;
1071 cx.assert_shared_state("ˇ{()}").await;
1072
1073 // test it works on immediate nesting inside braces
1074 cx.set_shared_state("{\n ˇ{()}\n}").await;
1075 cx.simulate_shared_keystrokes(["%"]).await;
1076 cx.assert_shared_state("{\n {()ˇ}\n}").await;
1077
1078 // test it jumps to the next paren on a line
1079 cx.set_shared_state("func ˇboop() {\n}").await;
1080 cx.simulate_shared_keystrokes(["%"]).await;
1081 cx.assert_shared_state("func boop(ˇ) {\n}").await;
1082 }
1083
1084 #[gpui::test]
1085 async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) {
1086 let mut cx = NeovimBackedTestContext::new(cx).await;
1087
1088 cx.set_shared_state("ˇone two three four").await;
1089 cx.simulate_shared_keystrokes(["f", "o"]).await;
1090 cx.assert_shared_state("one twˇo three four").await;
1091 cx.simulate_shared_keystrokes([","]).await;
1092 cx.assert_shared_state("ˇone two three four").await;
1093 cx.simulate_shared_keystrokes(["2", ";"]).await;
1094 cx.assert_shared_state("one two three fˇour").await;
1095 cx.simulate_shared_keystrokes(["shift-t", "e"]).await;
1096 cx.assert_shared_state("one two threeˇ four").await;
1097 cx.simulate_shared_keystrokes(["3", ";"]).await;
1098 cx.assert_shared_state("oneˇ two three four").await;
1099 cx.simulate_shared_keystrokes([","]).await;
1100 cx.assert_shared_state("one two thˇree four").await;
1101 }
1102
1103 #[gpui::test]
1104 async fn test_next_line_start(cx: &mut gpui::TestAppContext) {
1105 let mut cx = NeovimBackedTestContext::new(cx).await;
1106 cx.set_shared_state("ˇone\n two\nthree").await;
1107 cx.simulate_shared_keystrokes(["enter"]).await;
1108 cx.assert_shared_state("one\n ˇtwo\nthree").await;
1109 }
1110}